GC: Restore: SharePoint: Page Logic (#2225)

## Description
Restore logic for restoring a SharePoint Page to M365 given a valid `[]byte`.  
Delete API also included

Tests included
<!-- Insert PR description-->

## Does this PR need a docs update or release note?
- [x]  No 

## Type of change

<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature


## Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* related to  #2169<issue>

## Test Plan

<!-- How will this be tested prior to merging.-->
- [x]  Unit test
Must be tested locally due to CI Library issues, See #2086.
Clean-up is handled within the tests.
This commit is contained in:
Danny 2023-02-06 09:42:50 -05:00 committed by GitHub
parent ac8fe1e9c1
commit d82b5cacdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 199 additions and 16 deletions

View File

@ -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)
}

View File

@ -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,
}
}

View File

@ -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)
}

View File

@ -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
}