diff --git a/src/internal/connector/sharepoint/api/helper_test.go b/src/internal/connector/sharepoint/api/helper_test.go index 631dd7b3b..33dee1561 100644 --- a/src/internal/connector/sharepoint/api/helper_test.go +++ b/src/internal/connector/sharepoint/api/helper_test.go @@ -1,15 +1,15 @@ -package api +package api_test import ( "testing" - "github.com/alcionai/corso/src/internal/connector/discovery/api" + discover "github.com/alcionai/corso/src/internal/connector/discovery/api" "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/pkg/account" "github.com/stretchr/testify/require" ) -func createTestBetaService(t *testing.T, credentials account.M365Config) *api.BetaService { +func createTestBetaService(t *testing.T, credentials account.M365Config) *discover.BetaService { adapter, err := graph.CreateAdapter( credentials.AzureTenantID, credentials.AzureClientID, @@ -17,5 +17,5 @@ func createTestBetaService(t *testing.T, credentials account.M365Config) *api.Be ) require.NoError(t, err) - return api.NewBetaService(adapter) + return discover.NewBetaService(adapter) } diff --git a/src/internal/connector/sharepoint/api/pages.go b/src/internal/connector/sharepoint/api/pages.go index a2232140c..16eb3f0ae 100644 --- a/src/internal/connector/sharepoint/api/pages.go +++ b/src/internal/connector/sharepoint/api/pages.go @@ -2,18 +2,26 @@ package api import ( "context" + "fmt" + "io" + "time" - "github.com/alcionai/corso/src/internal/connector/discovery/api" + "github.com/pkg/errors" + + discover "github.com/alcionai/corso/src/internal/connector/discovery/api" "github.com/alcionai/corso/src/internal/connector/graph/betasdk/models" "github.com/alcionai/corso/src/internal/connector/graph/betasdk/sites" "github.com/alcionai/corso/src/internal/connector/support" + "github.com/alcionai/corso/src/internal/data" + D "github.com/alcionai/corso/src/internal/diagnostics" + "github.com/alcionai/corso/src/pkg/backup/details" ) // GetSitePages retrieves a collection of Pages related to the give Site. // Returns error if error experienced during the call func GetSitePage( ctx context.Context, - serv *api.BetaService, + serv *discover.BetaService, siteID string, pages []string, ) ([]models.SitePageable, error) { @@ -33,7 +41,7 @@ func GetSitePage( } // fetchPages utility function to return the tuple of item -func FetchPages(ctx context.Context, bs *api.BetaService, siteID string) ([]Tuple, error) { +func FetchPages(ctx context.Context, bs *discover.BetaService, siteID string) ([]Tuple, error) { var ( builder = bs.Client().SitesById(siteID).Pages() opts = fetchPageOptions() @@ -80,6 +88,21 @@ func fetchPageOptions() *sites.ItemPagesRequestBuilderGetRequestConfiguration { return options } +// DeleteSitePage removes the selected page from the SharePoint Site +// https://learn.microsoft.com/en-us/graph/api/sitepage-delete?view=graph-rest-beta +func DeleteSitePage( + ctx context.Context, + serv *discover.BetaService, + siteID, pageID string, +) error { + err := serv.Client().SitesById(siteID).PagesById(pageID).Delete(ctx, nil) + if err != nil { + return support.ConnectorStackErrorTraceWrap(err, "deleting page: "+pageID) + } + + return nil +} + // retrievePageOptions returns options to expand func retrieveSitePageOptions() *sites.ItemPagesSitePageItemRequestBuilderGetRequestConfiguration { fields := []string{"canvasLayout"} @@ -91,3 +114,113 @@ func retrieveSitePageOptions() *sites.ItemPagesSitePageItemRequestBuilderGetRequ return options } + +func RestoreSitePage( + ctx context.Context, + service *discover.BetaService, + itemData data.Stream, + siteID, destName string, +) (details.ItemInfo, error) { + ctx, end := D.Span(ctx, "gc:sharepoint:restorePage", D.Label("item_uuid", itemData.UUID())) + defer end() + + var ( + dii = details.ItemInfo{} + pageID = itemData.UUID() + pageName = pageID + ) + + byteArray, err := io.ReadAll(itemData.ToReader()) + if err != nil { + return dii, errors.Wrap(err, "reading sharepoint page bytes from stream") + } + + // Hydrate Page + page, err := support.CreatePageFromBytes(byteArray) + if err != nil { + return dii, errors.Wrapf(err, "creating Page object %s", pageID) + } + + pageNamePtr := page.GetName() + if pageNamePtr != nil { + pageName = *pageNamePtr + } + + newName := fmt.Sprintf("%s_%s", destName, pageName) + page.SetName(&newName) + + // Restore is a 2-Step Process in Graph API + // 1. Create the Page on the site + // 2. Publish the site + // See: https://learn.microsoft.com/en-us/graph/api/sitepage-create?view=graph-rest-beta + restoredPage, err := service.Client().SitesById(siteID).Pages().Post(ctx, page, nil) + if err != nil { + sendErr := support.ConnectorStackErrorTraceWrap( + err, + "creating page from ID: %s"+pageName+" API Error Details", + ) + + return dii, sendErr + } + + pageID = *restoredPage.GetId() + // Publish page to make visible + // See https://learn.microsoft.com/en-us/graph/api/sitepage-publish?view=graph-rest-beta + if restoredPage.GetWebUrl() == nil { + return dii, fmt.Errorf("creating page %s incomplete. Field `webURL` not populated", pageID) + } + + err = service.Client(). + SitesById(siteID). + PagesById(pageID). + Publish(). + Post(ctx, nil) + if err != nil { + return dii, support.ConnectorStackErrorTraceWrap( + err, + "publishing page ID: "+*restoredPage.GetId()+" API Error Details", + ) + } + + dii.SharePoint = PageInfo(restoredPage, int64(len(byteArray))) + // Storing new pageID in unused field. + dii.SharePoint.ParentPath = pageID + + return dii, nil +} + +// ============================== +// Helpers +// ============================== +// PageInfo extracts useful metadata into struct for book keeping +func PageInfo(page models.SitePageable, size int64) *details.SharePointInfo { + var ( + name, webURL string + created, modified time.Time + ) + + if page.GetTitle() != nil { + name = *page.GetTitle() + } + + if page.GetWebUrl() != nil { + webURL = *page.GetWebUrl() + } + + if page.GetCreatedDateTime() != nil { + created = *page.GetCreatedDateTime() + } + + if page.GetLastModifiedDateTime() != nil { + modified = *page.GetLastModifiedDateTime() + } + + return &details.SharePointInfo{ + ItemType: details.SharePointItem, + ItemName: name, + Created: created, + Modified: modified, + WebURL: webURL, + Size: size, + } +} diff --git a/src/internal/connector/sharepoint/api/pages_test.go b/src/internal/connector/sharepoint/api/pages_test.go index ecc2cf18d..c6295748f 100644 --- a/src/internal/connector/sharepoint/api/pages_test.go +++ b/src/internal/connector/sharepoint/api/pages_test.go @@ -1,20 +1,28 @@ -package api +package api_test import ( + "bytes" + "io" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/alcionai/corso/src/internal/common" + discover "github.com/alcionai/corso/src/internal/connector/discovery/api" + "github.com/alcionai/corso/src/internal/connector/mockconnector" + "github.com/alcionai/corso/src/internal/connector/sharepoint" + "github.com/alcionai/corso/src/internal/connector/sharepoint/api" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/account" ) type SharePointPageSuite struct { suite.Suite - siteID string - creds account.M365Config + siteID string + creds account.M365Config + service *discover.BetaService } func (suite *SharePointPageSuite) SetupSuite() { @@ -27,6 +35,7 @@ func (suite *SharePointPageSuite) SetupSuite() { require.NoError(t, err) suite.creds = m365 + suite.service = createTestBetaService(t, suite.creds) } func TestSharePointPageSuite(t *testing.T) { @@ -42,9 +51,7 @@ func (suite *SharePointPageSuite) TestFetchPages() { defer flush() t := suite.T() - service := createTestBetaService(t, suite.creds) - - pgs, err := FetchPages(ctx, service, suite.siteID) + pgs, err := api.FetchPages(ctx, suite.service, suite.siteID) assert.NoError(t, err) require.NotNil(t, pgs) assert.NotZero(t, len(pgs)) @@ -59,13 +66,47 @@ func (suite *SharePointPageSuite) TestGetSitePage() { defer flush() t := suite.T() - service := createTestBetaService(t, suite.creds) - tuples, err := FetchPages(ctx, service, suite.siteID) + tuples, err := api.FetchPages(ctx, suite.service, suite.siteID) require.NoError(t, err) require.NotNil(t, tuples) jobs := []string{tuples[0].ID} - pages, err := GetSitePage(ctx, service, suite.siteID, jobs) + pages, err := api.GetSitePage(ctx, suite.service, suite.siteID, jobs) assert.NoError(t, err) assert.NotEmpty(t, pages) } + +func (suite *SharePointPageSuite) TestRestoreSinglePage() { + ctx, flush := tester.NewContext() + defer flush() + + t := suite.T() + + destName := "Corso_Restore_" + common.FormatNow(common.SimpleTimeTesting) + testName := "MockPage" + + // Create Test Page + //nolint:lll + byteArray := mockconnector.GetMockPage("Byte Test") + + pageData := sharepoint.NewItem( + testName, + io.NopCloser(bytes.NewReader(byteArray)), + ) + + info, err := api.RestoreSitePage( + ctx, + suite.service, + pageData, + suite.siteID, + destName, + ) + + require.NoError(t, err) + require.NotNil(t, info) + + // Clean Up + pageID := info.SharePoint.ParentPath + err = api.DeleteSitePage(ctx, suite.service, suite.siteID, pageID) + assert.NoError(t, err) +} diff --git a/src/internal/connector/sharepoint/collection.go b/src/internal/connector/sharepoint/collection.go index c540af4e6..603edd685 100644 --- a/src/internal/connector/sharepoint/collection.go +++ b/src/internal/connector/sharepoint/collection.go @@ -106,6 +106,15 @@ type Item struct { deleted bool } +func NewItem(name string, d io.ReadCloser) *Item { + item := &Item{ + id: name, + data: d, + } + + return item +} + func (sd *Item) UUID() string { return sd.id }