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:
parent
ac8fe1e9c1
commit
d82b5cacdf
@ -1,15 +1,15 @@
|
|||||||
package api
|
package api_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"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/internal/connector/graph"
|
||||||
"github.com/alcionai/corso/src/pkg/account"
|
"github.com/alcionai/corso/src/pkg/account"
|
||||||
"github.com/stretchr/testify/require"
|
"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(
|
adapter, err := graph.CreateAdapter(
|
||||||
credentials.AzureTenantID,
|
credentials.AzureTenantID,
|
||||||
credentials.AzureClientID,
|
credentials.AzureClientID,
|
||||||
@ -17,5 +17,5 @@ func createTestBetaService(t *testing.T, credentials account.M365Config) *api.Be
|
|||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return api.NewBetaService(adapter)
|
return discover.NewBetaService(adapter)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,18 +2,26 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"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/models"
|
||||||
"github.com/alcionai/corso/src/internal/connector/graph/betasdk/sites"
|
"github.com/alcionai/corso/src/internal/connector/graph/betasdk/sites"
|
||||||
"github.com/alcionai/corso/src/internal/connector/support"
|
"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.
|
// GetSitePages retrieves a collection of Pages related to the give Site.
|
||||||
// Returns error if error experienced during the call
|
// Returns error if error experienced during the call
|
||||||
func GetSitePage(
|
func GetSitePage(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
serv *api.BetaService,
|
serv *discover.BetaService,
|
||||||
siteID string,
|
siteID string,
|
||||||
pages []string,
|
pages []string,
|
||||||
) ([]models.SitePageable, error) {
|
) ([]models.SitePageable, error) {
|
||||||
@ -33,7 +41,7 @@ func GetSitePage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetchPages utility function to return the tuple of item
|
// 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 (
|
var (
|
||||||
builder = bs.Client().SitesById(siteID).Pages()
|
builder = bs.Client().SitesById(siteID).Pages()
|
||||||
opts = fetchPageOptions()
|
opts = fetchPageOptions()
|
||||||
@ -80,6 +88,21 @@ func fetchPageOptions() *sites.ItemPagesRequestBuilderGetRequestConfiguration {
|
|||||||
return options
|
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
|
// retrievePageOptions returns options to expand
|
||||||
func retrieveSitePageOptions() *sites.ItemPagesSitePageItemRequestBuilderGetRequestConfiguration {
|
func retrieveSitePageOptions() *sites.ItemPagesSitePageItemRequestBuilderGetRequestConfiguration {
|
||||||
fields := []string{"canvasLayout"}
|
fields := []string{"canvasLayout"}
|
||||||
@ -91,3 +114,113 @@ func retrieveSitePageOptions() *sites.ItemPagesSitePageItemRequestBuilderGetRequ
|
|||||||
|
|
||||||
return options
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,20 +1,28 @@
|
|||||||
package api
|
package api_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"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/internal/tester"
|
||||||
"github.com/alcionai/corso/src/pkg/account"
|
"github.com/alcionai/corso/src/pkg/account"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SharePointPageSuite struct {
|
type SharePointPageSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
siteID string
|
siteID string
|
||||||
creds account.M365Config
|
creds account.M365Config
|
||||||
|
service *discover.BetaService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *SharePointPageSuite) SetupSuite() {
|
func (suite *SharePointPageSuite) SetupSuite() {
|
||||||
@ -27,6 +35,7 @@ func (suite *SharePointPageSuite) SetupSuite() {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
suite.creds = m365
|
suite.creds = m365
|
||||||
|
suite.service = createTestBetaService(t, suite.creds)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSharePointPageSuite(t *testing.T) {
|
func TestSharePointPageSuite(t *testing.T) {
|
||||||
@ -42,9 +51,7 @@ func (suite *SharePointPageSuite) TestFetchPages() {
|
|||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
service := createTestBetaService(t, suite.creds)
|
pgs, err := api.FetchPages(ctx, suite.service, suite.siteID)
|
||||||
|
|
||||||
pgs, err := FetchPages(ctx, service, suite.siteID)
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
require.NotNil(t, pgs)
|
require.NotNil(t, pgs)
|
||||||
assert.NotZero(t, len(pgs))
|
assert.NotZero(t, len(pgs))
|
||||||
@ -59,13 +66,47 @@ func (suite *SharePointPageSuite) TestGetSitePage() {
|
|||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
service := createTestBetaService(t, suite.creds)
|
tuples, err := api.FetchPages(ctx, suite.service, suite.siteID)
|
||||||
tuples, err := FetchPages(ctx, service, suite.siteID)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, tuples)
|
require.NotNil(t, tuples)
|
||||||
|
|
||||||
jobs := []string{tuples[0].ID}
|
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.NoError(t, err)
|
||||||
assert.NotEmpty(t, pages)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@ -106,6 +106,15 @@ type Item struct {
|
|||||||
deleted bool
|
deleted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewItem(name string, d io.ReadCloser) *Item {
|
||||||
|
item := &Item{
|
||||||
|
id: name,
|
||||||
|
data: d,
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
func (sd *Item) UUID() string {
|
func (sd *Item) UUID() string {
|
||||||
return sd.id
|
return sd.id
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user