From f3b2e9a632c55804916f69fbcda157143e34b8f2 Mon Sep 17 00:00:00 2001 From: Abin Simon Date: Wed, 1 Feb 2023 03:27:38 +0530 Subject: [PATCH 01/40] Retry handling for delta queries in Exchange (#2328) ## Description Added retry handling for delta queries in OneDrive. Also, bumping time timeout for graph api calls from 90s to 3m as we were seeing client timeouts for graph api calls. ~Haven't added retry for every request in exchange as I'm hoping https://github.com/alcionai/corso/issues/2287 will be a better way to handle this.~ ## Does this PR need a docs update or release note? - [x] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [ ] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * # ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- CHANGELOG.md | 1 + src/internal/connector/exchange/api/api.go | 24 ++ .../connector/exchange/api/contacts.go | 51 +++- src/internal/connector/exchange/api/events.go | 52 +++- src/internal/connector/exchange/api/mail.go | 41 ++- src/internal/connector/graph/errors.go | 24 +- src/internal/connector/graph/errors_test.go | 248 ++++++++++++++++++ src/internal/connector/graph/service.go | 4 +- src/internal/connector/graph/service_test.go | 2 +- src/internal/connector/onedrive/collection.go | 10 +- src/internal/connector/onedrive/item.go | 4 + 11 files changed, 424 insertions(+), 37 deletions(-) create mode 100644 src/internal/connector/graph/errors_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b18a4d04..b96aca9e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Document Corso's fault-tolerance and restartability features +- Add retries on timeouts and status code 500 for Exchange ## [v0.2.0] (alpha) - 2023-1-29 diff --git a/src/internal/connector/exchange/api/api.go b/src/internal/connector/exchange/api/api.go index c4858c5c1..c47cc9b5b 100644 --- a/src/internal/connector/exchange/api/api.go +++ b/src/internal/connector/exchange/api/api.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/alcionai/corso/src/internal/connector/graph" + "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/pkg/account" ) @@ -153,3 +154,26 @@ func HasAttachments(body models.ItemBodyable) bool { return strings.Contains(content, "src=\"cid:") } + +// Run a function with retries +func runWithRetry(run func() error) error { + var err error + + for i := 0; i < numberOfRetries; i++ { + err = run() + if err == nil { + return nil + } + + // only retry on timeouts and 500-internal-errors. + if !(graph.IsErrTimeout(err) || graph.IsInternalServerError(err)) { + break + } + + if i < numberOfRetries { + time.Sleep(time.Duration(3*(i+2)) * time.Second) + } + } + + return support.ConnectorStackErrorTraceWrap(err, "maximum retries or unretryable") +} diff --git a/src/internal/connector/exchange/api/contacts.go b/src/internal/connector/exchange/api/contacts.go index 0db1e964c..33f05d2a3 100644 --- a/src/internal/connector/exchange/api/contacts.go +++ b/src/internal/connector/exchange/api/contacts.go @@ -61,7 +61,16 @@ func (c Contacts) GetItem( ctx context.Context, user, itemID string, ) (serialization.Parsable, *details.ExchangeInfo, error) { - cont, err := c.stable.Client().UsersById(user).ContactsById(itemID).Get(ctx, nil) + var ( + cont models.Contactable + err error + ) + + err = runWithRetry(func() error { + cont, err = c.stable.Client().UsersById(user).ContactsById(itemID).Get(ctx, nil) + return err + }) + if err != nil { return nil, nil, err } @@ -81,7 +90,14 @@ func (c Contacts) GetAllContactFolderNamesForUser( return nil, err } - return c.stable.Client().UsersById(user).ContactFolders().Get(ctx, options) + var resp models.ContactFolderCollectionResponseable + + err = runWithRetry(func() error { + resp, err = c.stable.Client().UsersById(user).ContactFolders().Get(ctx, options) + return err + }) + + return resp, err } func (c Contacts) GetContainerByID( @@ -93,10 +109,14 @@ func (c Contacts) GetContainerByID( return nil, errors.Wrap(err, "options for contact folder") } - return c.stable.Client(). - UsersById(userID). - ContactFoldersById(dirID). - Get(ctx, ofcf) + var resp models.ContactFolderable + + err = runWithRetry(func() error { + resp, err = c.stable.Client().UsersById(userID).ContactFoldersById(dirID).Get(ctx, ofcf) + return err + }) + + return resp, err } // EnumerateContainers iterates through all of the users current @@ -117,6 +137,7 @@ func (c Contacts) EnumerateContainers( var ( errs *multierror.Error + resp models.ContactFolderCollectionResponseable fields = []string{"displayName", "parentFolderId"} ) @@ -131,7 +152,11 @@ func (c Contacts) EnumerateContainers( ChildFolders() for { - resp, err := builder.Get(ctx, ofcf) + err = runWithRetry(func() error { + resp, err = builder.Get(ctx, ofcf) + return err + }) + if err != nil { return errors.Wrap(err, support.ConnectorStackErrorTrace(err)) } @@ -174,7 +199,17 @@ type contactPager struct { } func (p *contactPager) getPage(ctx context.Context) (api.DeltaPageLinker, error) { - return p.builder.Get(ctx, p.options) + var ( + resp api.DeltaPageLinker + err error + ) + + err = runWithRetry(func() error { + resp, err = p.builder.Get(ctx, p.options) + return err + }) + + return resp, err } func (p *contactPager) setNext(nextLink string) { diff --git a/src/internal/connector/exchange/api/events.go b/src/internal/connector/exchange/api/events.go index e643c1f89..63545143d 100644 --- a/src/internal/connector/exchange/api/events.go +++ b/src/internal/connector/exchange/api/events.go @@ -73,7 +73,13 @@ func (c Events) GetContainerByID( return nil, errors.Wrap(err, "options for event calendar") } - cal, err := service.Client().UsersById(userID).CalendarsById(containerID).Get(ctx, ofc) + var cal models.Calendarable + + err = runWithRetry(func() error { + cal, err = service.Client().UsersById(userID).CalendarsById(containerID).Get(ctx, ofc) + return err + }) + if err != nil { return nil, err } @@ -86,7 +92,16 @@ func (c Events) GetItem( ctx context.Context, user, itemID string, ) (serialization.Parsable, *details.ExchangeInfo, error) { - event, err := c.stable.Client().UsersById(user).EventsById(itemID).Get(ctx, nil) + var ( + event models.Eventable + err error + ) + + err = runWithRetry(func() error { + event, err = c.stable.Client().UsersById(user).EventsById(itemID).Get(ctx, nil) + return err + }) + if err != nil { return nil, nil, err } @@ -128,7 +143,14 @@ func (c Client) GetAllCalendarNamesForUser( return nil, err } - return c.stable.Client().UsersById(user).Calendars().Get(ctx, options) + var resp models.CalendarCollectionResponseable + + err = runWithRetry(func() error { + resp, err = c.stable.Client().UsersById(user).Calendars().Get(ctx, options) + return err + }) + + return resp, err } // EnumerateContainers iterates through all of the users current @@ -147,7 +169,10 @@ func (c Events) EnumerateContainers( return err } - var errs *multierror.Error + var ( + resp models.CalendarCollectionResponseable + errs *multierror.Error + ) ofc, err := optionsForCalendars([]string{"name"}) if err != nil { @@ -157,7 +182,13 @@ func (c Events) EnumerateContainers( builder := service.Client().UsersById(userID).Calendars() for { - resp, err := builder.Get(ctx, ofc) + var err error + + err = runWithRetry(func() error { + resp, err = builder.Get(ctx, ofc) + return err + }) + if err != nil { return errors.Wrap(err, support.ConnectorStackErrorTrace(err)) } @@ -205,7 +236,16 @@ type eventPager struct { } func (p *eventPager) getPage(ctx context.Context) (api.DeltaPageLinker, error) { - resp, err := p.builder.Get(ctx, p.options) + var ( + resp api.DeltaPageLinker + err error + ) + + err = runWithRetry(func() error { + resp, err = p.builder.Get(ctx, p.options) + return err + }) + return resp, err } diff --git a/src/internal/connector/exchange/api/mail.go b/src/internal/connector/exchange/api/mail.go index bbac48a66..597676874 100644 --- a/src/internal/connector/exchange/api/mail.go +++ b/src/internal/connector/exchange/api/mail.go @@ -95,7 +95,14 @@ func (c Mail) GetContainerByID( return nil, errors.Wrap(err, "options for mail folder") } - return service.Client().UsersById(userID).MailFoldersById(dirID).Get(ctx, ofmf) + var resp graph.Container + + err = runWithRetry(func() error { + resp, err = service.Client().UsersById(userID).MailFoldersById(dirID).Get(ctx, ofmf) + return err + }) + + return resp, err } // GetItem retrieves a Messageable item. If the item contains an attachment, that @@ -104,7 +111,16 @@ func (c Mail) GetItem( ctx context.Context, user, itemID string, ) (serialization.Parsable, *details.ExchangeInfo, error) { - mail, err := c.stable.Client().UsersById(user).MessagesById(itemID).Get(ctx, nil) + var ( + mail models.Messageable + err error + ) + + err = runWithRetry(func() error { + mail, err = c.stable.Client().UsersById(user).MessagesById(itemID).Get(ctx, nil) + return err + }) + if err != nil { return nil, nil, err } @@ -154,6 +170,7 @@ func (c Mail) EnumerateContainers( } var ( + resp users.ItemMailFoldersDeltaResponseable errs *multierror.Error builder = service.Client(). UsersById(userID). @@ -162,7 +179,13 @@ func (c Mail) EnumerateContainers( ) for { - resp, err := builder.Get(ctx, nil) + var err error + + err = runWithRetry(func() error { + resp, err = builder.Get(ctx, nil) + return err + }) + if err != nil { return errors.Wrap(err, support.ConnectorStackErrorTrace(err)) } @@ -200,7 +223,17 @@ type mailPager struct { } func (p *mailPager) getPage(ctx context.Context) (api.DeltaPageLinker, error) { - return p.builder.Get(ctx, p.options) + var ( + page api.DeltaPageLinker + err error + ) + + err = runWithRetry(func() error { + page, err = p.builder.Get(ctx, p.options) + return err + }) + + return page, err } func (p *mailPager) setNext(nextLink string) { diff --git a/src/internal/connector/graph/errors.go b/src/internal/connector/graph/errors.go index c75e4a6cb..21116057d 100644 --- a/src/internal/connector/graph/errors.go +++ b/src/internal/connector/graph/errors.go @@ -17,6 +17,7 @@ import ( // --------------------------------------------------------------------------- const ( + errCodeActivityLimitReached = "activityLimitReached" errCodeItemNotFound = "ErrorItemNotFound" errCodeEmailFolderNotFound = "ErrorSyncFolderNotFound" errCodeResyncRequired = "ResyncRequired" @@ -31,8 +32,10 @@ var ( // normally the graph client will catch this for us, but in case we // run our own client Do(), we need to translate it to a timeout type // failure locally. - Err429TooManyRequests = errors.New("429 too many requests") - Err503ServiceUnavailable = errors.New("503 Service Unavailable") + Err429TooManyRequests = errors.New("429 too many requests") + Err503ServiceUnavailable = errors.New("503 Service Unavailable") + Err504GatewayTimeout = errors.New("504 Gateway Timeout") + Err500InternalServerError = errors.New("500 Internal Server Error") ) // The folder or item was deleted between the time we identified @@ -113,6 +116,10 @@ func IsErrThrottled(err error) bool { return true } + if hasErrorCode(err, errCodeActivityLimitReached) { + return true + } + e := ErrThrottled{} return errors.As(err, &e) @@ -135,21 +142,18 @@ func IsErrUnauthorized(err error) bool { return errors.As(err, &e) } -type ErrServiceUnavailable struct { +type ErrInternalServerError struct { common.Err } -func IsSericeUnavailable(err error) bool { - if errors.Is(err, Err503ServiceUnavailable) { +func IsInternalServerError(err error) bool { + if errors.Is(err, Err500InternalServerError) { return true } - e := ErrUnauthorized{} - if errors.As(err, &e) { - return true - } + e := ErrInternalServerError{} - return true + return errors.As(err, &e) } // --------------------------------------------------------------------------- diff --git a/src/internal/connector/graph/errors_test.go b/src/internal/connector/graph/errors_test.go new file mode 100644 index 000000000..c7f889b83 --- /dev/null +++ b/src/internal/connector/graph/errors_test.go @@ -0,0 +1,248 @@ +package graph + +import ( + "context" + "testing" + + "github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/internal/common" +) + +type GraphErrorsUnitSuite struct { + suite.Suite +} + +func TestGraphErrorsUnitSuite(t *testing.T) { + suite.Run(t, new(GraphErrorsUnitSuite)) +} + +func odErr(code string) *odataerrors.ODataError { + odErr := &odataerrors.ODataError{} + merr := odataerrors.MainError{} + merr.SetCode(&code) + odErr.SetError(&merr) + + return odErr +} + +func (suite *GraphErrorsUnitSuite) TestIsErrDeletedInFlight() { + table := []struct { + name string + err error + expect assert.BoolAssertionFunc + }{ + { + name: "nil", + err: nil, + expect: assert.False, + }, + { + name: "non-matching", + err: assert.AnError, + expect: assert.False, + }, + { + name: "as", + err: ErrDeletedInFlight{Err: *common.EncapsulateError(assert.AnError)}, + expect: assert.True, + }, + { + name: "non-matching oDataErr", + err: odErr("fnords"), + expect: assert.False, + }, + { + name: "not-found oDataErr", + err: odErr(errCodeItemNotFound), + expect: assert.True, + }, + { + name: "sync-not-found oDataErr", + err: odErr(errCodeSyncFolderNotFound), + expect: assert.True, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + test.expect(t, IsErrDeletedInFlight(test.err)) + }) + } +} + +func (suite *GraphErrorsUnitSuite) TestIsErrInvalidDelta() { + table := []struct { + name string + err error + expect assert.BoolAssertionFunc + }{ + { + name: "nil", + err: nil, + expect: assert.False, + }, + { + name: "non-matching", + err: assert.AnError, + expect: assert.False, + }, + { + name: "as", + err: ErrInvalidDelta{Err: *common.EncapsulateError(assert.AnError)}, + expect: assert.True, + }, + { + name: "non-matching oDataErr", + err: odErr("fnords"), + expect: assert.False, + }, + { + name: "resync-required oDataErr", + err: odErr(errCodeResyncRequired), + expect: assert.True, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + test.expect(t, IsErrInvalidDelta(test.err)) + }) + } +} + +func (suite *GraphErrorsUnitSuite) TestIsErrTimeout() { + table := []struct { + name string + err error + expect assert.BoolAssertionFunc + }{ + { + name: "nil", + err: nil, + expect: assert.False, + }, + { + name: "non-matching", + err: assert.AnError, + expect: assert.False, + }, + { + name: "as", + err: ErrTimeout{Err: *common.EncapsulateError(assert.AnError)}, + expect: assert.True, + }, + { + name: "context deadline", + err: context.DeadlineExceeded, + expect: assert.True, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + test.expect(t, IsErrTimeout(test.err)) + }) + } +} + +func (suite *GraphErrorsUnitSuite) TestIsErrThrottled() { + table := []struct { + name string + err error + expect assert.BoolAssertionFunc + }{ + { + name: "nil", + err: nil, + expect: assert.False, + }, + { + name: "non-matching", + err: assert.AnError, + expect: assert.False, + }, + { + name: "as", + err: ErrThrottled{Err: *common.EncapsulateError(assert.AnError)}, + expect: assert.True, + }, + { + name: "is429", + err: Err429TooManyRequests, + expect: assert.True, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + test.expect(t, IsErrThrottled(test.err)) + }) + } +} + +func (suite *GraphErrorsUnitSuite) TestIsErrUnauthorized() { + table := []struct { + name string + err error + expect assert.BoolAssertionFunc + }{ + { + name: "nil", + err: nil, + expect: assert.False, + }, + { + name: "non-matching", + err: assert.AnError, + expect: assert.False, + }, + { + name: "as", + err: ErrUnauthorized{Err: *common.EncapsulateError(assert.AnError)}, + expect: assert.True, + }, + { + name: "is429", + err: Err401Unauthorized, + expect: assert.True, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + test.expect(t, IsErrUnauthorized(test.err)) + }) + } +} + +func (suite *GraphErrorsUnitSuite) TestIsInternalServerError() { + table := []struct { + name string + err error + expect assert.BoolAssertionFunc + }{ + { + name: "nil", + err: nil, + expect: assert.False, + }, + { + name: "non-matching", + err: assert.AnError, + expect: assert.False, + }, + { + name: "as", + err: ErrInternalServerError{Err: *common.EncapsulateError(assert.AnError)}, + expect: assert.True, + }, + { + name: "is429", + err: Err500InternalServerError, + expect: assert.True, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + test.expect(t, IsInternalServerError(test.err)) + }) + } +} diff --git a/src/internal/connector/graph/service.go b/src/internal/connector/graph/service.go index 6c0e6dbc1..093995a42 100644 --- a/src/internal/connector/graph/service.go +++ b/src/internal/connector/graph/service.go @@ -149,7 +149,7 @@ func HTTPClient(opts ...option) *http.Client { middlewares := msgraphgocore.GetDefaultMiddlewaresWithOptions(&clientOptions) middlewares = append(middlewares, &LoggingMiddleware{}) httpClient := msgraphgocore.GetDefaultClient(&clientOptions, middlewares...) - httpClient.Timeout = time.Second * 90 + httpClient.Timeout = time.Minute * 3 (&clientConfig{}). populate(opts...). @@ -250,7 +250,6 @@ func (handler *LoggingMiddleware) Intercept( respDump, _ := httputil.DumpResponse(resp, false) metadata := []any{ - "idx", middlewareIndex, "method", req.Method, "status", resp.Status, "statusCode", resp.StatusCode, @@ -273,7 +272,6 @@ func (handler *LoggingMiddleware) Intercept( respDump, _ := httputil.DumpResponse(resp, true) metadata := []any{ - "idx", middlewareIndex, "method", req.Method, "status", resp.Status, "statusCode", resp.StatusCode, diff --git a/src/internal/connector/graph/service_test.go b/src/internal/connector/graph/service_test.go index 14bdc9c36..c2ef2d699 100644 --- a/src/internal/connector/graph/service_test.go +++ b/src/internal/connector/graph/service_test.go @@ -53,7 +53,7 @@ func (suite *GraphUnitSuite) TestHTTPClient() { name: "no options", opts: []option{}, check: func(t *testing.T, c *http.Client) { - assert.Equal(t, 90*time.Second, c.Timeout, "default timeout") + assert.Equal(t, 3*time.Minute, c.Timeout, "default timeout") }, }, { diff --git a/src/internal/connector/onedrive/collection.go b/src/internal/connector/onedrive/collection.go index a786de0ab..c4e1825bd 100644 --- a/src/internal/connector/onedrive/collection.go +++ b/src/internal/connector/onedrive/collection.go @@ -271,11 +271,11 @@ func (oc *Collection) populateItems(ctx context.Context) { continue - } else if !graph.IsErrTimeout(err) && !graph.IsErrThrottled(err) && !graph.IsSericeUnavailable(err) { - // TODO: graphAPI will provides headers that state the duration to wait - // in order to succeed again. The one second sleep won't cut it here. - // - // for all non-timeout, non-unauth, non-throttling errors, do not retry + } else if !graph.IsErrTimeout(err) && + !graph.IsInternalServerError(err) { + // Don't retry for non-timeout, on-unauth, as + // we are already retrying it in the default + // retry middleware break } diff --git a/src/internal/connector/onedrive/item.go b/src/internal/connector/onedrive/item.go index c4fd1b380..b1027de9d 100644 --- a/src/internal/connector/onedrive/item.go +++ b/src/internal/connector/onedrive/item.go @@ -105,6 +105,10 @@ func downloadItem(hc *http.Client, item models.DriveItemable) (*http.Response, e return resp, graph.Err401Unauthorized } + if resp.StatusCode == http.StatusInternalServerError { + return resp, graph.Err500InternalServerError + } + if resp.StatusCode == http.StatusServiceUnavailable { return resp, graph.Err503ServiceUnavailable } From 070b8fddeebac78f74f77c30917d10d6074910f4 Mon Sep 17 00:00:00 2001 From: Keepers Date: Tue, 31 Jan 2023 15:25:02 -0700 Subject: [PATCH 02/40] update operation/backup errs (#2239) ## Description Begins updating operations/backup with the new error handling procedures. For backwards compatibility, errors are currently duplicated in the old stats.Errs and the new Errors struct. ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :broom: Tech Debt/Cleanup ## Issue(s) * #1970 ## Test Plan - [x] :zap: Unit test - [x] :green_heart: E2E --- src/internal/operations/backup.go | 84 ++++--- src/internal/operations/backup_test.go | 252 -------------------- src/internal/operations/manifests.go | 58 +++-- src/internal/operations/manifests_test.go | 277 +++++++++++++++++++++- src/pkg/fault/fault.go | 4 +- src/pkg/fault/mock/mock.go | 17 ++ 6 files changed, 374 insertions(+), 318 deletions(-) create mode 100644 src/pkg/fault/mock/mock.go diff --git a/src/internal/operations/backup.go b/src/internal/operations/backup.go index b2a0c552c..51e92181c 100644 --- a/src/internal/operations/backup.go +++ b/src/internal/operations/backup.go @@ -144,6 +144,7 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) { err = op.persistResults(startTime, &opStats) if err != nil { + op.Errors.Fail(errors.Wrap(err, "persisting backup results")) return } @@ -153,7 +154,8 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) { opStats.k.SnapshotID, backupDetails.Details()) if err != nil { - opStats.writeErr = err + op.Errors.Fail(errors.Wrap(err, "persisting backup")) + opStats.writeErr = op.Errors.Err() } }() @@ -164,21 +166,27 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) { reasons, tenantID, uib, - ) + op.Errors) if err != nil { - opStats.readErr = errors.Wrap(err, "connecting to M365") + op.Errors.Fail(errors.Wrap(err, "collecting manifest heuristics")) + opStats.readErr = op.Errors.Err() + return opStats.readErr } gc, err := connectToM365(ctx, op.Selectors, op.account) if err != nil { - opStats.readErr = errors.Wrap(err, "connecting to M365") + op.Errors.Fail(errors.Wrap(err, "connecting to m365")) + opStats.readErr = op.Errors.Err() + return opStats.readErr } cs, err := produceBackupDataCollections(ctx, gc, op.Selectors, mdColls, op.Options) if err != nil { - opStats.readErr = errors.Wrap(err, "retrieving data to backup") + op.Errors.Fail(errors.Wrap(err, "retrieving data to backup")) + opStats.readErr = op.Errors.Err() + return opStats.readErr } @@ -194,7 +202,9 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) { op.Results.BackupID, uib && canUseMetaData) if err != nil { - opStats.writeErr = errors.Wrap(err, "backing up service data") + op.Errors.Fail(errors.Wrap(err, "backing up service data")) + opStats.writeErr = op.Errors.Err() + return opStats.writeErr } @@ -211,12 +221,15 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) { toMerge, backupDetails, ); err != nil { - opStats.writeErr = errors.Wrap(err, "merging backup details") + op.Errors.Fail(errors.Wrap(err, "merging backup details")) + opStats.writeErr = op.Errors.Err() + return opStats.writeErr } opStats.gc = gc.AwaitStatus() + // TODO(keepers): remove when fault.Errors handles all iterable error aggregation. if opStats.gc.ErrorCount > 0 { merr := multierror.Append(opStats.readErr, errors.Wrap(opStats.gc.Err, "retrieving data")) opStats.readErr = merr.ErrorOrNil() @@ -307,7 +320,9 @@ func selectorToReasons(sel selectors.Selector) []kopia.Reason { return reasons } -func builderFromReason(tenant string, r kopia.Reason) (*path.Builder, error) { +func builderFromReason(ctx context.Context, tenant string, r kopia.Reason) (*path.Builder, error) { + ctx = clues.Add(ctx, "category", r.Category.String()) + // This is hacky, but we want the path package to format the path the right // way (e.x. proper order for service, category, etc), but we don't care about // the folders after the prefix. @@ -319,12 +334,7 @@ func builderFromReason(tenant string, r kopia.Reason) (*path.Builder, error) { false, ) if err != nil { - return nil, errors.Wrapf( - err, - "building path for service %s category %s", - r.Service.String(), - r.Category.String(), - ) + return nil, clues.Wrap(err, "building path").WithMap(clues.Values(ctx)) } return p.ToBuilder().Dir(), nil @@ -367,7 +377,7 @@ func consumeBackupDataCollections( categories := map[string]struct{}{} for _, reason := range m.Reasons { - pb, err := builderFromReason(tenantID, reason) + pb, err := builderFromReason(ctx, tenantID, reason) if err != nil { return nil, nil, nil, errors.Wrap(err, "getting subtree paths for bases") } @@ -461,6 +471,8 @@ func mergeDetails( var addedEntries int for _, man := range mans { + mctx := clues.Add(ctx, "manifest_id", man.ID) + // For now skip snapshots that aren't complete. We will need to revisit this // when we tackle restartability. if len(man.IncompleteReason) > 0 { @@ -469,9 +481,11 @@ func mergeDetails( bID, ok := man.GetTag(kopia.TagBackupID) if !ok { - return errors.Errorf("no backup ID in snapshot manifest with ID %s", man.ID) + return clues.New("no backup ID in snapshot manifest").WithMap(clues.Values(mctx)) } + mctx = clues.Add(mctx, "manifest_backup_id", bID) + _, baseDeets, err := getBackupAndDetailsFromID( ctx, model.StableID(bID), @@ -479,18 +493,15 @@ func mergeDetails( detailsStore, ) if err != nil { - return errors.Wrapf(err, "backup fetching base details for backup %s", bID) + return clues.New("fetching base details for backup").WithMap(clues.Values(mctx)) } for _, entry := range baseDeets.Items() { rr, err := path.FromDataLayerPath(entry.RepoRef, true) if err != nil { - return errors.Wrapf( - err, - "parsing base item info path %s in backup %s", - entry.RepoRef, - bID, - ) + return clues.New("parsing base item info path"). + WithMap(clues.Values(mctx)). + With("repo_ref", entry.RepoRef) // todo: pii } // Although this base has an entry it may not be the most recent. Check @@ -513,11 +524,7 @@ func mergeDetails( // Fixup paths in the item. item := entry.ItemInfo if err := details.UpdateItem(&item, newPath); err != nil { - return errors.Wrapf( - err, - "updating item info for entry from backup %s", - bID, - ) + return clues.New("updating item details").WithMap(clues.Values(mctx)) } // TODO(ashmrtn): This may need updated if we start using this merge @@ -542,11 +549,9 @@ func mergeDetails( } if addedEntries != len(shortRefsFromPrevBackup) { - return errors.Errorf( - "incomplete migration of backup details: found %v of %v expected items", - addedEntries, - len(shortRefsFromPrevBackup), - ) + return clues.New("incomplete migration of backup details"). + WithMap(clues.Values(ctx)). + WithAll("item_count", addedEntries, "expected_item_count", len(shortRefsFromPrevBackup)) } return nil @@ -568,6 +573,7 @@ func (op *BackupOperation) persistResults( if opStats.readErr != nil || opStats.writeErr != nil { op.Status = Failed + // TODO(keepers): replace with fault.Errors handling. return multierror.Append( errors.New("errors prevented the operation from processing"), opStats.readErr, @@ -594,15 +600,18 @@ func (op *BackupOperation) createBackupModels( snapID string, backupDetails *details.Details, ) error { + ctx = clues.Add(ctx, "snapshot_id", snapID) + if backupDetails == nil { - return errors.New("no backup details to record") + return clues.New("no backup details to record").WithMap(clues.Values(ctx)) } detailsID, err := detailsStore.WriteBackupDetails(ctx, backupDetails) if err != nil { - return errors.Wrap(err, "creating backupdetails model") + return clues.Wrap(err, "creating backupDetails model").WithMap(clues.Values(ctx)) } + ctx = clues.Add(ctx, "details_id", detailsID) b := backup.New( snapID, detailsID, op.Status.String(), op.Results.BackupID, @@ -612,9 +621,8 @@ func (op *BackupOperation) createBackupModels( op.Errors, ) - err = op.store.Put(ctx, model.BackupSchema, b) - if err != nil { - return errors.Wrap(err, "creating backup model") + if err = op.store.Put(ctx, model.BackupSchema, b); err != nil { + return clues.Wrap(err, "creating backup model").WithMap(clues.Values(ctx)) } dur := op.Results.CompletedAt.Sub(op.Results.StartedAt) diff --git a/src/internal/operations/backup_test.go b/src/internal/operations/backup_test.go index f867e2a11..149448e66 100644 --- a/src/internal/operations/backup_test.go +++ b/src/internal/operations/backup_test.go @@ -432,258 +432,6 @@ func (suite *BackupOpSuite) TestBackupOperation_PersistResults() { } } -func (suite *BackupOpSuite) TestBackupOperation_VerifyDistinctBases() { - const user = "a-user" - - table := []struct { - name string - input []*kopia.ManifestEntry - errCheck assert.ErrorAssertionFunc - }{ - { - name: "SingleManifestMultipleReasons", - input: []*kopia.ManifestEntry{ - { - Manifest: &snapshot.Manifest{ - ID: "id1", - }, - Reasons: []kopia.Reason{ - { - ResourceOwner: user, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - { - ResourceOwner: user, - Service: path.ExchangeService, - Category: path.EventsCategory, - }, - }, - }, - }, - errCheck: assert.NoError, - }, - { - name: "MultipleManifestsDistinctReason", - input: []*kopia.ManifestEntry{ - { - Manifest: &snapshot.Manifest{ - ID: "id1", - }, - Reasons: []kopia.Reason{ - { - ResourceOwner: user, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - { - Manifest: &snapshot.Manifest{ - ID: "id2", - }, - Reasons: []kopia.Reason{ - { - ResourceOwner: user, - Service: path.ExchangeService, - Category: path.EventsCategory, - }, - }, - }, - }, - errCheck: assert.NoError, - }, - { - name: "MultipleManifestsSameReason", - input: []*kopia.ManifestEntry{ - { - Manifest: &snapshot.Manifest{ - ID: "id1", - }, - Reasons: []kopia.Reason{ - { - ResourceOwner: user, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - { - Manifest: &snapshot.Manifest{ - ID: "id2", - }, - Reasons: []kopia.Reason{ - { - ResourceOwner: user, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - }, - errCheck: assert.Error, - }, - { - name: "MultipleManifestsSameReasonOneIncomplete", - input: []*kopia.ManifestEntry{ - { - Manifest: &snapshot.Manifest{ - ID: "id1", - }, - Reasons: []kopia.Reason{ - { - ResourceOwner: user, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - { - Manifest: &snapshot.Manifest{ - ID: "id2", - IncompleteReason: "checkpoint", - }, - Reasons: []kopia.Reason{ - { - ResourceOwner: user, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - }, - errCheck: assert.NoError, - }, - } - - for _, test := range table { - suite.T().Run(test.name, func(t *testing.T) { - test.errCheck(t, verifyDistinctBases(test.input)) - }) - } -} - -func (suite *BackupOpSuite) TestBackupOperation_CollectMetadata() { - var ( - tenant = "a-tenant" - resourceOwner = "a-user" - fileNames = []string{ - "delta", - "paths", - } - - emailDeltaPath = makeMetadataPath( - suite.T(), - tenant, - path.ExchangeService, - resourceOwner, - path.EmailCategory, - fileNames[0], - ) - emailPathsPath = makeMetadataPath( - suite.T(), - tenant, - path.ExchangeService, - resourceOwner, - path.EmailCategory, - fileNames[1], - ) - contactsDeltaPath = makeMetadataPath( - suite.T(), - tenant, - path.ExchangeService, - resourceOwner, - path.ContactsCategory, - fileNames[0], - ) - contactsPathsPath = makeMetadataPath( - suite.T(), - tenant, - path.ExchangeService, - resourceOwner, - path.ContactsCategory, - fileNames[1], - ) - ) - - table := []struct { - name string - inputMan *kopia.ManifestEntry - inputFiles []string - expected []path.Path - }{ - { - name: "SingleReasonSingleFile", - inputMan: &kopia.ManifestEntry{ - Manifest: &snapshot.Manifest{}, - Reasons: []kopia.Reason{ - { - ResourceOwner: resourceOwner, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - inputFiles: []string{fileNames[0]}, - expected: []path.Path{emailDeltaPath}, - }, - { - name: "SingleReasonMultipleFiles", - inputMan: &kopia.ManifestEntry{ - Manifest: &snapshot.Manifest{}, - Reasons: []kopia.Reason{ - { - ResourceOwner: resourceOwner, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - inputFiles: fileNames, - expected: []path.Path{emailDeltaPath, emailPathsPath}, - }, - { - name: "MultipleReasonsMultipleFiles", - inputMan: &kopia.ManifestEntry{ - Manifest: &snapshot.Manifest{}, - Reasons: []kopia.Reason{ - { - ResourceOwner: resourceOwner, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - { - ResourceOwner: resourceOwner, - Service: path.ExchangeService, - Category: path.ContactsCategory, - }, - }, - }, - inputFiles: fileNames, - expected: []path.Path{ - emailDeltaPath, - emailPathsPath, - contactsDeltaPath, - contactsPathsPath, - }, - }, - } - - for _, test := range table { - suite.T().Run(test.name, func(t *testing.T) { - ctx, flush := tester.NewContext() - defer flush() - - mr := &mockRestorer{} - - _, err := collectMetadata(ctx, mr, test.inputMan, test.inputFiles, tenant) - assert.NoError(t, err) - - checkPaths(t, test.expected, mr.gotPaths) - }) - } -} - func (suite *BackupOpSuite) TestBackupOperation_ConsumeBackupDataCollections_Paths() { var ( tenant = "a-tenant" diff --git a/src/internal/operations/manifests.go b/src/internal/operations/manifests.go index fe0e4d09d..9c765fe70 100644 --- a/src/internal/operations/manifests.go +++ b/src/internal/operations/manifests.go @@ -3,7 +3,7 @@ package operations import ( "context" - multierror "github.com/hashicorp/go-multierror" + "github.com/alcionai/clues" "github.com/kopia/kopia/repo/manifest" "github.com/pkg/errors" @@ -12,6 +12,7 @@ import ( "github.com/alcionai/corso/src/internal/kopia" "github.com/alcionai/corso/src/internal/model" "github.com/alcionai/corso/src/pkg/backup" + "github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/path" ) @@ -44,6 +45,7 @@ func produceManifestsAndMetadata( reasons []kopia.Reason, tenantID string, getMetadata bool, + errs fault.Adder, ) ([]*kopia.ManifestEntry, []data.Collection, bool, error) { var ( metadataFiles = graph.AllMetadataFileNames() @@ -68,12 +70,10 @@ func produceManifestsAndMetadata( // // TODO(ashmrtn): This may need updating if we start sourcing item backup // details from previous snapshots when using kopia-assisted incrementals. - if err := verifyDistinctBases(ms); err != nil { - logger.Ctx(ctx).Warnw( + if err := verifyDistinctBases(ctx, ms, errs); err != nil { + logger.Ctx(ctx).With("error", err).Infow( "base snapshot collision, falling back to full backup", - "error", - err, - ) + clues.Slice(ctx)...) return ms, nil, false, nil } @@ -83,40 +83,41 @@ func produceManifestsAndMetadata( continue } + mctx := clues.Add(ctx, "manifest_id", man.ID) + bID, ok := man.GetTag(kopia.TagBackupID) if !ok { - return nil, nil, false, errors.New("snapshot manifest missing backup ID") + err = clues.New("snapshot manifest missing backup ID").WithMap(clues.Values(mctx)) + return nil, nil, false, err } - dID, _, err := gdi.GetDetailsIDFromBackupID(ctx, model.StableID(bID)) + mctx = clues.Add(mctx, "manifest_backup_id", man.ID) + + dID, _, err := gdi.GetDetailsIDFromBackupID(mctx, model.StableID(bID)) if err != nil { // if no backup exists for any of the complete manifests, we want // to fall back to a complete backup. if errors.Is(err, kopia.ErrNotFound) { - logger.Ctx(ctx).Infow( - "backup missing, falling back to full backup", - "backup_id", bID) - + logger.Ctx(ctx).Infow("backup missing, falling back to full backup", clues.Slice(mctx)...) return ms, nil, false, nil } return nil, nil, false, errors.Wrap(err, "retrieving prior backup data") } + mctx = clues.Add(mctx, "manifest_details_id", dID) + // if no detailsID exists for any of the complete manifests, we want // to fall back to a complete backup. This is a temporary prevention // mechanism to keep backups from falling into a perpetually bad state. // This makes an assumption that the ID points to a populated set of // details; we aren't doing the work to look them up. if len(dID) == 0 { - logger.Ctx(ctx).Infow( - "backup missing details ID, falling back to full backup", - "backup_id", bID) - + logger.Ctx(ctx).Infow("backup missing details ID, falling back to full backup", clues.Slice(mctx)...) return ms, nil, false, nil } - colls, err := collectMetadata(ctx, mr, man, metadataFiles, tenantID) + colls, err := collectMetadata(mctx, mr, man, metadataFiles, tenantID) if err != nil && !errors.Is(err, kopia.ErrNotFound) { // prior metadata isn't guaranteed to exist. // if it doesn't, we'll just have to do a @@ -134,9 +135,9 @@ func produceManifestsAndMetadata( // of manifests, that each manifest's Reason (owner, service, category) is only // included once. If a reason is duplicated by any two manifests, an error is // returned. -func verifyDistinctBases(mans []*kopia.ManifestEntry) error { +func verifyDistinctBases(ctx context.Context, mans []*kopia.ManifestEntry, errs fault.Adder) error { var ( - errs *multierror.Error + failed bool reasons = map[string]manifest.ID{} ) @@ -155,10 +156,11 @@ func verifyDistinctBases(mans []*kopia.ManifestEntry) error { reasonKey := reason.ResourceOwner + reason.Service.String() + reason.Category.String() if b, ok := reasons[reasonKey]; ok { - errs = multierror.Append(errs, errors.Errorf( - "multiple base snapshots source data for %s %s. IDs: %s, %s", - reason.Service, reason.Category, b, man.ID, - )) + failed = true + + errs.Add(clues.New("manifests have overlapping reasons"). + WithMap(clues.Values(ctx)). + With("other_manifest_id", b)) continue } @@ -167,7 +169,11 @@ func verifyDistinctBases(mans []*kopia.ManifestEntry) error { } } - return errs.ErrorOrNil() + if failed { + return clues.New("multiple base snapshots qualify").WithMap(clues.Values(ctx)) + } + + return nil } // collectMetadata retrieves all metadata files associated with the manifest. @@ -191,7 +197,9 @@ func collectMetadata( reason.Category, true) if err != nil { - return nil, errors.Wrapf(err, "building metadata path") + return nil, clues. + Wrap(err, "building metadata path"). + WithAll("metadata_file", fn, "category", reason.Category) } paths = append(paths, p) diff --git a/src/internal/operations/manifests_test.go b/src/internal/operations/manifests_test.go index 7cfc9ac9a..93cdb982f 100644 --- a/src/internal/operations/manifests_test.go +++ b/src/internal/operations/manifests_test.go @@ -14,6 +14,7 @@ import ( "github.com/alcionai/corso/src/internal/model" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/backup" + "github.com/alcionai/corso/src/pkg/fault/mock" "github.com/alcionai/corso/src/pkg/path" ) @@ -400,7 +401,10 @@ func (suite *OperationsManifestsUnitSuite) TestVerifyDistinctBases() { } for _, test := range table { suite.T().Run(test.name, func(t *testing.T) { - err := verifyDistinctBases(test.mans) + ctx, flush := tester.NewContext() + defer flush() + + err := verifyDistinctBases(ctx, test.mans, mock.NewAdder()) test.expect(t, err) }) } @@ -646,6 +650,8 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() { ctx, flush := tester.NewContext() defer flush() + ma := mock.NewAdder() + mans, dcs, b, err := produceManifestsAndMetadata( ctx, &test.mr, @@ -653,7 +659,7 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() { test.reasons, tid, test.getMeta, - ) + ma) test.assertErr(t, err) test.assertB(t, b) @@ -683,3 +689,270 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() { }) } } + +// --------------------------------------------------------------------------- +// older tests +// --------------------------------------------------------------------------- + +type BackupManifestSuite struct { + suite.Suite +} + +func TestBackupManifestSuite(t *testing.T) { + suite.Run(t, new(BackupOpSuite)) +} + +func (suite *BackupManifestSuite) TestBackupOperation_VerifyDistinctBases() { + const user = "a-user" + + table := []struct { + name string + input []*kopia.ManifestEntry + errCheck assert.ErrorAssertionFunc + }{ + { + name: "SingleManifestMultipleReasons", + input: []*kopia.ManifestEntry{ + { + Manifest: &snapshot.Manifest{ + ID: "id1", + }, + Reasons: []kopia.Reason{ + { + ResourceOwner: user, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + { + ResourceOwner: user, + Service: path.ExchangeService, + Category: path.EventsCategory, + }, + }, + }, + }, + errCheck: assert.NoError, + }, + { + name: "MultipleManifestsDistinctReason", + input: []*kopia.ManifestEntry{ + { + Manifest: &snapshot.Manifest{ + ID: "id1", + }, + Reasons: []kopia.Reason{ + { + ResourceOwner: user, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + }, + }, + { + Manifest: &snapshot.Manifest{ + ID: "id2", + }, + Reasons: []kopia.Reason{ + { + ResourceOwner: user, + Service: path.ExchangeService, + Category: path.EventsCategory, + }, + }, + }, + }, + errCheck: assert.NoError, + }, + { + name: "MultipleManifestsSameReason", + input: []*kopia.ManifestEntry{ + { + Manifest: &snapshot.Manifest{ + ID: "id1", + }, + Reasons: []kopia.Reason{ + { + ResourceOwner: user, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + }, + }, + { + Manifest: &snapshot.Manifest{ + ID: "id2", + }, + Reasons: []kopia.Reason{ + { + ResourceOwner: user, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + }, + }, + }, + errCheck: assert.Error, + }, + { + name: "MultipleManifestsSameReasonOneIncomplete", + input: []*kopia.ManifestEntry{ + { + Manifest: &snapshot.Manifest{ + ID: "id1", + }, + Reasons: []kopia.Reason{ + { + ResourceOwner: user, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + }, + }, + { + Manifest: &snapshot.Manifest{ + ID: "id2", + IncompleteReason: "checkpoint", + }, + Reasons: []kopia.Reason{ + { + ResourceOwner: user, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + }, + }, + }, + errCheck: assert.NoError, + }, + } + + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + ctx, flush := tester.NewContext() + defer flush() + + test.errCheck(t, verifyDistinctBases(ctx, test.input, mock.NewAdder())) + }) + } +} + +func (suite *BackupManifestSuite) TestBackupOperation_CollectMetadata() { + var ( + tenant = "a-tenant" + resourceOwner = "a-user" + fileNames = []string{ + "delta", + "paths", + } + + emailDeltaPath = makeMetadataPath( + suite.T(), + tenant, + path.ExchangeService, + resourceOwner, + path.EmailCategory, + fileNames[0], + ) + emailPathsPath = makeMetadataPath( + suite.T(), + tenant, + path.ExchangeService, + resourceOwner, + path.EmailCategory, + fileNames[1], + ) + contactsDeltaPath = makeMetadataPath( + suite.T(), + tenant, + path.ExchangeService, + resourceOwner, + path.ContactsCategory, + fileNames[0], + ) + contactsPathsPath = makeMetadataPath( + suite.T(), + tenant, + path.ExchangeService, + resourceOwner, + path.ContactsCategory, + fileNames[1], + ) + ) + + table := []struct { + name string + inputMan *kopia.ManifestEntry + inputFiles []string + expected []path.Path + }{ + { + name: "SingleReasonSingleFile", + inputMan: &kopia.ManifestEntry{ + Manifest: &snapshot.Manifest{}, + Reasons: []kopia.Reason{ + { + ResourceOwner: resourceOwner, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + }, + }, + inputFiles: []string{fileNames[0]}, + expected: []path.Path{emailDeltaPath}, + }, + { + name: "SingleReasonMultipleFiles", + inputMan: &kopia.ManifestEntry{ + Manifest: &snapshot.Manifest{}, + Reasons: []kopia.Reason{ + { + ResourceOwner: resourceOwner, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + }, + }, + inputFiles: fileNames, + expected: []path.Path{emailDeltaPath, emailPathsPath}, + }, + { + name: "MultipleReasonsMultipleFiles", + inputMan: &kopia.ManifestEntry{ + Manifest: &snapshot.Manifest{}, + Reasons: []kopia.Reason{ + { + ResourceOwner: resourceOwner, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + { + ResourceOwner: resourceOwner, + Service: path.ExchangeService, + Category: path.ContactsCategory, + }, + }, + }, + inputFiles: fileNames, + expected: []path.Path{ + emailDeltaPath, + emailPathsPath, + contactsDeltaPath, + contactsPathsPath, + }, + }, + } + + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + ctx, flush := tester.NewContext() + defer flush() + + mr := &mockRestorer{} + + _, err := collectMetadata(ctx, mr, test.inputMan, test.inputFiles, tenant) + assert.NoError(t, err) + + checkPaths(t, test.expected, mr.gotPaths) + }) + } +} diff --git a/src/pkg/fault/fault.go b/src/pkg/fault/fault.go index f4171ce77..69017029c 100644 --- a/src/pkg/fault/fault.go +++ b/src/pkg/fault/fault.go @@ -96,7 +96,9 @@ func (e *Errors) setErr(err error) *Errors { return e } -// TODO: introduce Adder interface +type Adder interface { + Add(err error) *Errors +} // Add appends the error to the slice of recoverable and // iterated errors (ie: errors.errs). If failFast is true, diff --git a/src/pkg/fault/mock/mock.go b/src/pkg/fault/mock/mock.go new file mode 100644 index 000000000..ba560996d --- /dev/null +++ b/src/pkg/fault/mock/mock.go @@ -0,0 +1,17 @@ +package mock + +import "github.com/alcionai/corso/src/pkg/fault" + +// Adder mocks an adder interface for testing. +type Adder struct { + Errs []error +} + +func NewAdder() *Adder { + return &Adder{Errs: []error{}} +} + +func (ma *Adder) Add(err error) *fault.Errors { + ma.Errs = append(ma.Errs, err) + return fault.New(false) +} From 387f8e8cd7eb2e1f4565d241ebe9472ad1dd0207 Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Tue, 31 Jan 2023 14:48:30 -0800 Subject: [PATCH 03/40] Deserialize OneDrive metadata during backup (#2263) ## Description Create helper functions to deserialize OneDrive metadata during subsequent backups. Currently deserialized data is not passed to the function that generates Collections nor is metadata passed in even though it's wired through GraphConnector Additional changes to BackupOp and operations/manifests.go are required to begin passing in metadata ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [x] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * closes #2122 ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- src/internal/connector/data_collections.go | 5 +- .../connector/onedrive/collections.go | 140 +++++++- .../connector/onedrive/collections_test.go | 302 +++++++++++++++++- src/internal/connector/onedrive/drive_test.go | 2 +- .../connector/sharepoint/data_collections.go | 4 +- 5 files changed, 444 insertions(+), 9 deletions(-) diff --git a/src/internal/connector/data_collections.go b/src/internal/connector/data_collections.go index 7d187a854..410a05462 100644 --- a/src/internal/connector/data_collections.go +++ b/src/internal/connector/data_collections.go @@ -83,7 +83,7 @@ func (gc *GraphConnector) DataCollections( return colls, excludes, nil case selectors.ServiceOneDrive: - return gc.OneDriveDataCollections(ctx, sels, ctrlOpts) + return gc.OneDriveDataCollections(ctx, sels, metadata, ctrlOpts) case selectors.ServiceSharePoint: colls, excludes, err := sharepoint.DataCollections( @@ -182,6 +182,7 @@ func (fm odFolderMatcher) Matches(dir string) bool { func (gc *GraphConnector) OneDriveDataCollections( ctx context.Context, selector selectors.Selector, + metadata []data.Collection, ctrlOpts control.Options, ) ([]data.Collection, map[string]struct{}, error) { odb, err := selector.ToOneDriveBackup() @@ -209,7 +210,7 @@ func (gc *GraphConnector) OneDriveDataCollections( gc.Service, gc.UpdateStatus, ctrlOpts, - ).Get(ctx) + ).Get(ctx, metadata) if err != nil { return nil, nil, support.WrapAndAppend(user, err, errs) } diff --git a/src/internal/connector/onedrive/collections.go b/src/internal/connector/onedrive/collections.go index f83ce342a..35b81f7f2 100644 --- a/src/internal/connector/onedrive/collections.go +++ b/src/internal/connector/onedrive/collections.go @@ -2,7 +2,9 @@ package onedrive import ( "context" + "encoding/json" "fmt" + "io" "net/http" "strings" @@ -92,9 +94,145 @@ func NewCollections( } } +func deserializeMetadata( + ctx context.Context, + cols []data.Collection, +) (map[string]string, map[string]map[string]string, error) { + logger.Ctx(ctx).Infow( + "deserialzing previous backup metadata", + "num_collections", + len(cols), + ) + + prevDeltas := map[string]string{} + prevFolders := map[string]map[string]string{} + + for _, col := range cols { + items := col.Items() + + for breakLoop := false; !breakLoop; { + select { + case <-ctx.Done(): + return nil, nil, errors.Wrap(ctx.Err(), "deserialzing previous backup metadata") + + case item, ok := <-items: + if !ok { + // End of collection items. + breakLoop = true + break + } + + var err error + + switch item.UUID() { + case graph.PreviousPathFileName: + err = deserializeMap(item.ToReader(), prevFolders) + + case graph.DeltaURLsFileName: + err = deserializeMap(item.ToReader(), prevDeltas) + + default: + logger.Ctx(ctx).Infow( + "skipping unknown metadata file", + "file_name", + item.UUID(), + ) + + continue + } + + if err == nil { + // Successful decode. + continue + } + + // This is conservative, but report an error if any of the items for + // any of the deserialized maps have duplicate drive IDs. This will + // cause the entire backup to fail, but it's not clear if higher + // layers would have caught this. Worst case if we don't handle this + // we end up in a situation where we're sourcing items from the wrong + // base in kopia wrapper. + if errors.Is(err, errExistingMapping) { + return nil, nil, errors.Wrapf( + err, + "deserializing metadata file %s", + item.UUID(), + ) + } + + logger.Ctx(ctx).Errorw( + "deserializing base backup metadata. Falling back to full backup for selected drives", + "error", + err, + "file_name", + item.UUID(), + ) + } + } + + // Go through and remove partial results (i.e. path mapping but no delta URL + // or vice-versa). + for k := range prevDeltas { + if _, ok := prevFolders[k]; !ok { + delete(prevDeltas, k) + } + } + + for k := range prevFolders { + if _, ok := prevDeltas[k]; !ok { + delete(prevFolders, k) + } + } + } + + return prevDeltas, prevFolders, nil +} + +var errExistingMapping = errors.New("mapping already exists for same drive ID") + +// deserializeMap takes an reader and a map of already deserialized items and +// adds the newly deserialized items to alreadyFound. Items are only added to +// alreadyFound if none of the keys in the freshly deserialized map already +// exist in alreadyFound. reader is closed at the end of this function. +func deserializeMap[T any](reader io.ReadCloser, alreadyFound map[string]T) error { + defer reader.Close() + + tmp := map[string]T{} + + err := json.NewDecoder(reader).Decode(&tmp) + if err != nil { + return errors.Wrap(err, "deserializing file contents") + } + + var duplicate bool + + for k := range tmp { + if _, ok := alreadyFound[k]; ok { + duplicate = true + break + } + } + + if duplicate { + return errors.WithStack(errExistingMapping) + } + + maps.Copy(alreadyFound, tmp) + + return nil +} + // Retrieves drive data as set of `data.Collections` and a set of item names to // be excluded from the upcoming backup. -func (c *Collections) Get(ctx context.Context) ([]data.Collection, map[string]struct{}, error) { +func (c *Collections) Get( + ctx context.Context, + prevMetadata []data.Collection, +) ([]data.Collection, map[string]struct{}, error) { + _, _, err := deserializeMetadata(ctx, prevMetadata) + if err != nil { + return nil, nil, err + } + // Enumerate drives for the specified resourceOwner pager, err := PagerForSource(c.source, c.service, c.resourceOwner, nil) if err != nil { diff --git a/src/internal/connector/onedrive/collections_test.go b/src/internal/connector/onedrive/collections_test.go index b69253918..c250afe2a 100644 --- a/src/internal/connector/onedrive/collections_test.go +++ b/src/internal/connector/onedrive/collections_test.go @@ -11,8 +11,11 @@ import ( "golang.org/x/exp/maps" "github.com/alcionai/corso/src/internal/connector/graph" + "github.com/alcionai/corso/src/internal/connector/support" + "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/control" + "github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/selectors" ) @@ -621,13 +624,304 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { } } -func driveItem(id string, name string, path string, isFile, isFolder, isPackage bool) models.DriveItemable { +func (suite *OneDriveCollectionsSuite) TestDeserializeMetadata() { + tenant := "a-tenant" + user := "a-user" + driveID1 := "1" + driveID2 := "2" + deltaURL1 := "url/1" + deltaURL2 := "url/2" + + folderID1 := "folder1" + folderID2 := "folder2" + path1 := "folder1/path" + path2 := "folder2/path" + + table := []struct { + name string + // Each function returns the set of files for a single data.Collection. + cols []func() []graph.MetadataCollectionEntry + expectedDeltas map[string]string + expectedPaths map[string]map[string]string + errCheck assert.ErrorAssertionFunc + }{ + { + name: "SuccessOneDriveAllOneCollection", + cols: []func() []graph.MetadataCollectionEntry{ + func() []graph.MetadataCollectionEntry { + return []graph.MetadataCollectionEntry{ + graph.NewMetadataEntry( + graph.DeltaURLsFileName, + map[string]string{driveID1: deltaURL1}, + ), + graph.NewMetadataEntry( + graph.PreviousPathFileName, + map[string]map[string]string{ + driveID1: { + folderID1: path1, + }, + }, + ), + } + }, + }, + expectedDeltas: map[string]string{ + driveID1: deltaURL1, + }, + expectedPaths: map[string]map[string]string{ + driveID1: { + folderID1: path1, + }, + }, + errCheck: assert.NoError, + }, + { + name: "MissingPaths", + cols: []func() []graph.MetadataCollectionEntry{ + func() []graph.MetadataCollectionEntry { + return []graph.MetadataCollectionEntry{ + graph.NewMetadataEntry( + graph.DeltaURLsFileName, + map[string]string{driveID1: deltaURL1}, + ), + } + }, + }, + expectedDeltas: map[string]string{}, + expectedPaths: map[string]map[string]string{}, + errCheck: assert.NoError, + }, + { + name: "MissingDeltas", + cols: []func() []graph.MetadataCollectionEntry{ + func() []graph.MetadataCollectionEntry { + return []graph.MetadataCollectionEntry{ + graph.NewMetadataEntry( + graph.PreviousPathFileName, + map[string]map[string]string{ + driveID1: { + folderID1: path1, + }, + }, + ), + } + }, + }, + expectedDeltas: map[string]string{}, + expectedPaths: map[string]map[string]string{}, + errCheck: assert.NoError, + }, + { + name: "SuccessTwoDrivesTwoCollections", + cols: []func() []graph.MetadataCollectionEntry{ + func() []graph.MetadataCollectionEntry { + return []graph.MetadataCollectionEntry{ + graph.NewMetadataEntry( + graph.DeltaURLsFileName, + map[string]string{driveID1: deltaURL1}, + ), + graph.NewMetadataEntry( + graph.PreviousPathFileName, + map[string]map[string]string{ + driveID1: { + folderID1: path1, + }, + }, + ), + } + }, + func() []graph.MetadataCollectionEntry { + return []graph.MetadataCollectionEntry{ + graph.NewMetadataEntry( + graph.DeltaURLsFileName, + map[string]string{driveID2: deltaURL2}, + ), + graph.NewMetadataEntry( + graph.PreviousPathFileName, + map[string]map[string]string{ + driveID2: { + folderID2: path2, + }, + }, + ), + } + }, + }, + expectedDeltas: map[string]string{ + driveID1: deltaURL1, + driveID2: deltaURL2, + }, + expectedPaths: map[string]map[string]string{ + driveID1: { + folderID1: path1, + }, + driveID2: { + folderID2: path2, + }, + }, + errCheck: assert.NoError, + }, + { + // Bad formats are logged but skip adding entries to the maps and don't + // return an error. + name: "BadFormat", + cols: []func() []graph.MetadataCollectionEntry{ + func() []graph.MetadataCollectionEntry { + return []graph.MetadataCollectionEntry{ + graph.NewMetadataEntry( + graph.PreviousPathFileName, + map[string]string{driveID1: deltaURL1}, + ), + } + }, + }, + expectedDeltas: map[string]string{}, + expectedPaths: map[string]map[string]string{}, + errCheck: assert.NoError, + }, + { + // Unexpected files are logged and skipped. They don't cause an error to + // be returned. + name: "BadFileName", + cols: []func() []graph.MetadataCollectionEntry{ + func() []graph.MetadataCollectionEntry { + return []graph.MetadataCollectionEntry{ + graph.NewMetadataEntry( + graph.DeltaURLsFileName, + map[string]string{driveID1: deltaURL1}, + ), + graph.NewMetadataEntry( + graph.PreviousPathFileName, + map[string]map[string]string{ + driveID1: { + folderID1: path1, + }, + }, + ), + graph.NewMetadataEntry( + "foo", + map[string]string{driveID1: deltaURL1}, + ), + } + }, + }, + expectedDeltas: map[string]string{ + driveID1: deltaURL1, + }, + expectedPaths: map[string]map[string]string{ + driveID1: { + folderID1: path1, + }, + }, + errCheck: assert.NoError, + }, + { + name: "DriveAlreadyFound_Paths", + cols: []func() []graph.MetadataCollectionEntry{ + func() []graph.MetadataCollectionEntry { + return []graph.MetadataCollectionEntry{ + graph.NewMetadataEntry( + graph.DeltaURLsFileName, + map[string]string{driveID1: deltaURL1}, + ), + graph.NewMetadataEntry( + graph.PreviousPathFileName, + map[string]map[string]string{ + driveID1: { + folderID1: path1, + }, + }, + ), + } + }, + func() []graph.MetadataCollectionEntry { + return []graph.MetadataCollectionEntry{ + graph.NewMetadataEntry( + graph.PreviousPathFileName, + map[string]map[string]string{ + driveID1: { + folderID2: path2, + }, + }, + ), + } + }, + }, + expectedDeltas: nil, + expectedPaths: nil, + errCheck: assert.Error, + }, + { + name: "DriveAlreadyFound_Deltas", + cols: []func() []graph.MetadataCollectionEntry{ + func() []graph.MetadataCollectionEntry { + return []graph.MetadataCollectionEntry{ + graph.NewMetadataEntry( + graph.DeltaURLsFileName, + map[string]string{driveID1: deltaURL1}, + ), + graph.NewMetadataEntry( + graph.PreviousPathFileName, + map[string]map[string]string{ + driveID1: { + folderID1: path1, + }, + }, + ), + } + }, + func() []graph.MetadataCollectionEntry { + return []graph.MetadataCollectionEntry{ + graph.NewMetadataEntry( + graph.DeltaURLsFileName, + map[string]string{driveID1: deltaURL2}, + ), + } + }, + }, + expectedDeltas: nil, + expectedPaths: nil, + errCheck: assert.Error, + }, + } + + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + ctx, flush := tester.NewContext() + defer flush() + + cols := []data.Collection{} + + for _, c := range test.cols { + mc, err := graph.MakeMetadataCollection( + tenant, + user, + path.OneDriveService, + path.FilesCategory, + c(), + func(*support.ConnectorOperationStatus) {}, + ) + require.NoError(t, err) + + cols = append(cols, mc) + } + + deltas, paths, err := deserializeMetadata(ctx, cols) + test.errCheck(t, err) + + assert.Equal(t, test.expectedDeltas, deltas) + assert.Equal(t, test.expectedPaths, paths) + }) + } +} + +func driveItem(id string, name string, parentPath string, isFile, isFolder, isPackage bool) models.DriveItemable { item := models.NewDriveItem() item.SetName(&name) item.SetId(&id) parentReference := models.NewItemReference() - parentReference.SetPath(&path) + parentReference.SetPath(&parentPath) item.SetParentReference(parentReference) switch { @@ -644,13 +938,13 @@ func driveItem(id string, name string, path string, isFile, isFolder, isPackage // delItem creates a DriveItemable that is marked as deleted. path must be set // to the base drive path. -func delItem(id string, path string, isFile, isFolder, isPackage bool) models.DriveItemable { +func delItem(id string, parentPath string, isFile, isFolder, isPackage bool) models.DriveItemable { item := models.NewDriveItem() item.SetId(&id) item.SetDeleted(models.NewDeleted()) parentReference := models.NewItemReference() - parentReference.SetPath(&path) + parentReference.SetPath(&parentPath) item.SetParentReference(parentReference) switch { diff --git a/src/internal/connector/onedrive/drive_test.go b/src/internal/connector/onedrive/drive_test.go index 36fef30ab..0ba3ec1c2 100644 --- a/src/internal/connector/onedrive/drive_test.go +++ b/src/internal/connector/onedrive/drive_test.go @@ -463,7 +463,7 @@ func (suite *OneDriveSuite) TestOneDriveNewCollections() { service, service.updateStatus, control.Options{}, - ).Get(ctx) + ).Get(ctx, nil) assert.NoError(t, err) // Don't expect excludes as this isn't an incremental backup. assert.Empty(t, excludes) diff --git a/src/internal/connector/sharepoint/data_collections.go b/src/internal/connector/sharepoint/data_collections.go index 6011c32a0..1fd2f786d 100644 --- a/src/internal/connector/sharepoint/data_collections.go +++ b/src/internal/connector/sharepoint/data_collections.go @@ -152,7 +152,9 @@ func collectLibraries( updater.UpdateStatus, ctrlOpts) - odcs, excludes, err := colls.Get(ctx) + // TODO(ashmrtn): Pass previous backup metadata when SharePoint supports delta + // token-based incrementals. + odcs, excludes, err := colls.Get(ctx, nil) if err != nil { return nil, nil, support.WrapAndAppend(siteID, err, errs) } From c510af3fda60202f7379b4da06c2bf2b36f4b2eb Mon Sep 17 00:00:00 2001 From: Vaibhav Kamra Date: Tue, 31 Jan 2023 17:01:31 -0800 Subject: [PATCH 04/40] Bump up max page size preference for exchange delta requests (#2338) ## Description Reduces the number of roundtrips when requesting delta records for exchange TODO: Need to investigate OneDrive behavior - this header doesn't appear to work with OneDrive. ## Does this PR need a docs update or release note? - [x] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [ ] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * #2332 ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [x] :green_heart: E2E --- CHANGELOG.md | 1 + .../connector/exchange/api/options.go | 22 +++++++++++++++++++ src/internal/connector/exchange/api/shared.go | 3 +++ 3 files changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b96aca9e5..3333c7ad6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Document Corso's fault-tolerance and restartability features - Add retries on timeouts and status code 500 for Exchange +- Increase page size preference for delta requests for Exchange to reduce number of roundtrips ## [v0.2.0] (alpha) - 2023-1-29 diff --git a/src/internal/connector/exchange/api/options.go b/src/internal/connector/exchange/api/options.go index 49debf334..67725225f 100644 --- a/src/internal/connector/exchange/api/options.go +++ b/src/internal/connector/exchange/api/options.go @@ -3,6 +3,7 @@ package api import ( "fmt" + abstractions "github.com/microsoft/kiota-abstractions-go" "github.com/microsoftgraph/msgraph-sdk-go/users" ) @@ -53,6 +54,16 @@ var ( } ) +const ( + // headerKeyPrefer is used to set query preferences + headerKeyPrefer = "Prefer" + // maxPageSizeHeaderFmt is used to indicate max page size + // preferences + maxPageSizeHeaderFmt = "odata.maxpagesize=%d" + // deltaMaxPageSize is the max page size to use for delta queries + deltaMaxPageSize = 200 +) + // ----------------------------------------------------------------------- // exchange.Query Option Section // These functions can be used to filter a response on M365 @@ -71,8 +82,10 @@ func optionsForFolderMessagesDelta( requestParameters := &users.ItemMailFoldersItemMessagesDeltaRequestBuilderGetQueryParameters{ Select: selecting, } + options := &users.ItemMailFoldersItemMessagesDeltaRequestBuilderGetRequestConfiguration{ QueryParameters: requestParameters, + Headers: buildDeltaRequestHeaders(), } return options, nil @@ -218,6 +231,7 @@ func optionsForContactFoldersItemDelta( options := &users.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration{ QueryParameters: requestParameters, + Headers: buildDeltaRequestHeaders(), } return options, nil @@ -275,3 +289,11 @@ func buildOptions(fields []string, allowed map[string]struct{}) ([]string, error return append(returnedOptions, fields...), nil } + +// buildDeltaRequestHeaders returns the headers we add to delta page requests +func buildDeltaRequestHeaders() *abstractions.RequestHeaders { + headers := abstractions.NewRequestHeaders() + headers.Add(headerKeyPrefer, fmt.Sprintf(maxPageSizeHeaderFmt, deltaMaxPageSize)) + + return headers +} diff --git a/src/internal/connector/exchange/api/shared.go b/src/internal/connector/exchange/api/shared.go index d89ce7411..9cb4d8ab0 100644 --- a/src/internal/connector/exchange/api/shared.go +++ b/src/internal/connector/exchange/api/shared.go @@ -8,6 +8,7 @@ import ( "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph/api" "github.com/alcionai/corso/src/internal/connector/support" + "github.com/alcionai/corso/src/pkg/logger" ) // --------------------------------------------------------------------------- @@ -82,6 +83,8 @@ func getItemsAddedAndRemovedFromContainer( return nil, nil, "", err } + logger.Ctx(ctx).Infow("Got page", "items", len(items)) + // iterate through the items in the page for _, item := range items { // if the additional data conains a `@removed` key, the value will either From b5b457639338369e5e165788b2781b7e3493bd07 Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Tue, 31 Jan 2023 17:25:27 -0800 Subject: [PATCH 05/40] Run gofmt on beta package (#2340) ## Description This package is mostly generated code, but since it's not formatted and check in it causes subsequent runs of gofmt to format it and add chaff to git diff. Checking in the formatted version removes the chaff. ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [x] :broom: Tech Debt/Cleanup --- .../models/horizontal_section_layout_type.go | 76 ++--- .../models/meta_data_key_string_pair.go | 195 ++++++------ .../models/meta_data_key_string_pairable.go | 20 +- .../graph/betasdk/models/page_layout_type.go | 52 ++-- .../betasdk/models/page_promotion_type.go | 52 ++-- .../graph/betasdk/models/publication_facet.go | 195 ++++++------ .../betasdk/models/publication_facetable.go | 20 +- .../graph/betasdk/models/reactions_facet.go | 241 ++++++++------- .../betasdk/models/reactions_facetable.go | 24 +- .../betasdk/models/section_emphasis_type.go | 58 ++-- .../graph/betasdk/models/site_access_type.go | 46 +-- .../betasdk/models/site_security_level.go | 76 ++--- .../graph/betasdk/models/site_settings.go | 195 ++++++------ .../graph/betasdk/models/site_settingsable.go | 20 +- .../graph/betasdk/models/standard_web_part.go | 138 +++++---- .../betasdk/models/standard_web_partable.go | 16 +- .../graph/betasdk/models/text_web_part.go | 92 +++--- .../graph/betasdk/models/text_web_partable.go | 12 +- .../betasdk/models/title_area_layout_type.go | 58 ++-- .../models/title_area_text_alignment_type.go | 46 +-- .../graph/betasdk/models/web_part_position.go | 287 +++++++++--------- .../betasdk/models/web_part_positionable.go | 28 +- 22 files changed, 1019 insertions(+), 928 deletions(-) diff --git a/src/internal/connector/graph/betasdk/models/horizontal_section_layout_type.go b/src/internal/connector/graph/betasdk/models/horizontal_section_layout_type.go index 43c2643a2..80e208ffe 100644 --- a/src/internal/connector/graph/betasdk/models/horizontal_section_layout_type.go +++ b/src/internal/connector/graph/betasdk/models/horizontal_section_layout_type.go @@ -1,52 +1,54 @@ package models + import ( - "errors" + "errors" ) + // Provides operations to call the remove method. type HorizontalSectionLayoutType int const ( - NONE_HORIZONTALSECTIONLAYOUTTYPE HorizontalSectionLayoutType = iota - ONECOLUMN_HORIZONTALSECTIONLAYOUTTYPE - TWOCOLUMNS_HORIZONTALSECTIONLAYOUTTYPE - THREECOLUMNS_HORIZONTALSECTIONLAYOUTTYPE - ONETHIRDLEFTCOLUMN_HORIZONTALSECTIONLAYOUTTYPE - ONETHIRDRIGHTCOLUMN_HORIZONTALSECTIONLAYOUTTYPE - FULLWIDTH_HORIZONTALSECTIONLAYOUTTYPE - UNKNOWNFUTUREVALUE_HORIZONTALSECTIONLAYOUTTYPE + NONE_HORIZONTALSECTIONLAYOUTTYPE HorizontalSectionLayoutType = iota + ONECOLUMN_HORIZONTALSECTIONLAYOUTTYPE + TWOCOLUMNS_HORIZONTALSECTIONLAYOUTTYPE + THREECOLUMNS_HORIZONTALSECTIONLAYOUTTYPE + ONETHIRDLEFTCOLUMN_HORIZONTALSECTIONLAYOUTTYPE + ONETHIRDRIGHTCOLUMN_HORIZONTALSECTIONLAYOUTTYPE + FULLWIDTH_HORIZONTALSECTIONLAYOUTTYPE + UNKNOWNFUTUREVALUE_HORIZONTALSECTIONLAYOUTTYPE ) func (i HorizontalSectionLayoutType) String() string { - return []string{"none", "oneColumn", "twoColumns", "threeColumns", "oneThirdLeftColumn", "oneThirdRightColumn", "fullWidth", "unknownFutureValue"}[i] + return []string{"none", "oneColumn", "twoColumns", "threeColumns", "oneThirdLeftColumn", "oneThirdRightColumn", "fullWidth", "unknownFutureValue"}[i] } func ParseHorizontalSectionLayoutType(v string) (interface{}, error) { - result := NONE_HORIZONTALSECTIONLAYOUTTYPE - switch v { - case "none": - result = NONE_HORIZONTALSECTIONLAYOUTTYPE - case "oneColumn": - result = ONECOLUMN_HORIZONTALSECTIONLAYOUTTYPE - case "twoColumns": - result = TWOCOLUMNS_HORIZONTALSECTIONLAYOUTTYPE - case "threeColumns": - result = THREECOLUMNS_HORIZONTALSECTIONLAYOUTTYPE - case "oneThirdLeftColumn": - result = ONETHIRDLEFTCOLUMN_HORIZONTALSECTIONLAYOUTTYPE - case "oneThirdRightColumn": - result = ONETHIRDRIGHTCOLUMN_HORIZONTALSECTIONLAYOUTTYPE - case "fullWidth": - result = FULLWIDTH_HORIZONTALSECTIONLAYOUTTYPE - case "unknownFutureValue": - result = UNKNOWNFUTUREVALUE_HORIZONTALSECTIONLAYOUTTYPE - default: - return 0, errors.New("Unknown HorizontalSectionLayoutType value: " + v) - } - return &result, nil + result := NONE_HORIZONTALSECTIONLAYOUTTYPE + switch v { + case "none": + result = NONE_HORIZONTALSECTIONLAYOUTTYPE + case "oneColumn": + result = ONECOLUMN_HORIZONTALSECTIONLAYOUTTYPE + case "twoColumns": + result = TWOCOLUMNS_HORIZONTALSECTIONLAYOUTTYPE + case "threeColumns": + result = THREECOLUMNS_HORIZONTALSECTIONLAYOUTTYPE + case "oneThirdLeftColumn": + result = ONETHIRDLEFTCOLUMN_HORIZONTALSECTIONLAYOUTTYPE + case "oneThirdRightColumn": + result = ONETHIRDRIGHTCOLUMN_HORIZONTALSECTIONLAYOUTTYPE + case "fullWidth": + result = FULLWIDTH_HORIZONTALSECTIONLAYOUTTYPE + case "unknownFutureValue": + result = UNKNOWNFUTUREVALUE_HORIZONTALSECTIONLAYOUTTYPE + default: + return 0, errors.New("Unknown HorizontalSectionLayoutType value: " + v) + } + return &result, nil } func SerializeHorizontalSectionLayoutType(values []HorizontalSectionLayoutType) []string { - result := make([]string, len(values)) - for i, v := range values { - result[i] = v.String() - } - return result + result := make([]string, len(values)) + for i, v := range values { + result[i] = v.String() + } + return result } diff --git a/src/internal/connector/graph/betasdk/models/meta_data_key_string_pair.go b/src/internal/connector/graph/betasdk/models/meta_data_key_string_pair.go index e7df06165..c79f17cfb 100644 --- a/src/internal/connector/graph/betasdk/models/meta_data_key_string_pair.go +++ b/src/internal/connector/graph/betasdk/models/meta_data_key_string_pair.go @@ -1,123 +1,134 @@ package models import ( - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" ) -// MetaDataKeyStringPair +// MetaDataKeyStringPair type MetaDataKeyStringPair struct { - // Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. - additionalData map[string]interface{} - // Key of the meta data. - key *string - // The OdataType property - odataType *string - // Value of the meta data. - value *string + // Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + additionalData map[string]interface{} + // Key of the meta data. + key *string + // The OdataType property + odataType *string + // Value of the meta data. + value *string } + // NewMetaDataKeyStringPair instantiates a new metaDataKeyStringPair and sets the default values. -func NewMetaDataKeyStringPair()(*MetaDataKeyStringPair) { - m := &MetaDataKeyStringPair{ - } - m.SetAdditionalData(make(map[string]interface{})); - return m +func NewMetaDataKeyStringPair() *MetaDataKeyStringPair { + m := &MetaDataKeyStringPair{} + m.SetAdditionalData(make(map[string]interface{})) + return m } + // CreateMetaDataKeyStringPairFromDiscriminatorValue creates a new instance of the appropriate class based on discriminator value -func CreateMetaDataKeyStringPairFromDiscriminatorValue(parseNode i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable, error) { - return NewMetaDataKeyStringPair(), nil +func CreateMetaDataKeyStringPairFromDiscriminatorValue(parseNode i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) (i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable, error) { + return NewMetaDataKeyStringPair(), nil } + // GetAdditionalData gets the additionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. -func (m *MetaDataKeyStringPair) GetAdditionalData()(map[string]interface{}) { - return m.additionalData +func (m *MetaDataKeyStringPair) GetAdditionalData() map[string]interface{} { + return m.additionalData } + // GetFieldDeserializers the deserialization information for the current model -func (m *MetaDataKeyStringPair) GetFieldDeserializers()(map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(error)) { - res := make(map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(error)) - res["key"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetStringValue() - if err != nil { - return err - } - if val != nil { - m.SetKey(val) - } - return nil - } - res["@odata.type"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetStringValue() - if err != nil { - return err - } - if val != nil { - m.SetOdataType(val) - } - return nil - } - res["value"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetStringValue() - if err != nil { - return err - } - if val != nil { - m.SetValue(val) - } - return nil - } - return res +func (m *MetaDataKeyStringPair) GetFieldDeserializers() map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + res := make(map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error) + res["key"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetStringValue() + if err != nil { + return err + } + if val != nil { + m.SetKey(val) + } + return nil + } + res["@odata.type"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetStringValue() + if err != nil { + return err + } + if val != nil { + m.SetOdataType(val) + } + return nil + } + res["value"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetStringValue() + if err != nil { + return err + } + if val != nil { + m.SetValue(val) + } + return nil + } + return res } + // GetKey gets the key property value. Key of the meta data. -func (m *MetaDataKeyStringPair) GetKey()(*string) { - return m.key +func (m *MetaDataKeyStringPair) GetKey() *string { + return m.key } + // GetOdataType gets the @odata.type property value. The OdataType property -func (m *MetaDataKeyStringPair) GetOdataType()(*string) { - return m.odataType +func (m *MetaDataKeyStringPair) GetOdataType() *string { + return m.odataType } + // GetValue gets the value property value. Value of the meta data. -func (m *MetaDataKeyStringPair) GetValue()(*string) { - return m.value +func (m *MetaDataKeyStringPair) GetValue() *string { + return m.value } + // Serialize serializes information the current object -func (m *MetaDataKeyStringPair) Serialize(writer i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.SerializationWriter)(error) { - { - err := writer.WriteStringValue("key", m.GetKey()) - if err != nil { - return err - } - } - { - err := writer.WriteStringValue("@odata.type", m.GetOdataType()) - if err != nil { - return err - } - } - { - err := writer.WriteStringValue("value", m.GetValue()) - if err != nil { - return err - } - } - { - err := writer.WriteAdditionalData(m.GetAdditionalData()) - if err != nil { - return err - } - } - return nil +func (m *MetaDataKeyStringPair) Serialize(writer i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.SerializationWriter) error { + { + err := writer.WriteStringValue("key", m.GetKey()) + if err != nil { + return err + } + } + { + err := writer.WriteStringValue("@odata.type", m.GetOdataType()) + if err != nil { + return err + } + } + { + err := writer.WriteStringValue("value", m.GetValue()) + if err != nil { + return err + } + } + { + err := writer.WriteAdditionalData(m.GetAdditionalData()) + if err != nil { + return err + } + } + return nil } + // SetAdditionalData sets the additionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. -func (m *MetaDataKeyStringPair) SetAdditionalData(value map[string]interface{})() { - m.additionalData = value +func (m *MetaDataKeyStringPair) SetAdditionalData(value map[string]interface{}) { + m.additionalData = value } + // SetKey sets the key property value. Key of the meta data. -func (m *MetaDataKeyStringPair) SetKey(value *string)() { - m.key = value +func (m *MetaDataKeyStringPair) SetKey(value *string) { + m.key = value } + // SetOdataType sets the @odata.type property value. The OdataType property -func (m *MetaDataKeyStringPair) SetOdataType(value *string)() { - m.odataType = value +func (m *MetaDataKeyStringPair) SetOdataType(value *string) { + m.odataType = value } + // SetValue sets the value property value. Value of the meta data. -func (m *MetaDataKeyStringPair) SetValue(value *string)() { - m.value = value +func (m *MetaDataKeyStringPair) SetValue(value *string) { + m.value = value } diff --git a/src/internal/connector/graph/betasdk/models/meta_data_key_string_pairable.go b/src/internal/connector/graph/betasdk/models/meta_data_key_string_pairable.go index 49908469e..4168f4dce 100644 --- a/src/internal/connector/graph/betasdk/models/meta_data_key_string_pairable.go +++ b/src/internal/connector/graph/betasdk/models/meta_data_key_string_pairable.go @@ -1,17 +1,17 @@ package models import ( - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" ) -// MetaDataKeyStringPairable +// MetaDataKeyStringPairable type MetaDataKeyStringPairable interface { - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.AdditionalDataHolder - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable - GetKey()(*string) - GetOdataType()(*string) - GetValue()(*string) - SetKey(value *string)() - SetOdataType(value *string)() - SetValue(value *string)() + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.AdditionalDataHolder + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable + GetKey() *string + GetOdataType() *string + GetValue() *string + SetKey(value *string) + SetOdataType(value *string) + SetValue(value *string) } diff --git a/src/internal/connector/graph/betasdk/models/page_layout_type.go b/src/internal/connector/graph/betasdk/models/page_layout_type.go index 0338a5c30..fce795760 100644 --- a/src/internal/connector/graph/betasdk/models/page_layout_type.go +++ b/src/internal/connector/graph/betasdk/models/page_layout_type.go @@ -1,40 +1,42 @@ package models + import ( - "errors" + "errors" ) + // Provides operations to call the remove method. type PageLayoutType int const ( - MICROSOFTRESERVED_PAGELAYOUTTYPE PageLayoutType = iota - ARTICLE_PAGELAYOUTTYPE - HOME_PAGELAYOUTTYPE - UNKNOWNFUTUREVALUE_PAGELAYOUTTYPE + MICROSOFTRESERVED_PAGELAYOUTTYPE PageLayoutType = iota + ARTICLE_PAGELAYOUTTYPE + HOME_PAGELAYOUTTYPE + UNKNOWNFUTUREVALUE_PAGELAYOUTTYPE ) func (i PageLayoutType) String() string { - return []string{"microsoftReserved", "article", "home", "unknownFutureValue"}[i] + return []string{"microsoftReserved", "article", "home", "unknownFutureValue"}[i] } func ParsePageLayoutType(v string) (interface{}, error) { - result := MICROSOFTRESERVED_PAGELAYOUTTYPE - switch v { - case "microsoftReserved": - result = MICROSOFTRESERVED_PAGELAYOUTTYPE - case "article": - result = ARTICLE_PAGELAYOUTTYPE - case "home": - result = HOME_PAGELAYOUTTYPE - case "unknownFutureValue": - result = UNKNOWNFUTUREVALUE_PAGELAYOUTTYPE - default: - return 0, errors.New("Unknown PageLayoutType value: " + v) - } - return &result, nil + result := MICROSOFTRESERVED_PAGELAYOUTTYPE + switch v { + case "microsoftReserved": + result = MICROSOFTRESERVED_PAGELAYOUTTYPE + case "article": + result = ARTICLE_PAGELAYOUTTYPE + case "home": + result = HOME_PAGELAYOUTTYPE + case "unknownFutureValue": + result = UNKNOWNFUTUREVALUE_PAGELAYOUTTYPE + default: + return 0, errors.New("Unknown PageLayoutType value: " + v) + } + return &result, nil } func SerializePageLayoutType(values []PageLayoutType) []string { - result := make([]string, len(values)) - for i, v := range values { - result[i] = v.String() - } - return result + result := make([]string, len(values)) + for i, v := range values { + result[i] = v.String() + } + return result } diff --git a/src/internal/connector/graph/betasdk/models/page_promotion_type.go b/src/internal/connector/graph/betasdk/models/page_promotion_type.go index a8cbcd058..e78ce63f0 100644 --- a/src/internal/connector/graph/betasdk/models/page_promotion_type.go +++ b/src/internal/connector/graph/betasdk/models/page_promotion_type.go @@ -1,40 +1,42 @@ package models + import ( - "errors" + "errors" ) + // Provides operations to call the remove method. type PagePromotionType int const ( - MICROSOFTRESERVED_PAGEPROMOTIONTYPE PagePromotionType = iota - PAGE_PAGEPROMOTIONTYPE - NEWSPOST_PAGEPROMOTIONTYPE - UNKNOWNFUTUREVALUE_PAGEPROMOTIONTYPE + MICROSOFTRESERVED_PAGEPROMOTIONTYPE PagePromotionType = iota + PAGE_PAGEPROMOTIONTYPE + NEWSPOST_PAGEPROMOTIONTYPE + UNKNOWNFUTUREVALUE_PAGEPROMOTIONTYPE ) func (i PagePromotionType) String() string { - return []string{"microsoftReserved", "page", "newsPost", "unknownFutureValue"}[i] + return []string{"microsoftReserved", "page", "newsPost", "unknownFutureValue"}[i] } func ParsePagePromotionType(v string) (interface{}, error) { - result := MICROSOFTRESERVED_PAGEPROMOTIONTYPE - switch v { - case "microsoftReserved": - result = MICROSOFTRESERVED_PAGEPROMOTIONTYPE - case "page": - result = PAGE_PAGEPROMOTIONTYPE - case "newsPost": - result = NEWSPOST_PAGEPROMOTIONTYPE - case "unknownFutureValue": - result = UNKNOWNFUTUREVALUE_PAGEPROMOTIONTYPE - default: - return 0, errors.New("Unknown PagePromotionType value: " + v) - } - return &result, nil + result := MICROSOFTRESERVED_PAGEPROMOTIONTYPE + switch v { + case "microsoftReserved": + result = MICROSOFTRESERVED_PAGEPROMOTIONTYPE + case "page": + result = PAGE_PAGEPROMOTIONTYPE + case "newsPost": + result = NEWSPOST_PAGEPROMOTIONTYPE + case "unknownFutureValue": + result = UNKNOWNFUTUREVALUE_PAGEPROMOTIONTYPE + default: + return 0, errors.New("Unknown PagePromotionType value: " + v) + } + return &result, nil } func SerializePagePromotionType(values []PagePromotionType) []string { - result := make([]string, len(values)) - for i, v := range values { - result[i] = v.String() - } - return result + result := make([]string, len(values)) + for i, v := range values { + result[i] = v.String() + } + return result } diff --git a/src/internal/connector/graph/betasdk/models/publication_facet.go b/src/internal/connector/graph/betasdk/models/publication_facet.go index 87e59d34b..860b88bf3 100644 --- a/src/internal/connector/graph/betasdk/models/publication_facet.go +++ b/src/internal/connector/graph/betasdk/models/publication_facet.go @@ -1,123 +1,134 @@ package models import ( - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" ) -// PublicationFacet +// PublicationFacet type PublicationFacet struct { - // Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. - additionalData map[string]interface{} - // The state of publication for this document. Either published or checkout. Read-only. - level *string - // The OdataType property - odataType *string - // The unique identifier for the version that is visible to the current caller. Read-only. - versionId *string + // Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + additionalData map[string]interface{} + // The state of publication for this document. Either published or checkout. Read-only. + level *string + // The OdataType property + odataType *string + // The unique identifier for the version that is visible to the current caller. Read-only. + versionId *string } + // NewPublicationFacet instantiates a new publicationFacet and sets the default values. -func NewPublicationFacet()(*PublicationFacet) { - m := &PublicationFacet{ - } - m.SetAdditionalData(make(map[string]interface{})); - return m +func NewPublicationFacet() *PublicationFacet { + m := &PublicationFacet{} + m.SetAdditionalData(make(map[string]interface{})) + return m } + // CreatePublicationFacetFromDiscriminatorValue creates a new instance of the appropriate class based on discriminator value -func CreatePublicationFacetFromDiscriminatorValue(parseNode i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable, error) { - return NewPublicationFacet(), nil +func CreatePublicationFacetFromDiscriminatorValue(parseNode i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) (i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable, error) { + return NewPublicationFacet(), nil } + // GetAdditionalData gets the additionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. -func (m *PublicationFacet) GetAdditionalData()(map[string]interface{}) { - return m.additionalData +func (m *PublicationFacet) GetAdditionalData() map[string]interface{} { + return m.additionalData } + // GetFieldDeserializers the deserialization information for the current model -func (m *PublicationFacet) GetFieldDeserializers()(map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(error)) { - res := make(map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(error)) - res["level"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetStringValue() - if err != nil { - return err - } - if val != nil { - m.SetLevel(val) - } - return nil - } - res["@odata.type"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetStringValue() - if err != nil { - return err - } - if val != nil { - m.SetOdataType(val) - } - return nil - } - res["versionId"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetStringValue() - if err != nil { - return err - } - if val != nil { - m.SetVersionId(val) - } - return nil - } - return res +func (m *PublicationFacet) GetFieldDeserializers() map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + res := make(map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error) + res["level"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetStringValue() + if err != nil { + return err + } + if val != nil { + m.SetLevel(val) + } + return nil + } + res["@odata.type"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetStringValue() + if err != nil { + return err + } + if val != nil { + m.SetOdataType(val) + } + return nil + } + res["versionId"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetStringValue() + if err != nil { + return err + } + if val != nil { + m.SetVersionId(val) + } + return nil + } + return res } + // GetLevel gets the level property value. The state of publication for this document. Either published or checkout. Read-only. -func (m *PublicationFacet) GetLevel()(*string) { - return m.level +func (m *PublicationFacet) GetLevel() *string { + return m.level } + // GetOdataType gets the @odata.type property value. The OdataType property -func (m *PublicationFacet) GetOdataType()(*string) { - return m.odataType +func (m *PublicationFacet) GetOdataType() *string { + return m.odataType } + // GetVersionId gets the versionId property value. The unique identifier for the version that is visible to the current caller. Read-only. -func (m *PublicationFacet) GetVersionId()(*string) { - return m.versionId +func (m *PublicationFacet) GetVersionId() *string { + return m.versionId } + // Serialize serializes information the current object -func (m *PublicationFacet) Serialize(writer i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.SerializationWriter)(error) { - { - err := writer.WriteStringValue("level", m.GetLevel()) - if err != nil { - return err - } - } - { - err := writer.WriteStringValue("@odata.type", m.GetOdataType()) - if err != nil { - return err - } - } - { - err := writer.WriteStringValue("versionId", m.GetVersionId()) - if err != nil { - return err - } - } - { - err := writer.WriteAdditionalData(m.GetAdditionalData()) - if err != nil { - return err - } - } - return nil +func (m *PublicationFacet) Serialize(writer i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.SerializationWriter) error { + { + err := writer.WriteStringValue("level", m.GetLevel()) + if err != nil { + return err + } + } + { + err := writer.WriteStringValue("@odata.type", m.GetOdataType()) + if err != nil { + return err + } + } + { + err := writer.WriteStringValue("versionId", m.GetVersionId()) + if err != nil { + return err + } + } + { + err := writer.WriteAdditionalData(m.GetAdditionalData()) + if err != nil { + return err + } + } + return nil } + // SetAdditionalData sets the additionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. -func (m *PublicationFacet) SetAdditionalData(value map[string]interface{})() { - m.additionalData = value +func (m *PublicationFacet) SetAdditionalData(value map[string]interface{}) { + m.additionalData = value } + // SetLevel sets the level property value. The state of publication for this document. Either published or checkout. Read-only. -func (m *PublicationFacet) SetLevel(value *string)() { - m.level = value +func (m *PublicationFacet) SetLevel(value *string) { + m.level = value } + // SetOdataType sets the @odata.type property value. The OdataType property -func (m *PublicationFacet) SetOdataType(value *string)() { - m.odataType = value +func (m *PublicationFacet) SetOdataType(value *string) { + m.odataType = value } + // SetVersionId sets the versionId property value. The unique identifier for the version that is visible to the current caller. Read-only. -func (m *PublicationFacet) SetVersionId(value *string)() { - m.versionId = value +func (m *PublicationFacet) SetVersionId(value *string) { + m.versionId = value } diff --git a/src/internal/connector/graph/betasdk/models/publication_facetable.go b/src/internal/connector/graph/betasdk/models/publication_facetable.go index 20d82ccf8..4098c89b1 100644 --- a/src/internal/connector/graph/betasdk/models/publication_facetable.go +++ b/src/internal/connector/graph/betasdk/models/publication_facetable.go @@ -1,17 +1,17 @@ package models import ( - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" ) -// PublicationFacetable +// PublicationFacetable type PublicationFacetable interface { - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.AdditionalDataHolder - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable - GetLevel()(*string) - GetOdataType()(*string) - GetVersionId()(*string) - SetLevel(value *string)() - SetOdataType(value *string)() - SetVersionId(value *string)() + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.AdditionalDataHolder + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable + GetLevel() *string + GetOdataType() *string + GetVersionId() *string + SetLevel(value *string) + SetOdataType(value *string) + SetVersionId(value *string) } diff --git a/src/internal/connector/graph/betasdk/models/reactions_facet.go b/src/internal/connector/graph/betasdk/models/reactions_facet.go index b298a9fe1..c971925dc 100644 --- a/src/internal/connector/graph/betasdk/models/reactions_facet.go +++ b/src/internal/connector/graph/betasdk/models/reactions_facet.go @@ -1,149 +1,162 @@ package models import ( - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" ) -// ReactionsFacet +// ReactionsFacet type ReactionsFacet struct { - // Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. - additionalData map[string]interface{} - // Count of comments. - commentCount *int32 - // Count of likes. - likeCount *int32 - // The OdataType property - odataType *string - // Count of shares. - shareCount *int32 + // Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + additionalData map[string]interface{} + // Count of comments. + commentCount *int32 + // Count of likes. + likeCount *int32 + // The OdataType property + odataType *string + // Count of shares. + shareCount *int32 } + // NewReactionsFacet instantiates a new reactionsFacet and sets the default values. -func NewReactionsFacet()(*ReactionsFacet) { - m := &ReactionsFacet{ - } - m.SetAdditionalData(make(map[string]interface{})); - return m +func NewReactionsFacet() *ReactionsFacet { + m := &ReactionsFacet{} + m.SetAdditionalData(make(map[string]interface{})) + return m } + // CreateReactionsFacetFromDiscriminatorValue creates a new instance of the appropriate class based on discriminator value -func CreateReactionsFacetFromDiscriminatorValue(parseNode i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable, error) { - return NewReactionsFacet(), nil +func CreateReactionsFacetFromDiscriminatorValue(parseNode i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) (i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable, error) { + return NewReactionsFacet(), nil } + // GetAdditionalData gets the additionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. -func (m *ReactionsFacet) GetAdditionalData()(map[string]interface{}) { - return m.additionalData +func (m *ReactionsFacet) GetAdditionalData() map[string]interface{} { + return m.additionalData } + // GetCommentCount gets the commentCount property value. Count of comments. -func (m *ReactionsFacet) GetCommentCount()(*int32) { - return m.commentCount +func (m *ReactionsFacet) GetCommentCount() *int32 { + return m.commentCount } + // GetFieldDeserializers the deserialization information for the current model -func (m *ReactionsFacet) GetFieldDeserializers()(map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(error)) { - res := make(map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(error)) - res["commentCount"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetInt32Value() - if err != nil { - return err - } - if val != nil { - m.SetCommentCount(val) - } - return nil - } - res["likeCount"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetInt32Value() - if err != nil { - return err - } - if val != nil { - m.SetLikeCount(val) - } - return nil - } - res["@odata.type"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetStringValue() - if err != nil { - return err - } - if val != nil { - m.SetOdataType(val) - } - return nil - } - res["shareCount"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetInt32Value() - if err != nil { - return err - } - if val != nil { - m.SetShareCount(val) - } - return nil - } - return res +func (m *ReactionsFacet) GetFieldDeserializers() map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + res := make(map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error) + res["commentCount"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetInt32Value() + if err != nil { + return err + } + if val != nil { + m.SetCommentCount(val) + } + return nil + } + res["likeCount"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetInt32Value() + if err != nil { + return err + } + if val != nil { + m.SetLikeCount(val) + } + return nil + } + res["@odata.type"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetStringValue() + if err != nil { + return err + } + if val != nil { + m.SetOdataType(val) + } + return nil + } + res["shareCount"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetInt32Value() + if err != nil { + return err + } + if val != nil { + m.SetShareCount(val) + } + return nil + } + return res } + // GetLikeCount gets the likeCount property value. Count of likes. -func (m *ReactionsFacet) GetLikeCount()(*int32) { - return m.likeCount +func (m *ReactionsFacet) GetLikeCount() *int32 { + return m.likeCount } + // GetOdataType gets the @odata.type property value. The OdataType property -func (m *ReactionsFacet) GetOdataType()(*string) { - return m.odataType +func (m *ReactionsFacet) GetOdataType() *string { + return m.odataType } + // GetShareCount gets the shareCount property value. Count of shares. -func (m *ReactionsFacet) GetShareCount()(*int32) { - return m.shareCount +func (m *ReactionsFacet) GetShareCount() *int32 { + return m.shareCount } + // Serialize serializes information the current object -func (m *ReactionsFacet) Serialize(writer i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.SerializationWriter)(error) { - { - err := writer.WriteInt32Value("commentCount", m.GetCommentCount()) - if err != nil { - return err - } - } - { - err := writer.WriteInt32Value("likeCount", m.GetLikeCount()) - if err != nil { - return err - } - } - { - err := writer.WriteStringValue("@odata.type", m.GetOdataType()) - if err != nil { - return err - } - } - { - err := writer.WriteInt32Value("shareCount", m.GetShareCount()) - if err != nil { - return err - } - } - { - err := writer.WriteAdditionalData(m.GetAdditionalData()) - if err != nil { - return err - } - } - return nil +func (m *ReactionsFacet) Serialize(writer i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.SerializationWriter) error { + { + err := writer.WriteInt32Value("commentCount", m.GetCommentCount()) + if err != nil { + return err + } + } + { + err := writer.WriteInt32Value("likeCount", m.GetLikeCount()) + if err != nil { + return err + } + } + { + err := writer.WriteStringValue("@odata.type", m.GetOdataType()) + if err != nil { + return err + } + } + { + err := writer.WriteInt32Value("shareCount", m.GetShareCount()) + if err != nil { + return err + } + } + { + err := writer.WriteAdditionalData(m.GetAdditionalData()) + if err != nil { + return err + } + } + return nil } + // SetAdditionalData sets the additionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. -func (m *ReactionsFacet) SetAdditionalData(value map[string]interface{})() { - m.additionalData = value +func (m *ReactionsFacet) SetAdditionalData(value map[string]interface{}) { + m.additionalData = value } + // SetCommentCount sets the commentCount property value. Count of comments. -func (m *ReactionsFacet) SetCommentCount(value *int32)() { - m.commentCount = value +func (m *ReactionsFacet) SetCommentCount(value *int32) { + m.commentCount = value } + // SetLikeCount sets the likeCount property value. Count of likes. -func (m *ReactionsFacet) SetLikeCount(value *int32)() { - m.likeCount = value +func (m *ReactionsFacet) SetLikeCount(value *int32) { + m.likeCount = value } + // SetOdataType sets the @odata.type property value. The OdataType property -func (m *ReactionsFacet) SetOdataType(value *string)() { - m.odataType = value +func (m *ReactionsFacet) SetOdataType(value *string) { + m.odataType = value } + // SetShareCount sets the shareCount property value. Count of shares. -func (m *ReactionsFacet) SetShareCount(value *int32)() { - m.shareCount = value +func (m *ReactionsFacet) SetShareCount(value *int32) { + m.shareCount = value } diff --git a/src/internal/connector/graph/betasdk/models/reactions_facetable.go b/src/internal/connector/graph/betasdk/models/reactions_facetable.go index 4e5086047..acdefec37 100644 --- a/src/internal/connector/graph/betasdk/models/reactions_facetable.go +++ b/src/internal/connector/graph/betasdk/models/reactions_facetable.go @@ -1,19 +1,19 @@ package models import ( - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" ) -// ReactionsFacetable +// ReactionsFacetable type ReactionsFacetable interface { - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.AdditionalDataHolder - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable - GetCommentCount()(*int32) - GetLikeCount()(*int32) - GetOdataType()(*string) - GetShareCount()(*int32) - SetCommentCount(value *int32)() - SetLikeCount(value *int32)() - SetOdataType(value *string)() - SetShareCount(value *int32)() + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.AdditionalDataHolder + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable + GetCommentCount() *int32 + GetLikeCount() *int32 + GetOdataType() *string + GetShareCount() *int32 + SetCommentCount(value *int32) + SetLikeCount(value *int32) + SetOdataType(value *string) + SetShareCount(value *int32) } diff --git a/src/internal/connector/graph/betasdk/models/section_emphasis_type.go b/src/internal/connector/graph/betasdk/models/section_emphasis_type.go index 0016aec10..301ae839f 100644 --- a/src/internal/connector/graph/betasdk/models/section_emphasis_type.go +++ b/src/internal/connector/graph/betasdk/models/section_emphasis_type.go @@ -1,43 +1,45 @@ package models + import ( - "errors" + "errors" ) + // Provides operations to call the remove method. type SectionEmphasisType int const ( - NONE_SECTIONEMPHASISTYPE SectionEmphasisType = iota - NEUTRAL_SECTIONEMPHASISTYPE - SOFT_SECTIONEMPHASISTYPE - STRONG_SECTIONEMPHASISTYPE - UNKNOWNFUTUREVALUE_SECTIONEMPHASISTYPE + NONE_SECTIONEMPHASISTYPE SectionEmphasisType = iota + NEUTRAL_SECTIONEMPHASISTYPE + SOFT_SECTIONEMPHASISTYPE + STRONG_SECTIONEMPHASISTYPE + UNKNOWNFUTUREVALUE_SECTIONEMPHASISTYPE ) func (i SectionEmphasisType) String() string { - return []string{"none", "neutral", "soft", "strong", "unknownFutureValue"}[i] + return []string{"none", "neutral", "soft", "strong", "unknownFutureValue"}[i] } func ParseSectionEmphasisType(v string) (interface{}, error) { - result := NONE_SECTIONEMPHASISTYPE - switch v { - case "none": - result = NONE_SECTIONEMPHASISTYPE - case "neutral": - result = NEUTRAL_SECTIONEMPHASISTYPE - case "soft": - result = SOFT_SECTIONEMPHASISTYPE - case "strong": - result = STRONG_SECTIONEMPHASISTYPE - case "unknownFutureValue": - result = UNKNOWNFUTUREVALUE_SECTIONEMPHASISTYPE - default: - return 0, errors.New("Unknown SectionEmphasisType value: " + v) - } - return &result, nil + result := NONE_SECTIONEMPHASISTYPE + switch v { + case "none": + result = NONE_SECTIONEMPHASISTYPE + case "neutral": + result = NEUTRAL_SECTIONEMPHASISTYPE + case "soft": + result = SOFT_SECTIONEMPHASISTYPE + case "strong": + result = STRONG_SECTIONEMPHASISTYPE + case "unknownFutureValue": + result = UNKNOWNFUTUREVALUE_SECTIONEMPHASISTYPE + default: + return 0, errors.New("Unknown SectionEmphasisType value: " + v) + } + return &result, nil } func SerializeSectionEmphasisType(values []SectionEmphasisType) []string { - result := make([]string, len(values)) - for i, v := range values { - result[i] = v.String() - } - return result + result := make([]string, len(values)) + for i, v := range values { + result[i] = v.String() + } + return result } diff --git a/src/internal/connector/graph/betasdk/models/site_access_type.go b/src/internal/connector/graph/betasdk/models/site_access_type.go index 052a2efdb..2d4cedffe 100644 --- a/src/internal/connector/graph/betasdk/models/site_access_type.go +++ b/src/internal/connector/graph/betasdk/models/site_access_type.go @@ -1,37 +1,39 @@ package models + import ( - "errors" + "errors" ) + // Provides operations to call the remove method. type SiteAccessType int const ( - BLOCK_SITEACCESSTYPE SiteAccessType = iota - FULL_SITEACCESSTYPE - LIMITED_SITEACCESSTYPE + BLOCK_SITEACCESSTYPE SiteAccessType = iota + FULL_SITEACCESSTYPE + LIMITED_SITEACCESSTYPE ) func (i SiteAccessType) String() string { - return []string{"block", "full", "limited"}[i] + return []string{"block", "full", "limited"}[i] } func ParseSiteAccessType(v string) (interface{}, error) { - result := BLOCK_SITEACCESSTYPE - switch v { - case "block": - result = BLOCK_SITEACCESSTYPE - case "full": - result = FULL_SITEACCESSTYPE - case "limited": - result = LIMITED_SITEACCESSTYPE - default: - return 0, errors.New("Unknown SiteAccessType value: " + v) - } - return &result, nil + result := BLOCK_SITEACCESSTYPE + switch v { + case "block": + result = BLOCK_SITEACCESSTYPE + case "full": + result = FULL_SITEACCESSTYPE + case "limited": + result = LIMITED_SITEACCESSTYPE + default: + return 0, errors.New("Unknown SiteAccessType value: " + v) + } + return &result, nil } func SerializeSiteAccessType(values []SiteAccessType) []string { - result := make([]string, len(values)) - for i, v := range values { - result[i] = v.String() - } - return result + result := make([]string, len(values)) + for i, v := range values { + result[i] = v.String() + } + return result } diff --git a/src/internal/connector/graph/betasdk/models/site_security_level.go b/src/internal/connector/graph/betasdk/models/site_security_level.go index d2733ce47..0c75c164e 100644 --- a/src/internal/connector/graph/betasdk/models/site_security_level.go +++ b/src/internal/connector/graph/betasdk/models/site_security_level.go @@ -1,52 +1,54 @@ package models + import ( - "errors" + "errors" ) + // Provides operations to call the add method. type SiteSecurityLevel int const ( - // User Defined, default value, no intent. - USERDEFINED_SITESECURITYLEVEL SiteSecurityLevel = iota - // Low. - LOW_SITESECURITYLEVEL - // Medium-low. - MEDIUMLOW_SITESECURITYLEVEL - // Medium. - MEDIUM_SITESECURITYLEVEL - // Medium-high. - MEDIUMHIGH_SITESECURITYLEVEL - // High. - HIGH_SITESECURITYLEVEL + // User Defined, default value, no intent. + USERDEFINED_SITESECURITYLEVEL SiteSecurityLevel = iota + // Low. + LOW_SITESECURITYLEVEL + // Medium-low. + MEDIUMLOW_SITESECURITYLEVEL + // Medium. + MEDIUM_SITESECURITYLEVEL + // Medium-high. + MEDIUMHIGH_SITESECURITYLEVEL + // High. + HIGH_SITESECURITYLEVEL ) func (i SiteSecurityLevel) String() string { - return []string{"userDefined", "low", "mediumLow", "medium", "mediumHigh", "high"}[i] + return []string{"userDefined", "low", "mediumLow", "medium", "mediumHigh", "high"}[i] } func ParseSiteSecurityLevel(v string) (interface{}, error) { - result := USERDEFINED_SITESECURITYLEVEL - switch v { - case "userDefined": - result = USERDEFINED_SITESECURITYLEVEL - case "low": - result = LOW_SITESECURITYLEVEL - case "mediumLow": - result = MEDIUMLOW_SITESECURITYLEVEL - case "medium": - result = MEDIUM_SITESECURITYLEVEL - case "mediumHigh": - result = MEDIUMHIGH_SITESECURITYLEVEL - case "high": - result = HIGH_SITESECURITYLEVEL - default: - return 0, errors.New("Unknown SiteSecurityLevel value: " + v) - } - return &result, nil + result := USERDEFINED_SITESECURITYLEVEL + switch v { + case "userDefined": + result = USERDEFINED_SITESECURITYLEVEL + case "low": + result = LOW_SITESECURITYLEVEL + case "mediumLow": + result = MEDIUMLOW_SITESECURITYLEVEL + case "medium": + result = MEDIUM_SITESECURITYLEVEL + case "mediumHigh": + result = MEDIUMHIGH_SITESECURITYLEVEL + case "high": + result = HIGH_SITESECURITYLEVEL + default: + return 0, errors.New("Unknown SiteSecurityLevel value: " + v) + } + return &result, nil } func SerializeSiteSecurityLevel(values []SiteSecurityLevel) []string { - result := make([]string, len(values)) - for i, v := range values { - result[i] = v.String() - } - return result + result := make([]string, len(values)) + for i, v := range values { + result[i] = v.String() + } + return result } diff --git a/src/internal/connector/graph/betasdk/models/site_settings.go b/src/internal/connector/graph/betasdk/models/site_settings.go index a2a36d94a..1f8930408 100644 --- a/src/internal/connector/graph/betasdk/models/site_settings.go +++ b/src/internal/connector/graph/betasdk/models/site_settings.go @@ -1,123 +1,134 @@ package models import ( - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" ) -// SiteSettings +// SiteSettings type SiteSettings struct { - // Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. - additionalData map[string]interface{} - // The language tag for the language used on this site. - languageTag *string - // The OdataType property - odataType *string - // Indicates the time offset for the time zone of the site from Coordinated Universal Time (UTC). - timeZone *string + // Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + additionalData map[string]interface{} + // The language tag for the language used on this site. + languageTag *string + // The OdataType property + odataType *string + // Indicates the time offset for the time zone of the site from Coordinated Universal Time (UTC). + timeZone *string } + // NewSiteSettings instantiates a new siteSettings and sets the default values. -func NewSiteSettings()(*SiteSettings) { - m := &SiteSettings{ - } - m.SetAdditionalData(make(map[string]interface{})); - return m +func NewSiteSettings() *SiteSettings { + m := &SiteSettings{} + m.SetAdditionalData(make(map[string]interface{})) + return m } + // CreateSiteSettingsFromDiscriminatorValue creates a new instance of the appropriate class based on discriminator value -func CreateSiteSettingsFromDiscriminatorValue(parseNode i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable, error) { - return NewSiteSettings(), nil +func CreateSiteSettingsFromDiscriminatorValue(parseNode i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) (i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable, error) { + return NewSiteSettings(), nil } + // GetAdditionalData gets the additionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. -func (m *SiteSettings) GetAdditionalData()(map[string]interface{}) { - return m.additionalData +func (m *SiteSettings) GetAdditionalData() map[string]interface{} { + return m.additionalData } + // GetFieldDeserializers the deserialization information for the current model -func (m *SiteSettings) GetFieldDeserializers()(map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(error)) { - res := make(map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(error)) - res["languageTag"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetStringValue() - if err != nil { - return err - } - if val != nil { - m.SetLanguageTag(val) - } - return nil - } - res["@odata.type"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetStringValue() - if err != nil { - return err - } - if val != nil { - m.SetOdataType(val) - } - return nil - } - res["timeZone"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetStringValue() - if err != nil { - return err - } - if val != nil { - m.SetTimeZone(val) - } - return nil - } - return res +func (m *SiteSettings) GetFieldDeserializers() map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + res := make(map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error) + res["languageTag"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetStringValue() + if err != nil { + return err + } + if val != nil { + m.SetLanguageTag(val) + } + return nil + } + res["@odata.type"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetStringValue() + if err != nil { + return err + } + if val != nil { + m.SetOdataType(val) + } + return nil + } + res["timeZone"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetStringValue() + if err != nil { + return err + } + if val != nil { + m.SetTimeZone(val) + } + return nil + } + return res } + // GetLanguageTag gets the languageTag property value. The language tag for the language used on this site. -func (m *SiteSettings) GetLanguageTag()(*string) { - return m.languageTag +func (m *SiteSettings) GetLanguageTag() *string { + return m.languageTag } + // GetOdataType gets the @odata.type property value. The OdataType property -func (m *SiteSettings) GetOdataType()(*string) { - return m.odataType +func (m *SiteSettings) GetOdataType() *string { + return m.odataType } + // GetTimeZone gets the timeZone property value. Indicates the time offset for the time zone of the site from Coordinated Universal Time (UTC). -func (m *SiteSettings) GetTimeZone()(*string) { - return m.timeZone +func (m *SiteSettings) GetTimeZone() *string { + return m.timeZone } + // Serialize serializes information the current object -func (m *SiteSettings) Serialize(writer i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.SerializationWriter)(error) { - { - err := writer.WriteStringValue("languageTag", m.GetLanguageTag()) - if err != nil { - return err - } - } - { - err := writer.WriteStringValue("@odata.type", m.GetOdataType()) - if err != nil { - return err - } - } - { - err := writer.WriteStringValue("timeZone", m.GetTimeZone()) - if err != nil { - return err - } - } - { - err := writer.WriteAdditionalData(m.GetAdditionalData()) - if err != nil { - return err - } - } - return nil +func (m *SiteSettings) Serialize(writer i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.SerializationWriter) error { + { + err := writer.WriteStringValue("languageTag", m.GetLanguageTag()) + if err != nil { + return err + } + } + { + err := writer.WriteStringValue("@odata.type", m.GetOdataType()) + if err != nil { + return err + } + } + { + err := writer.WriteStringValue("timeZone", m.GetTimeZone()) + if err != nil { + return err + } + } + { + err := writer.WriteAdditionalData(m.GetAdditionalData()) + if err != nil { + return err + } + } + return nil } + // SetAdditionalData sets the additionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. -func (m *SiteSettings) SetAdditionalData(value map[string]interface{})() { - m.additionalData = value +func (m *SiteSettings) SetAdditionalData(value map[string]interface{}) { + m.additionalData = value } + // SetLanguageTag sets the languageTag property value. The language tag for the language used on this site. -func (m *SiteSettings) SetLanguageTag(value *string)() { - m.languageTag = value +func (m *SiteSettings) SetLanguageTag(value *string) { + m.languageTag = value } + // SetOdataType sets the @odata.type property value. The OdataType property -func (m *SiteSettings) SetOdataType(value *string)() { - m.odataType = value +func (m *SiteSettings) SetOdataType(value *string) { + m.odataType = value } + // SetTimeZone sets the timeZone property value. Indicates the time offset for the time zone of the site from Coordinated Universal Time (UTC). -func (m *SiteSettings) SetTimeZone(value *string)() { - m.timeZone = value +func (m *SiteSettings) SetTimeZone(value *string) { + m.timeZone = value } diff --git a/src/internal/connector/graph/betasdk/models/site_settingsable.go b/src/internal/connector/graph/betasdk/models/site_settingsable.go index 0423550ea..1b3825e05 100644 --- a/src/internal/connector/graph/betasdk/models/site_settingsable.go +++ b/src/internal/connector/graph/betasdk/models/site_settingsable.go @@ -1,17 +1,17 @@ package models import ( - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" ) -// SiteSettingsable +// SiteSettingsable type SiteSettingsable interface { - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.AdditionalDataHolder - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable - GetLanguageTag()(*string) - GetOdataType()(*string) - GetTimeZone()(*string) - SetLanguageTag(value *string)() - SetOdataType(value *string)() - SetTimeZone(value *string)() + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.AdditionalDataHolder + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable + GetLanguageTag() *string + GetOdataType() *string + GetTimeZone() *string + SetLanguageTag(value *string) + SetOdataType(value *string) + SetTimeZone(value *string) } diff --git a/src/internal/connector/graph/betasdk/models/standard_web_part.go b/src/internal/connector/graph/betasdk/models/standard_web_part.go index 0b7b4427a..4532e1d24 100644 --- a/src/internal/connector/graph/betasdk/models/standard_web_part.go +++ b/src/internal/connector/graph/betasdk/models/standard_web_part.go @@ -1,88 +1,96 @@ package models import ( - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" ) -// StandardWebPart +// StandardWebPart type StandardWebPart struct { - WebPart - // Data of the webPart. - data WebPartDataable - // A Guid which indicates the type of the webParts - webPartType *string + WebPart + // Data of the webPart. + data WebPartDataable + // A Guid which indicates the type of the webParts + webPartType *string } + // NewStandardWebPart instantiates a new StandardWebPart and sets the default values. -func NewStandardWebPart()(*StandardWebPart) { - m := &StandardWebPart{ - WebPart: *NewWebPart(), - } - odataTypeValue := "#microsoft.graph.standardWebPart"; - m.SetOdataType(&odataTypeValue); - return m +func NewStandardWebPart() *StandardWebPart { + m := &StandardWebPart{ + WebPart: *NewWebPart(), + } + odataTypeValue := "#microsoft.graph.standardWebPart" + m.SetOdataType(&odataTypeValue) + return m } + // CreateStandardWebPartFromDiscriminatorValue creates a new instance of the appropriate class based on discriminator value -func CreateStandardWebPartFromDiscriminatorValue(parseNode i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable, error) { - return NewStandardWebPart(), nil +func CreateStandardWebPartFromDiscriminatorValue(parseNode i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) (i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable, error) { + return NewStandardWebPart(), nil } + // GetData gets the data property value. Data of the webPart. -func (m *StandardWebPart) GetData()(WebPartDataable) { - return m.data +func (m *StandardWebPart) GetData() WebPartDataable { + return m.data } + // GetFieldDeserializers the deserialization information for the current model -func (m *StandardWebPart) GetFieldDeserializers()(map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(error)) { - res := m.WebPart.GetFieldDeserializers() - res["data"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetObjectValue(CreateWebPartDataFromDiscriminatorValue) - if err != nil { - return err - } - if val != nil { - m.SetData(val.(WebPartDataable)) - } - return nil - } - res["webPartType"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetStringValue() - if err != nil { - return err - } - if val != nil { - m.SetWebPartType(val) - } - return nil - } - return res +func (m *StandardWebPart) GetFieldDeserializers() map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + res := m.WebPart.GetFieldDeserializers() + res["data"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetObjectValue(CreateWebPartDataFromDiscriminatorValue) + if err != nil { + return err + } + if val != nil { + m.SetData(val.(WebPartDataable)) + } + return nil + } + res["webPartType"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetStringValue() + if err != nil { + return err + } + if val != nil { + m.SetWebPartType(val) + } + return nil + } + return res } + // GetWebPartType gets the webPartType property value. A Guid which indicates the type of the webParts -func (m *StandardWebPart) GetWebPartType()(*string) { - return m.webPartType +func (m *StandardWebPart) GetWebPartType() *string { + return m.webPartType } + // Serialize serializes information the current object -func (m *StandardWebPart) Serialize(writer i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.SerializationWriter)(error) { - err := m.WebPart.Serialize(writer) - if err != nil { - return err - } - { - err = writer.WriteObjectValue("data", m.GetData()) - if err != nil { - return err - } - } - { - err = writer.WriteStringValue("webPartType", m.GetWebPartType()) - if err != nil { - return err - } - } - return nil +func (m *StandardWebPart) Serialize(writer i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.SerializationWriter) error { + err := m.WebPart.Serialize(writer) + if err != nil { + return err + } + { + err = writer.WriteObjectValue("data", m.GetData()) + if err != nil { + return err + } + } + { + err = writer.WriteStringValue("webPartType", m.GetWebPartType()) + if err != nil { + return err + } + } + return nil } + // SetData sets the data property value. Data of the webPart. -func (m *StandardWebPart) SetData(value WebPartDataable)() { - m.data = value +func (m *StandardWebPart) SetData(value WebPartDataable) { + m.data = value } + // SetWebPartType sets the webPartType property value. A Guid which indicates the type of the webParts -func (m *StandardWebPart) SetWebPartType(value *string)() { - m.webPartType = value +func (m *StandardWebPart) SetWebPartType(value *string) { + m.webPartType = value } diff --git a/src/internal/connector/graph/betasdk/models/standard_web_partable.go b/src/internal/connector/graph/betasdk/models/standard_web_partable.go index e09160b2b..b33c25f15 100644 --- a/src/internal/connector/graph/betasdk/models/standard_web_partable.go +++ b/src/internal/connector/graph/betasdk/models/standard_web_partable.go @@ -1,15 +1,15 @@ package models import ( - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" ) -// StandardWebPartable +// StandardWebPartable type StandardWebPartable interface { - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable - WebPartable - GetData()(WebPartDataable) - GetWebPartType()(*string) - SetData(value WebPartDataable)() - SetWebPartType(value *string)() + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable + WebPartable + GetData() WebPartDataable + GetWebPartType() *string + SetData(value WebPartDataable) + SetWebPartType(value *string) } diff --git a/src/internal/connector/graph/betasdk/models/text_web_part.go b/src/internal/connector/graph/betasdk/models/text_web_part.go index f607ffa31..1ae554671 100644 --- a/src/internal/connector/graph/betasdk/models/text_web_part.go +++ b/src/internal/connector/graph/betasdk/models/text_web_part.go @@ -1,62 +1,68 @@ package models import ( - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" ) -// TextWebPart +// TextWebPart type TextWebPart struct { - WebPart - // The HTML string in text web part. - innerHtml *string + WebPart + // The HTML string in text web part. + innerHtml *string } + // NewTextWebPart instantiates a new TextWebPart and sets the default values. -func NewTextWebPart()(*TextWebPart) { - m := &TextWebPart{ - WebPart: *NewWebPart(), - } - odataTypeValue := "#microsoft.graph.textWebPart"; - m.SetOdataType(&odataTypeValue); - return m +func NewTextWebPart() *TextWebPart { + m := &TextWebPart{ + WebPart: *NewWebPart(), + } + odataTypeValue := "#microsoft.graph.textWebPart" + m.SetOdataType(&odataTypeValue) + return m } + // CreateTextWebPartFromDiscriminatorValue creates a new instance of the appropriate class based on discriminator value -func CreateTextWebPartFromDiscriminatorValue(parseNode i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable, error) { - return NewTextWebPart(), nil +func CreateTextWebPartFromDiscriminatorValue(parseNode i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) (i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable, error) { + return NewTextWebPart(), nil } + // GetFieldDeserializers the deserialization information for the current model -func (m *TextWebPart) GetFieldDeserializers()(map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(error)) { - res := m.WebPart.GetFieldDeserializers() - res["innerHtml"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetStringValue() - if err != nil { - return err - } - if val != nil { - m.SetInnerHtml(val) - } - return nil - } - return res +func (m *TextWebPart) GetFieldDeserializers() map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + res := m.WebPart.GetFieldDeserializers() + res["innerHtml"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetStringValue() + if err != nil { + return err + } + if val != nil { + m.SetInnerHtml(val) + } + return nil + } + return res } + // GetInnerHtml gets the innerHtml property value. The HTML string in text web part. -func (m *TextWebPart) GetInnerHtml()(*string) { - return m.innerHtml +func (m *TextWebPart) GetInnerHtml() *string { + return m.innerHtml } + // Serialize serializes information the current object -func (m *TextWebPart) Serialize(writer i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.SerializationWriter)(error) { - err := m.WebPart.Serialize(writer) - if err != nil { - return err - } - { - err = writer.WriteStringValue("innerHtml", m.GetInnerHtml()) - if err != nil { - return err - } - } - return nil +func (m *TextWebPart) Serialize(writer i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.SerializationWriter) error { + err := m.WebPart.Serialize(writer) + if err != nil { + return err + } + { + err = writer.WriteStringValue("innerHtml", m.GetInnerHtml()) + if err != nil { + return err + } + } + return nil } + // SetInnerHtml sets the innerHtml property value. The HTML string in text web part. -func (m *TextWebPart) SetInnerHtml(value *string)() { - m.innerHtml = value +func (m *TextWebPart) SetInnerHtml(value *string) { + m.innerHtml = value } diff --git a/src/internal/connector/graph/betasdk/models/text_web_partable.go b/src/internal/connector/graph/betasdk/models/text_web_partable.go index 45e21d92b..f58b6a0c8 100644 --- a/src/internal/connector/graph/betasdk/models/text_web_partable.go +++ b/src/internal/connector/graph/betasdk/models/text_web_partable.go @@ -1,13 +1,13 @@ package models import ( - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" ) -// TextWebPartable +// TextWebPartable type TextWebPartable interface { - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable - WebPartable - GetInnerHtml()(*string) - SetInnerHtml(value *string)() + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable + WebPartable + GetInnerHtml() *string + SetInnerHtml(value *string) } diff --git a/src/internal/connector/graph/betasdk/models/title_area_layout_type.go b/src/internal/connector/graph/betasdk/models/title_area_layout_type.go index 375b68874..3621288a4 100644 --- a/src/internal/connector/graph/betasdk/models/title_area_layout_type.go +++ b/src/internal/connector/graph/betasdk/models/title_area_layout_type.go @@ -1,43 +1,45 @@ package models + import ( - "errors" + "errors" ) + // Provides operations to call the remove method. type TitleAreaLayoutType int const ( - IMAGEANDTITLE_TITLEAREALAYOUTTYPE TitleAreaLayoutType = iota - PLAIN_TITLEAREALAYOUTTYPE - COLORBLOCK_TITLEAREALAYOUTTYPE - OVERLAP_TITLEAREALAYOUTTYPE - UNKNOWNFUTUREVALUE_TITLEAREALAYOUTTYPE + IMAGEANDTITLE_TITLEAREALAYOUTTYPE TitleAreaLayoutType = iota + PLAIN_TITLEAREALAYOUTTYPE + COLORBLOCK_TITLEAREALAYOUTTYPE + OVERLAP_TITLEAREALAYOUTTYPE + UNKNOWNFUTUREVALUE_TITLEAREALAYOUTTYPE ) func (i TitleAreaLayoutType) String() string { - return []string{"imageAndTitle", "plain", "colorBlock", "overlap", "unknownFutureValue"}[i] + return []string{"imageAndTitle", "plain", "colorBlock", "overlap", "unknownFutureValue"}[i] } func ParseTitleAreaLayoutType(v string) (interface{}, error) { - result := IMAGEANDTITLE_TITLEAREALAYOUTTYPE - switch v { - case "imageAndTitle": - result = IMAGEANDTITLE_TITLEAREALAYOUTTYPE - case "plain": - result = PLAIN_TITLEAREALAYOUTTYPE - case "colorBlock": - result = COLORBLOCK_TITLEAREALAYOUTTYPE - case "overlap": - result = OVERLAP_TITLEAREALAYOUTTYPE - case "unknownFutureValue": - result = UNKNOWNFUTUREVALUE_TITLEAREALAYOUTTYPE - default: - return 0, errors.New("Unknown TitleAreaLayoutType value: " + v) - } - return &result, nil + result := IMAGEANDTITLE_TITLEAREALAYOUTTYPE + switch v { + case "imageAndTitle": + result = IMAGEANDTITLE_TITLEAREALAYOUTTYPE + case "plain": + result = PLAIN_TITLEAREALAYOUTTYPE + case "colorBlock": + result = COLORBLOCK_TITLEAREALAYOUTTYPE + case "overlap": + result = OVERLAP_TITLEAREALAYOUTTYPE + case "unknownFutureValue": + result = UNKNOWNFUTUREVALUE_TITLEAREALAYOUTTYPE + default: + return 0, errors.New("Unknown TitleAreaLayoutType value: " + v) + } + return &result, nil } func SerializeTitleAreaLayoutType(values []TitleAreaLayoutType) []string { - result := make([]string, len(values)) - for i, v := range values { - result[i] = v.String() - } - return result + result := make([]string, len(values)) + for i, v := range values { + result[i] = v.String() + } + return result } diff --git a/src/internal/connector/graph/betasdk/models/title_area_text_alignment_type.go b/src/internal/connector/graph/betasdk/models/title_area_text_alignment_type.go index 27b1e1dba..a34f41dbe 100644 --- a/src/internal/connector/graph/betasdk/models/title_area_text_alignment_type.go +++ b/src/internal/connector/graph/betasdk/models/title_area_text_alignment_type.go @@ -1,37 +1,39 @@ package models + import ( - "errors" + "errors" ) + // Provides operations to call the remove method. type TitleAreaTextAlignmentType int const ( - LEFT_TITLEAREATEXTALIGNMENTTYPE TitleAreaTextAlignmentType = iota - CENTER_TITLEAREATEXTALIGNMENTTYPE - UNKNOWNFUTUREVALUE_TITLEAREATEXTALIGNMENTTYPE + LEFT_TITLEAREATEXTALIGNMENTTYPE TitleAreaTextAlignmentType = iota + CENTER_TITLEAREATEXTALIGNMENTTYPE + UNKNOWNFUTUREVALUE_TITLEAREATEXTALIGNMENTTYPE ) func (i TitleAreaTextAlignmentType) String() string { - return []string{"left", "center", "unknownFutureValue"}[i] + return []string{"left", "center", "unknownFutureValue"}[i] } func ParseTitleAreaTextAlignmentType(v string) (interface{}, error) { - result := LEFT_TITLEAREATEXTALIGNMENTTYPE - switch v { - case "left": - result = LEFT_TITLEAREATEXTALIGNMENTTYPE - case "center": - result = CENTER_TITLEAREATEXTALIGNMENTTYPE - case "unknownFutureValue": - result = UNKNOWNFUTUREVALUE_TITLEAREATEXTALIGNMENTTYPE - default: - return 0, errors.New("Unknown TitleAreaTextAlignmentType value: " + v) - } - return &result, nil + result := LEFT_TITLEAREATEXTALIGNMENTTYPE + switch v { + case "left": + result = LEFT_TITLEAREATEXTALIGNMENTTYPE + case "center": + result = CENTER_TITLEAREATEXTALIGNMENTTYPE + case "unknownFutureValue": + result = UNKNOWNFUTUREVALUE_TITLEAREATEXTALIGNMENTTYPE + default: + return 0, errors.New("Unknown TitleAreaTextAlignmentType value: " + v) + } + return &result, nil } func SerializeTitleAreaTextAlignmentType(values []TitleAreaTextAlignmentType) []string { - result := make([]string, len(values)) - for i, v := range values { - result[i] = v.String() - } - return result + result := make([]string, len(values)) + for i, v := range values { + result[i] = v.String() + } + return result } diff --git a/src/internal/connector/graph/betasdk/models/web_part_position.go b/src/internal/connector/graph/betasdk/models/web_part_position.go index f2f1c3c9e..f3be0e651 100644 --- a/src/internal/connector/graph/betasdk/models/web_part_position.go +++ b/src/internal/connector/graph/betasdk/models/web_part_position.go @@ -1,175 +1,190 @@ package models import ( - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" ) -// WebPartPosition +// WebPartPosition type WebPartPosition struct { - // Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. - additionalData map[string]interface{} - // Indicates the identifier of the column where the web part is located. - columnId *float64 - // Indicates the horizontal section where the web part is located. - horizontalSectionId *float64 - // Indicates whether the web part is located in the vertical section. - isInVerticalSection *bool - // The OdataType property - odataType *string - // Index of the current web part. Represents the order of the web part in this column or section. - webPartIndex *float64 + // Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + additionalData map[string]interface{} + // Indicates the identifier of the column where the web part is located. + columnId *float64 + // Indicates the horizontal section where the web part is located. + horizontalSectionId *float64 + // Indicates whether the web part is located in the vertical section. + isInVerticalSection *bool + // The OdataType property + odataType *string + // Index of the current web part. Represents the order of the web part in this column or section. + webPartIndex *float64 } + // NewWebPartPosition instantiates a new webPartPosition and sets the default values. -func NewWebPartPosition()(*WebPartPosition) { - m := &WebPartPosition{ - } - m.SetAdditionalData(make(map[string]interface{})); - return m +func NewWebPartPosition() *WebPartPosition { + m := &WebPartPosition{} + m.SetAdditionalData(make(map[string]interface{})) + return m } + // CreateWebPartPositionFromDiscriminatorValue creates a new instance of the appropriate class based on discriminator value -func CreateWebPartPositionFromDiscriminatorValue(parseNode i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable, error) { - return NewWebPartPosition(), nil +func CreateWebPartPositionFromDiscriminatorValue(parseNode i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) (i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable, error) { + return NewWebPartPosition(), nil } + // GetAdditionalData gets the additionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. -func (m *WebPartPosition) GetAdditionalData()(map[string]interface{}) { - return m.additionalData +func (m *WebPartPosition) GetAdditionalData() map[string]interface{} { + return m.additionalData } + // GetColumnId gets the columnId property value. Indicates the identifier of the column where the web part is located. -func (m *WebPartPosition) GetColumnId()(*float64) { - return m.columnId +func (m *WebPartPosition) GetColumnId() *float64 { + return m.columnId } + // GetFieldDeserializers the deserialization information for the current model -func (m *WebPartPosition) GetFieldDeserializers()(map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(error)) { - res := make(map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode)(error)) - res["columnId"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetFloat64Value() - if err != nil { - return err - } - if val != nil { - m.SetColumnId(val) - } - return nil - } - res["horizontalSectionId"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetFloat64Value() - if err != nil { - return err - } - if val != nil { - m.SetHorizontalSectionId(val) - } - return nil - } - res["isInVerticalSection"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetBoolValue() - if err != nil { - return err - } - if val != nil { - m.SetIsInVerticalSection(val) - } - return nil - } - res["@odata.type"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetStringValue() - if err != nil { - return err - } - if val != nil { - m.SetOdataType(val) - } - return nil - } - res["webPartIndex"] = func (n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetFloat64Value() - if err != nil { - return err - } - if val != nil { - m.SetWebPartIndex(val) - } - return nil - } - return res +func (m *WebPartPosition) GetFieldDeserializers() map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + res := make(map[string]func(i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error) + res["columnId"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetFloat64Value() + if err != nil { + return err + } + if val != nil { + m.SetColumnId(val) + } + return nil + } + res["horizontalSectionId"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetFloat64Value() + if err != nil { + return err + } + if val != nil { + m.SetHorizontalSectionId(val) + } + return nil + } + res["isInVerticalSection"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetBoolValue() + if err != nil { + return err + } + if val != nil { + m.SetIsInVerticalSection(val) + } + return nil + } + res["@odata.type"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetStringValue() + if err != nil { + return err + } + if val != nil { + m.SetOdataType(val) + } + return nil + } + res["webPartIndex"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + val, err := n.GetFloat64Value() + if err != nil { + return err + } + if val != nil { + m.SetWebPartIndex(val) + } + return nil + } + return res } + // GetHorizontalSectionId gets the horizontalSectionId property value. Indicates the horizontal section where the web part is located. -func (m *WebPartPosition) GetHorizontalSectionId()(*float64) { - return m.horizontalSectionId +func (m *WebPartPosition) GetHorizontalSectionId() *float64 { + return m.horizontalSectionId } + // GetIsInVerticalSection gets the isInVerticalSection property value. Indicates whether the web part is located in the vertical section. -func (m *WebPartPosition) GetIsInVerticalSection()(*bool) { - return m.isInVerticalSection +func (m *WebPartPosition) GetIsInVerticalSection() *bool { + return m.isInVerticalSection } + // GetOdataType gets the @odata.type property value. The OdataType property -func (m *WebPartPosition) GetOdataType()(*string) { - return m.odataType +func (m *WebPartPosition) GetOdataType() *string { + return m.odataType } + // GetWebPartIndex gets the webPartIndex property value. Index of the current web part. Represents the order of the web part in this column or section. -func (m *WebPartPosition) GetWebPartIndex()(*float64) { - return m.webPartIndex +func (m *WebPartPosition) GetWebPartIndex() *float64 { + return m.webPartIndex } + // Serialize serializes information the current object -func (m *WebPartPosition) Serialize(writer i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.SerializationWriter)(error) { - { - err := writer.WriteFloat64Value("columnId", m.GetColumnId()) - if err != nil { - return err - } - } - { - err := writer.WriteFloat64Value("horizontalSectionId", m.GetHorizontalSectionId()) - if err != nil { - return err - } - } - { - err := writer.WriteBoolValue("isInVerticalSection", m.GetIsInVerticalSection()) - if err != nil { - return err - } - } - { - err := writer.WriteStringValue("@odata.type", m.GetOdataType()) - if err != nil { - return err - } - } - { - err := writer.WriteFloat64Value("webPartIndex", m.GetWebPartIndex()) - if err != nil { - return err - } - } - { - err := writer.WriteAdditionalData(m.GetAdditionalData()) - if err != nil { - return err - } - } - return nil +func (m *WebPartPosition) Serialize(writer i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.SerializationWriter) error { + { + err := writer.WriteFloat64Value("columnId", m.GetColumnId()) + if err != nil { + return err + } + } + { + err := writer.WriteFloat64Value("horizontalSectionId", m.GetHorizontalSectionId()) + if err != nil { + return err + } + } + { + err := writer.WriteBoolValue("isInVerticalSection", m.GetIsInVerticalSection()) + if err != nil { + return err + } + } + { + err := writer.WriteStringValue("@odata.type", m.GetOdataType()) + if err != nil { + return err + } + } + { + err := writer.WriteFloat64Value("webPartIndex", m.GetWebPartIndex()) + if err != nil { + return err + } + } + { + err := writer.WriteAdditionalData(m.GetAdditionalData()) + if err != nil { + return err + } + } + return nil } + // SetAdditionalData sets the additionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. -func (m *WebPartPosition) SetAdditionalData(value map[string]interface{})() { - m.additionalData = value +func (m *WebPartPosition) SetAdditionalData(value map[string]interface{}) { + m.additionalData = value } + // SetColumnId sets the columnId property value. Indicates the identifier of the column where the web part is located. -func (m *WebPartPosition) SetColumnId(value *float64)() { - m.columnId = value +func (m *WebPartPosition) SetColumnId(value *float64) { + m.columnId = value } + // SetHorizontalSectionId sets the horizontalSectionId property value. Indicates the horizontal section where the web part is located. -func (m *WebPartPosition) SetHorizontalSectionId(value *float64)() { - m.horizontalSectionId = value +func (m *WebPartPosition) SetHorizontalSectionId(value *float64) { + m.horizontalSectionId = value } + // SetIsInVerticalSection sets the isInVerticalSection property value. Indicates whether the web part is located in the vertical section. -func (m *WebPartPosition) SetIsInVerticalSection(value *bool)() { - m.isInVerticalSection = value +func (m *WebPartPosition) SetIsInVerticalSection(value *bool) { + m.isInVerticalSection = value } + // SetOdataType sets the @odata.type property value. The OdataType property -func (m *WebPartPosition) SetOdataType(value *string)() { - m.odataType = value +func (m *WebPartPosition) SetOdataType(value *string) { + m.odataType = value } + // SetWebPartIndex sets the webPartIndex property value. Index of the current web part. Represents the order of the web part in this column or section. -func (m *WebPartPosition) SetWebPartIndex(value *float64)() { - m.webPartIndex = value +func (m *WebPartPosition) SetWebPartIndex(value *float64) { + m.webPartIndex = value } diff --git a/src/internal/connector/graph/betasdk/models/web_part_positionable.go b/src/internal/connector/graph/betasdk/models/web_part_positionable.go index f0939db2e..9655ac285 100644 --- a/src/internal/connector/graph/betasdk/models/web_part_positionable.go +++ b/src/internal/connector/graph/betasdk/models/web_part_positionable.go @@ -1,21 +1,21 @@ package models import ( - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" ) -// WebPartPositionable +// WebPartPositionable type WebPartPositionable interface { - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.AdditionalDataHolder - i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable - GetColumnId()(*float64) - GetHorizontalSectionId()(*float64) - GetIsInVerticalSection()(*bool) - GetOdataType()(*string) - GetWebPartIndex()(*float64) - SetColumnId(value *float64)() - SetHorizontalSectionId(value *float64)() - SetIsInVerticalSection(value *bool)() - SetOdataType(value *string)() - SetWebPartIndex(value *float64)() + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.AdditionalDataHolder + i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable + GetColumnId() *float64 + GetHorizontalSectionId() *float64 + GetIsInVerticalSection() *bool + GetOdataType() *string + GetWebPartIndex() *float64 + SetColumnId(value *float64) + SetHorizontalSectionId(value *float64) + SetIsInVerticalSection(value *bool) + SetOdataType(value *string) + SetWebPartIndex(value *float64) } From 4ac81e6253469060bd2ba3e93e0fb76cb779ab44 Mon Sep 17 00:00:00 2001 From: Keepers Date: Tue, 31 Jan 2023 18:47:49 -0700 Subject: [PATCH 06/40] print location of log file (#2343) ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :broom: Tech Debt/Cleanup ## Issue(s) * #2329 ## Test Plan - [x] :muscle: Manual --- src/pkg/logger/logger.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pkg/logger/logger.go b/src/pkg/logger/logger.go index 0d5ffe250..b76b46070 100644 --- a/src/pkg/logger/logger.go +++ b/src/pkg/logger/logger.go @@ -7,6 +7,7 @@ import ( "time" "github.com/alcionai/clues" + "github.com/alcionai/corso/src/cli/print" "github.com/spf13/cobra" "github.com/spf13/pflag" "go.uber.org/zap" @@ -118,6 +119,7 @@ func PreloadLoggingFlags() (string, string) { if logfile != "stdout" && logfile != "stderr" { logdir := filepath.Dir(logfile) + print.Info(context.Background(), "Logging to file: "+logfile) err := os.MkdirAll(logdir, 0o755) if err != nil { From ec7d3c6fc5568015c9f57e32b13a06c8656b5b1e Mon Sep 17 00:00:00 2001 From: Keepers Date: Tue, 31 Jan 2023 19:19:44 -0700 Subject: [PATCH 07/40] add logging around operation completion (#2337) ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :broom: Tech Debt/Cleanup ## Issue(s) * #2329 ## Test Plan - [x] :muscle: Manual --- src/cli/cli.go | 4 ++ src/go.mod | 2 +- src/go.sum | 4 +- src/internal/operations/backup.go | 85 +++++++++++++++------------- src/internal/operations/manifests.go | 12 ++-- src/internal/operations/restore.go | 40 ++++++++----- src/pkg/logger/logger.go | 2 +- src/pkg/repository/repository.go | 25 ++++---- 8 files changed, 103 insertions(+), 71 deletions(-) diff --git a/src/cli/cli.go b/src/cli/cli.go index b67663d06..f202e4953 100644 --- a/src/cli/cli.go +++ b/src/cli/cli.go @@ -6,6 +6,7 @@ import ( "regexp" "strings" + "github.com/alcionai/clues" "github.com/spf13/cobra" "github.com/alcionai/corso/src/cli/backup" @@ -121,6 +122,9 @@ func Handle() { }() if err := corsoCmd.ExecuteContext(ctx); err != nil { + logger.Ctx(ctx). + With("err", err). + Errorw("cli execution", clues.InErr(err).Slice()...) os.Exit(1) } } diff --git a/src/go.mod b/src/go.mod index a8054ab0e..0b7fd24ee 100644 --- a/src/go.mod +++ b/src/go.mod @@ -4,7 +4,7 @@ go 1.19 require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 - github.com/alcionai/clues v0.0.0-20230120231953-1cf61dbafc40 + github.com/alcionai/clues v0.0.0-20230131232239-cee86233b005 github.com/aws/aws-sdk-go v1.44.190 github.com/aws/aws-xray-sdk-go v1.8.0 github.com/google/uuid v1.3.0 diff --git a/src/go.sum b/src/go.sum index 72af64b2d..aa032076f 100644 --- a/src/go.sum +++ b/src/go.sum @@ -52,8 +52,8 @@ github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1o github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/alcionai/clues v0.0.0-20230120231953-1cf61dbafc40 h1:bvAwz0dcJeIyRjudVyzmmawOvc4SqlSerKd0B4dh0yw= -github.com/alcionai/clues v0.0.0-20230120231953-1cf61dbafc40/go.mod h1:UlAs8jkWIpsOMakiC8NxPgQQVQRdvyf1hYMszlYYLb4= +github.com/alcionai/clues v0.0.0-20230131232239-cee86233b005 h1:eTgICcmcydEWG8J+hgnidf0pzujV3Gd2XqmknykZkzA= +github.com/alcionai/clues v0.0.0-20230131232239-cee86233b005/go.mod h1:UlAs8jkWIpsOMakiC8NxPgQQVQRdvyf1hYMszlYYLb4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= diff --git a/src/internal/operations/backup.go b/src/internal/operations/backup.go index 51e92181c..8e40d820f 100644 --- a/src/internal/operations/backup.go +++ b/src/internal/operations/backup.go @@ -40,6 +40,9 @@ type BackupOperation struct { Version string `json:"version"` account account.Account + + // when true, this allows for incremental backups instead of full data pulls + incremental bool } // BackupResults aggregate the details of the result of the operation. @@ -66,6 +69,7 @@ func NewBackupOperation( Selectors: selector, Version: "v0", account: acct, + incremental: useIncrementalBackup(selector, opts), } if err := op.validate(); err != nil { return BackupOperation{}, err @@ -102,10 +106,36 @@ type detailsWriter interface { // --------------------------------------------------------------------------- // Run begins a synchronous backup operation. -func (op *BackupOperation) Run(ctx context.Context) (err error) { +func (op *BackupOperation) Run(ctx context.Context) error { ctx, end := D.Span(ctx, "operations:backup:run") - defer end() + defer func() { + end() + // wait for the progress display to clean up + observe.Complete() + }() + ctx = clues.AddAll( + ctx, + "tenant_id", op.account.ID(), // TODO: pii + "resource_owner", op.ResourceOwner, // TODO: pii + "backup_id", op.Results.BackupID, + "service", op.Selectors.Service, + "incremental", op.incremental) + + if err := op.do(ctx); err != nil { + logger.Ctx(ctx). + With("err", err). + Errorw("backup operation", clues.InErr(err).Slice()...) + + return err + } + + logger.Ctx(ctx).Infow("completed backup", "results", op.Results) + + return nil +} + +func (op *BackupOperation) do(ctx context.Context) (err error) { var ( opStats backupStats backupDetails *details.Builder @@ -114,19 +144,10 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) { startTime = time.Now() detailsStore = streamstore.New(op.kopia, tenantID, op.Selectors.PathService()) reasons = selectorToReasons(op.Selectors) - uib = useIncrementalBackup(op.Selectors, op.Options) ) op.Results.BackupID = model.StableID(uuid.NewString()) - ctx = clues.AddAll( - ctx, - "tenant_id", tenantID, // TODO: pii - "resource_owner", op.ResourceOwner, // TODO: pii - "backup_id", op.Results.BackupID, - "service", op.Selectors.Service, - "incremental", uib) - op.bus.Event( ctx, events.BackupStart, @@ -139,9 +160,6 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) { // persist operation results to the model store on exit defer func() { - // wait for the progress display to clean up - observe.Complete() - err = op.persistResults(startTime, &opStats) if err != nil { op.Errors.Fail(errors.Wrap(err, "persisting backup results")) @@ -165,7 +183,7 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) { op.store, reasons, tenantID, - uib, + op.incremental, op.Errors) if err != nil { op.Errors.Fail(errors.Wrap(err, "collecting manifest heuristics")) @@ -190,7 +208,7 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) { return opStats.readErr } - ctx = clues.Add(ctx, "collections", len(cs)) + ctx = clues.Add(ctx, "coll_count", len(cs)) opStats.k, backupDetails, toMerge, err = consumeBackupDataCollections( ctx, @@ -200,7 +218,7 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) { mans, cs, op.Results.BackupID, - uib && canUseMetaData) + op.incremental && canUseMetaData) if err != nil { op.Errors.Fail(errors.Wrap(err, "backing up service data")) opStats.writeErr = op.Errors.Err() @@ -208,11 +226,6 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) { return opStats.writeErr } - logger.Ctx(ctx).Debugf( - "Backed up %d directories and %d files", - opStats.k.TotalDirectoryCount, opStats.k.TotalFileCount, - ) - if err = mergeDetails( ctx, op.store, @@ -334,7 +347,7 @@ func builderFromReason(ctx context.Context, tenant string, r kopia.Reason) (*pat false, ) if err != nil { - return nil, clues.Wrap(err, "building path").WithMap(clues.Values(ctx)) + return nil, clues.Wrap(err, "building path").WithClues(ctx) } return p.ToBuilder().Dir(), nil @@ -404,13 +417,9 @@ func consumeBackupDataCollections( logger.Ctx(ctx).Infow( "using base for backup", - "snapshot_id", - m.ID, - "services", - svcs, - "categories", - cats, - ) + "snapshot_id", m.ID, + "services", svcs, + "categories", cats) } kopiaStats, deets, itemsSourcedFromBase, err := bu.BackupCollections( @@ -481,7 +490,7 @@ func mergeDetails( bID, ok := man.GetTag(kopia.TagBackupID) if !ok { - return clues.New("no backup ID in snapshot manifest").WithMap(clues.Values(mctx)) + return clues.New("no backup ID in snapshot manifest").WithClues(mctx) } mctx = clues.Add(mctx, "manifest_backup_id", bID) @@ -493,14 +502,14 @@ func mergeDetails( detailsStore, ) if err != nil { - return clues.New("fetching base details for backup").WithMap(clues.Values(mctx)) + return clues.New("fetching base details for backup").WithClues(mctx) } for _, entry := range baseDeets.Items() { rr, err := path.FromDataLayerPath(entry.RepoRef, true) if err != nil { return clues.New("parsing base item info path"). - WithMap(clues.Values(mctx)). + WithClues(mctx). With("repo_ref", entry.RepoRef) // todo: pii } @@ -524,7 +533,7 @@ func mergeDetails( // Fixup paths in the item. item := entry.ItemInfo if err := details.UpdateItem(&item, newPath); err != nil { - return clues.New("updating item details").WithMap(clues.Values(mctx)) + return clues.New("updating item details").WithClues(mctx) } // TODO(ashmrtn): This may need updated if we start using this merge @@ -550,7 +559,7 @@ func mergeDetails( if addedEntries != len(shortRefsFromPrevBackup) { return clues.New("incomplete migration of backup details"). - WithMap(clues.Values(ctx)). + WithClues(ctx). WithAll("item_count", addedEntries, "expected_item_count", len(shortRefsFromPrevBackup)) } @@ -603,12 +612,12 @@ func (op *BackupOperation) createBackupModels( ctx = clues.Add(ctx, "snapshot_id", snapID) if backupDetails == nil { - return clues.New("no backup details to record").WithMap(clues.Values(ctx)) + return clues.New("no backup details to record").WithClues(ctx) } detailsID, err := detailsStore.WriteBackupDetails(ctx, backupDetails) if err != nil { - return clues.Wrap(err, "creating backupDetails model").WithMap(clues.Values(ctx)) + return clues.Wrap(err, "creating backupDetails model").WithClues(ctx) } ctx = clues.Add(ctx, "details_id", detailsID) @@ -622,7 +631,7 @@ func (op *BackupOperation) createBackupModels( ) if err = op.store.Put(ctx, model.BackupSchema, b); err != nil { - return clues.Wrap(err, "creating backup model").WithMap(clues.Values(ctx)) + return clues.Wrap(err, "creating backup model").WithClues(ctx) } dur := op.Results.CompletedAt.Sub(op.Results.StartedAt) diff --git a/src/internal/operations/manifests.go b/src/internal/operations/manifests.go index 9c765fe70..c0ba35e43 100644 --- a/src/internal/operations/manifests.go +++ b/src/internal/operations/manifests.go @@ -73,7 +73,7 @@ func produceManifestsAndMetadata( if err := verifyDistinctBases(ctx, ms, errs); err != nil { logger.Ctx(ctx).With("error", err).Infow( "base snapshot collision, falling back to full backup", - clues.Slice(ctx)...) + clues.In(ctx).Slice()...) return ms, nil, false, nil } @@ -87,7 +87,7 @@ func produceManifestsAndMetadata( bID, ok := man.GetTag(kopia.TagBackupID) if !ok { - err = clues.New("snapshot manifest missing backup ID").WithMap(clues.Values(mctx)) + err = clues.New("snapshot manifest missing backup ID").WithClues(ctx) return nil, nil, false, err } @@ -98,7 +98,7 @@ func produceManifestsAndMetadata( // if no backup exists for any of the complete manifests, we want // to fall back to a complete backup. if errors.Is(err, kopia.ErrNotFound) { - logger.Ctx(ctx).Infow("backup missing, falling back to full backup", clues.Slice(mctx)...) + logger.Ctx(ctx).Infow("backup missing, falling back to full backup", clues.In(mctx).Slice()...) return ms, nil, false, nil } @@ -113,7 +113,7 @@ func produceManifestsAndMetadata( // This makes an assumption that the ID points to a populated set of // details; we aren't doing the work to look them up. if len(dID) == 0 { - logger.Ctx(ctx).Infow("backup missing details ID, falling back to full backup", clues.Slice(mctx)...) + logger.Ctx(ctx).Infow("backup missing details ID, falling back to full backup", clues.In(mctx).Slice()...) return ms, nil, false, nil } @@ -159,7 +159,7 @@ func verifyDistinctBases(ctx context.Context, mans []*kopia.ManifestEntry, errs failed = true errs.Add(clues.New("manifests have overlapping reasons"). - WithMap(clues.Values(ctx)). + WithClues(ctx). With("other_manifest_id", b)) continue @@ -170,7 +170,7 @@ func verifyDistinctBases(ctx context.Context, mans []*kopia.ManifestEntry, errs } if failed { - return clues.New("multiple base snapshots qualify").WithMap(clues.Values(ctx)) + return clues.New("multiple base snapshots qualify").WithClues(ctx) } return nil diff --git a/src/internal/operations/restore.go b/src/internal/operations/restore.go index db5ca9a93..30bc303a9 100644 --- a/src/internal/operations/restore.go +++ b/src/internal/operations/restore.go @@ -107,8 +107,33 @@ type restorer interface { // Run begins a synchronous restore operation. func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.Details, err error) { ctx, end := D.Span(ctx, "operations:restore:run") - defer end() + defer func() { + end() + // wait for the progress display to clean up + observe.Complete() + }() + ctx = clues.AddAll( + ctx, + "tenant_id", op.account.ID(), // TODO: pii + "backup_id", op.BackupID, + "service", op.Selectors.Service) + + deets, err := op.do(ctx) + if err != nil { + logger.Ctx(ctx). + With("err", err). + Errorw("restore operation", clues.InErr(err).Slice()...) + + return nil, err + } + + logger.Ctx(ctx).Infow("completed restore", "results", op.Results) + + return deets, nil +} + +func (op *RestoreOperation) do(ctx context.Context) (restoreDetails *details.Details, err error) { var ( opStats = restoreStats{ bytesRead: &stats.ByteCounter{}, @@ -118,9 +143,6 @@ func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.De ) defer func() { - // wait for the progress display to clean up - observe.Complete() - err = op.persistResults(ctx, startTime, &opStats) if err != nil { return @@ -129,12 +151,6 @@ func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.De detailsStore := streamstore.New(op.kopia, op.account.ID(), op.Selectors.PathService()) - ctx = clues.AddAll( - ctx, - "tenant_id", op.account.ID(), // TODO: pii - "backup_id", op.BackupID, - "service", op.Selectors.Service) - bup, deets, err := getBackupAndDetailsFromID( ctx, op.BackupID, @@ -166,7 +182,6 @@ func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.De } ctx = clues.Add(ctx, "details_paths", len(paths)) - observe.Message(ctx, observe.Safe(fmt.Sprintf("Discovered %d items in backup %s to restore", len(paths), op.BackupID))) kopiaComplete, closer := observe.MessageWithCompletion(ctx, observe.Safe("Enumerating items in repository")) @@ -180,8 +195,7 @@ func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.De } kopiaComplete <- struct{}{} - ctx = clues.Add(ctx, "collections", len(dcs)) - + ctx = clues.Add(ctx, "coll_count", len(dcs)) opStats.cs = dcs opStats.resourceCount = len(data.ResourceOwnerSet(dcs)) diff --git a/src/pkg/logger/logger.go b/src/pkg/logger/logger.go index b76b46070..923af44c8 100644 --- a/src/pkg/logger/logger.go +++ b/src/pkg/logger/logger.go @@ -267,7 +267,7 @@ func Ctx(ctx context.Context) *zap.SugaredLogger { return singleton(levelOf(llFlag), defaultLogLocation()) } - return l.(*zap.SugaredLogger).With(clues.Slice(ctx)...) + return l.(*zap.SugaredLogger).With(clues.In(ctx).Slice()...) } // transforms the llevel flag value to a logLevel enum diff --git a/src/pkg/repository/repository.go b/src/pkg/repository/repository.go index f8c8d3d49..087b193bc 100644 --- a/src/pkg/repository/repository.go +++ b/src/pkg/repository/repository.go @@ -2,11 +2,12 @@ package repository import ( "context" - "errors" "time" + "github.com/alcionai/clues" "github.com/google/uuid" "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" "github.com/alcionai/corso/src/internal/events" "github.com/alcionai/corso/src/internal/kopia" @@ -88,6 +89,8 @@ func Initialize( s storage.Storage, opts control.Options, ) (Repository, error) { + ctx = clues.AddAll(ctx, "acct_provider", acct.Provider, "storage_provider", s.Provider) + kopiaRef := kopia.NewConn(s) if err := kopiaRef.Initialize(ctx); err != nil { // replace common internal errors so that sdk users can check results with errors.Is() @@ -95,7 +98,7 @@ func Initialize( return nil, ErrorRepoAlreadyExists } - return nil, err + return nil, errors.Wrap(err, "initializing kopia") } // kopiaRef comes with a count of 1 and NewWrapper/NewModelStore bumps it again so safe // to close here. @@ -103,17 +106,17 @@ func Initialize( w, err := kopia.NewWrapper(kopiaRef) if err != nil { - return nil, err + return nil, clues.Stack(err).WithClues(ctx) } ms, err := kopia.NewModelStore(kopiaRef) if err != nil { - return nil, err + return nil, clues.Stack(err).WithClues(ctx) } bus, err := events.NewBus(ctx, s, acct.ID(), opts) if err != nil { - return nil, err + return nil, errors.Wrap(err, "constructing event bus") } repoID := newRepoID(s) @@ -131,7 +134,7 @@ func Initialize( } if err := newRepoModel(ctx, ms, r.ID); err != nil { - return nil, errors.New("setting up repository") + return nil, clues.New("setting up repository").WithClues(ctx) } r.Bus.Event(ctx, events.RepoInit, nil) @@ -150,6 +153,8 @@ func Connect( s storage.Storage, opts control.Options, ) (Repository, error) { + ctx = clues.AddAll(ctx, "acct_provider", acct.Provider, "storage_provider", s.Provider) + // Close/Reset the progress bar. This ensures callers don't have to worry about // their output getting clobbered (#1720) defer observe.Complete() @@ -160,7 +165,7 @@ func Connect( kopiaRef := kopia.NewConn(s) if err := kopiaRef.Connect(ctx); err != nil { - return nil, err + return nil, errors.Wrap(err, "connecting kopia client") } // kopiaRef comes with a count of 1 and NewWrapper/NewModelStore bumps it again so safe // to close here. @@ -168,17 +173,17 @@ func Connect( w, err := kopia.NewWrapper(kopiaRef) if err != nil { - return nil, err + return nil, clues.Stack(err).WithClues(ctx) } ms, err := kopia.NewModelStore(kopiaRef) if err != nil { - return nil, err + return nil, clues.Stack(err).WithClues(ctx) } bus, err := events.NewBus(ctx, s, acct.ID(), opts) if err != nil { - return nil, err + return nil, errors.Wrap(err, "constructing event bus") } rm, err := getRepoModel(ctx, ms) From 7ee1575dc2fa96cb3ac2d11b756bed34325d88e8 Mon Sep 17 00:00:00 2001 From: Keepers Date: Tue, 31 Jan 2023 19:41:06 -0700 Subject: [PATCH 08/40] log end-of-observe even when hidden (#2339) ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :broom: Tech Debt/Cleanup ## Issue(s) * #2329 ## Test Plan - [x] :muscle: Manual --- src/internal/observe/observe.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/internal/observe/observe.go b/src/internal/observe/observe.go index d8492109d..2b86066f7 100644 --- a/src/internal/observe/observe.go +++ b/src/internal/observe/observe.go @@ -177,7 +177,7 @@ func MessageWithCompletion( completionCh := make(chan struct{}, 1) if cfg.hidden() { - return completionCh, func() {} + return completionCh, func() { log.Info("done - " + clean) } } wg.Add(1) @@ -232,7 +232,7 @@ func ItemProgress( log.Debug(header) if cfg.hidden() || rc == nil || totalBytes == 0 { - return rc, func() {} + return rc, func() { log.Debug("done - " + header) } } wg.Add(1) @@ -286,7 +286,7 @@ func ProgressWithCount( } }(progressCh) - return progressCh, func() {} + return progressCh, func() { log.Info("done - " + lmsg) } } wg.Add(1) @@ -381,16 +381,19 @@ func CollectionProgress( if cfg.hidden() || len(user.String()) == 0 || len(dirName.String()) == 0 { ch := make(chan struct{}) + counted := 0 + go func(ci <-chan struct{}) { for { _, ok := <-ci if !ok { return } + counted++ } }(ch) - return ch, func() {} + return ch, func() { log.Infow("done - "+message, "count", counted) } } wg.Add(1) From 4ab504056900ed709041736aead9c69356ce52e8 Mon Sep 17 00:00:00 2001 From: Vaibhav Kamra Date: Tue, 31 Jan 2023 19:39:00 -0800 Subject: [PATCH 09/40] Add progress logging to delta queries (#2345) ## Description Adds logging to delta enumeration to log every 1000 items or so (each page is set to 200 items) ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * #2329 ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- src/internal/connector/exchange/api/contacts.go | 7 +++++++ src/internal/connector/exchange/api/events.go | 7 +++++++ src/internal/connector/exchange/api/mail.go | 7 +++++++ src/internal/connector/exchange/api/shared.go | 13 ++++++++++++- 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/internal/connector/exchange/api/contacts.go b/src/internal/connector/exchange/api/contacts.go index 33f05d2a3..185bf6e22 100644 --- a/src/internal/connector/exchange/api/contacts.go +++ b/src/internal/connector/exchange/api/contacts.go @@ -12,10 +12,12 @@ import ( "github.com/microsoftgraph/msgraph-sdk-go/users" "github.com/pkg/errors" + "github.com/alcionai/clues" "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph/api" "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/pkg/backup/details" + "github.com/alcionai/corso/src/pkg/selectors" ) // --------------------------------------------------------------------------- @@ -234,6 +236,11 @@ func (c Contacts) GetAddedAndRemovedItemIDs( resetDelta bool ) + ctx = clues.AddAll( + ctx, + "category", selectors.ExchangeContact, + "folder_id", directoryID) + options, err := optionsForContactFoldersItemDelta([]string{"parentFolderId"}) if err != nil { return nil, nil, DeltaUpdate{}, errors.Wrap(err, "getting query options") diff --git a/src/internal/connector/exchange/api/events.go b/src/internal/connector/exchange/api/events.go index 63545143d..962b2e576 100644 --- a/src/internal/connector/exchange/api/events.go +++ b/src/internal/connector/exchange/api/events.go @@ -12,6 +12,7 @@ import ( "github.com/microsoftgraph/msgraph-sdk-go/users" "github.com/pkg/errors" + "github.com/alcionai/clues" "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph/api" @@ -19,6 +20,7 @@ import ( "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/path" + "github.com/alcionai/corso/src/pkg/selectors" ) // --------------------------------------------------------------------------- @@ -271,6 +273,11 @@ func (c Events) GetAddedAndRemovedItemIDs( errs *multierror.Error ) + ctx = clues.AddAll( + ctx, + "category", selectors.ExchangeEvent, + "calendar_id", calendarID) + if len(oldDelta) > 0 { builder := users.NewItemCalendarsItemEventsDeltaRequestBuilder(oldDelta, service.Adapter()) pgr := &eventPager{service, builder, nil} diff --git a/src/internal/connector/exchange/api/mail.go b/src/internal/connector/exchange/api/mail.go index 597676874..29c03b82f 100644 --- a/src/internal/connector/exchange/api/mail.go +++ b/src/internal/connector/exchange/api/mail.go @@ -12,11 +12,13 @@ import ( "github.com/microsoftgraph/msgraph-sdk-go/users" "github.com/pkg/errors" + "github.com/alcionai/clues" "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph/api" "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/logger" + "github.com/alcionai/corso/src/pkg/selectors" ) // --------------------------------------------------------------------------- @@ -259,6 +261,11 @@ func (c Mail) GetAddedAndRemovedItemIDs( resetDelta bool ) + ctx = clues.AddAll( + ctx, + "category", selectors.ExchangeMail, + "folder_id", directoryID) + options, err := optionsForFolderMessagesDelta([]string{"isRead"}) if err != nil { return nil, nil, DeltaUpdate{}, errors.Wrap(err, "getting query options") diff --git a/src/internal/connector/exchange/api/shared.go b/src/internal/connector/exchange/api/shared.go index 9cb4d8ab0..e4d563e90 100644 --- a/src/internal/connector/exchange/api/shared.go +++ b/src/internal/connector/exchange/api/shared.go @@ -65,6 +65,9 @@ func getItemsAddedAndRemovedFromContainer( deltaURL string ) + itemCount := 0 + page := 0 + for { // get the next page of data, check for standard errors resp, err := pager.getPage(ctx) @@ -83,7 +86,13 @@ func getItemsAddedAndRemovedFromContainer( return nil, nil, "", err } - logger.Ctx(ctx).Infow("Got page", "items", len(items)) + itemCount += len(items) + page++ + + // Log every ~1000 items (the page size we use is 200) + if page%5 == 0 { + logger.Ctx(ctx).Infow("queried items", "count", itemCount) + } // iterate through the items in the page for _, item := range items { @@ -117,5 +126,7 @@ func getItemsAddedAndRemovedFromContainer( pager.setNext(nextLink) } + logger.Ctx(ctx).Infow("completed enumeration", "count", itemCount) + return addedIDs, removedIDs, deltaURL, nil } From 2873befe536358634a69c544d61528eda4734d06 Mon Sep 17 00:00:00 2001 From: Vaibhav Kamra Date: Tue, 31 Jan 2023 19:57:19 -0800 Subject: [PATCH 10/40] Log collection updates (#2346) ## Description Adds a log message to collection updates to indicate how many items we have streamed into the repository ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * #2329 ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- src/internal/observe/observe.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/internal/observe/observe.go b/src/internal/observe/observe.go index 2b86066f7..34da29331 100644 --- a/src/internal/observe/observe.go +++ b/src/internal/observe/observe.go @@ -390,6 +390,11 @@ func CollectionProgress( return } counted++ + + // Log every 1000 items that are processed + if counted%1000 == 0 { + log.Infow("uploading", "count", counted) + } } }(ch) @@ -435,6 +440,11 @@ func CollectionProgress( counted++ + // Log every 1000 items that are processed + if counted%1000 == 0 { + log.Infow("uploading", "count", counted) + } + bar.Increment() } } From 45eb71f1c2d6ce3dc7a4d5685040d26bedf398bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Feb 2023 05:18:49 +0000 Subject: [PATCH 11/40] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20sass=20from?= =?UTF-8?q?=201.57.1=20to=201.58.0=20in=20/website=20(#2350)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/package-lock.json | 14 +++++++------- website/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/website/package-lock.json b/website/package-lock.json index 11a0d15e1..100008906 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -24,7 +24,7 @@ "prism-react-renderer": "^1.3.5", "react": "^17.0.2", "react-dom": "^17.0.2", - "sass": "^1.57.1", + "sass": "^1.58.0", "tw-elements": "^1.0.0-alpha13", "wow.js": "^1.2.2" }, @@ -11861,9 +11861,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.57.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz", - "integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.58.0.tgz", + "integrity": "sha512-PiMJcP33DdKtZ/1jSjjqVIKihoDc6yWmYr9K/4r3fVVIEDAluD0q7XZiRKrNJcPK3qkLRF/79DND1H5q1LBjgg==", "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -22452,9 +22452,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sass": { - "version": "1.57.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz", - "integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.58.0.tgz", + "integrity": "sha512-PiMJcP33DdKtZ/1jSjjqVIKihoDc6yWmYr9K/4r3fVVIEDAluD0q7XZiRKrNJcPK3qkLRF/79DND1H5q1LBjgg==", "requires": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", diff --git a/website/package.json b/website/package.json index 5d5670674..08e24f415 100644 --- a/website/package.json +++ b/website/package.json @@ -30,7 +30,7 @@ "prism-react-renderer": "^1.3.5", "react": "^17.0.2", "react-dom": "^17.0.2", - "sass": "^1.57.1", + "sass": "^1.58.0", "tw-elements": "^1.0.0-alpha13", "wow.js": "^1.2.2" }, From d918001ee47951331f5018df8e3b4bdefe70d85e Mon Sep 17 00:00:00 2001 From: Niraj Tolia Date: Tue, 31 Jan 2023 21:32:17 -0800 Subject: [PATCH 12/40] Improve log file docs (#2347) ## Description Add some nuance to log file docs. ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * #2329 --- website/docs/setup/configuration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/docs/setup/configuration.md b/website/docs/setup/configuration.md index 85c99c6bb..d9255f6b7 100644 --- a/website/docs/setup/configuration.md +++ b/website/docs/setup/configuration.md @@ -129,7 +129,9 @@ directory within the container. ## Log Files +Corso generates a unique log file named with its timestamp for every invocation. The default location of Corso's log file is shown below but the location can be overridden by using the `--log-file` flag. +The log file will be appended to if multiple Corso invocations are pointed to the same file. You can also use `stdout` or `stderr` as the `--log-file` location to redirect the logs to "stdout" and "stderr" respectively. From 3fd3da7cafc15678ed5d42f2a1ac5f5135f7d500 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Feb 2023 16:33:53 +0000 Subject: [PATCH 13/40] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20github.com/aw?= =?UTF-8?q?s/aws-sdk-go=20from=201.44.190=20to=201.44.191=20in=20/src=20(#?= =?UTF-8?q?2351)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.44.190 to 1.44.191.
Release notes

Sourced from github.com/aws/aws-sdk-go's releases.

Release v1.44.191 (2023-01-31)

Service Client Updates

  • service/accessanalyzer: Adds new service
  • service/appsync: Updates service API and documentation
  • service/cloudtrail: Updates service API and documentation
    • Add new "Channel" APIs to enable users to manage channels used for CloudTrail Lake integrations, and "Resource Policy" APIs to enable users to manage the resource-based permissions policy attached to a channel.
  • service/cloudtrail-data: Adds new service
  • service/codeartifact: Updates service API and documentation
  • service/connectparticipant: Adds new service
  • service/ec2: Updates service API and documentation
    • This launch allows customers to associate up to 8 IP addresses to their NAT Gateways to increase the limit on concurrent connections to a single destination by eight times from 55K to 440K.
  • service/groundstation: Updates service API and documentation
  • service/iot: Updates service API and documentation
    • Added support for IoT Rules Engine Cloudwatch Logs action batch mode.
  • service/kinesis: Adds new service
    • Enabled FIPS endpoints for GovCloud (US) regions in SDK.
  • service/opensearch: Updates service API and documentation
  • service/outposts: Adds new service
  • service/polly: Updates service API
    • Amazon Polly adds two new neural American English voices - Ruth, Stephen
  • service/sagemaker: Updates service API and documentation
    • Amazon SageMaker Automatic Model Tuning now supports more completion criteria for Hyperparameter Optimization.
  • service/securityhub: Updates service API and documentation
  • service/support: Adds new service
    • This fixes incorrect endpoint construction when a customer is explicitly setting a region.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/aws/aws-sdk-go&package-manager=go_modules&previous-version=1.44.190&new-version=1.44.191)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- src/go.mod | 2 +- src/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/go.mod b/src/go.mod index 0b7fd24ee..f947ed45a 100644 --- a/src/go.mod +++ b/src/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 github.com/alcionai/clues v0.0.0-20230131232239-cee86233b005 - github.com/aws/aws-sdk-go v1.44.190 + github.com/aws/aws-sdk-go v1.44.191 github.com/aws/aws-xray-sdk-go v1.8.0 github.com/google/uuid v1.3.0 github.com/hashicorp/go-multierror v1.1.1 diff --git a/src/go.sum b/src/go.sum index aa032076f..02f39a5a5 100644 --- a/src/go.sum +++ b/src/go.sum @@ -62,8 +62,8 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5 github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/aws/aws-sdk-go v1.44.190 h1:QC+Pf/Ooj7Waf2obOPZbIQOqr00hy4h54j3ZK9mvHcc= -github.com/aws/aws-sdk-go v1.44.190/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.191 h1:GnbkalCx/AgobaorDMFCa248acmk+91+aHBQOk7ljzU= +github.com/aws/aws-sdk-go v1.44.191/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-xray-sdk-go v1.8.0 h1:0xncHZ588wB/geLjbM/esoW3FOEThWy2TJyb4VXfLFY= github.com/aws/aws-xray-sdk-go v1.8.0/go.mod h1:7LKe47H+j3evfvS1+q0wzpoaGXGrF3mUsfM+thqVO+A= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= From 1594a86c223d45ae410e1dbeec39993d0048450d Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Wed, 1 Feb 2023 09:03:23 -0800 Subject: [PATCH 14/40] OneDrive Items API for mocking (#2322) ## Description Create a pager for drive items that allows for better testing via mocking. Increased testing will come in later PRs ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [x] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * #2264 ## Test Plan - [ ] :muscle: Manual - [x] :zap: Unit test - [x] :green_heart: E2E --- src/internal/connector/onedrive/api/drive.go | 80 +++++++++++++---- .../connector/onedrive/collections.go | 14 ++- src/internal/connector/onedrive/drive.go | 88 +++++++++++-------- src/internal/connector/onedrive/item_test.go | 12 ++- 4 files changed, 137 insertions(+), 57 deletions(-) diff --git a/src/internal/connector/onedrive/api/drive.go b/src/internal/connector/onedrive/api/drive.go index 6dd7d46a1..fea6e53a7 100644 --- a/src/internal/connector/onedrive/api/drive.go +++ b/src/internal/connector/onedrive/api/drive.go @@ -3,6 +3,7 @@ package api import ( "context" + msdrives "github.com/microsoftgraph/msgraph-sdk-go/drives" "github.com/microsoftgraph/msgraph-sdk-go/models" mssites "github.com/microsoftgraph/msgraph-sdk-go/sites" msusers "github.com/microsoftgraph/msgraph-sdk-go/users" @@ -12,6 +13,65 @@ import ( "github.com/alcionai/corso/src/internal/connector/graph/api" ) +func getValues[T any](l api.PageLinker) ([]T, error) { + page, ok := l.(interface{ GetValue() []T }) + if !ok { + return nil, errors.Errorf( + "response of type [%T] does not comply with GetValue() interface", + l, + ) + } + + return page.GetValue(), nil +} + +// max we can do is 999 +const pageSize = int32(999) + +type driveItemPager struct { + gs graph.Servicer + builder *msdrives.ItemRootDeltaRequestBuilder + options *msdrives.ItemRootDeltaRequestBuilderGetRequestConfiguration +} + +func NewItemPager( + gs graph.Servicer, + driveID, link string, + fields []string, +) *driveItemPager { + pageCount := pageSize + requestConfig := &msdrives.ItemRootDeltaRequestBuilderGetRequestConfiguration{ + QueryParameters: &msdrives.ItemRootDeltaRequestBuilderGetQueryParameters{ + Top: &pageCount, + Select: fields, + }, + } + + res := &driveItemPager{ + gs: gs, + options: requestConfig, + builder: gs.Client().DrivesById(driveID).Root().Delta(), + } + + if len(link) > 0 { + res.builder = msdrives.NewItemRootDeltaRequestBuilder(link, gs.Adapter()) + } + + return res +} + +func (p *driveItemPager) GetPage(ctx context.Context) (api.DeltaPageLinker, error) { + return p.builder.Get(ctx, p.options) +} + +func (p *driveItemPager) SetNext(link string) { + p.builder = msdrives.NewItemRootDeltaRequestBuilder(link, p.gs.Adapter()) +} + +func (p *driveItemPager) ValuesIn(l api.DeltaPageLinker) ([]models.DriveItemable, error) { + return getValues[models.DriveItemable](l) +} + type userDrivePager struct { gs graph.Servicer builder *msusers.ItemDrivesRequestBuilder @@ -47,15 +107,7 @@ func (p *userDrivePager) SetNext(link string) { } func (p *userDrivePager) ValuesIn(l api.PageLinker) ([]models.Driveable, error) { - page, ok := l.(interface{ GetValue() []models.Driveable }) - if !ok { - return nil, errors.Errorf( - "response of type [%T] does not comply with GetValue() interface", - l, - ) - } - - return page.GetValue(), nil + return getValues[models.Driveable](l) } type siteDrivePager struct { @@ -93,13 +145,5 @@ func (p *siteDrivePager) SetNext(link string) { } func (p *siteDrivePager) ValuesIn(l api.PageLinker) ([]models.Driveable, error) { - page, ok := l.(interface{ GetValue() []models.Driveable }) - if !ok { - return nil, errors.Errorf( - "response of type [%T] does not comply with GetValue() interface", - l, - ) - } - - return page.GetValue(), nil + return getValues[models.Driveable](l) } diff --git a/src/internal/connector/onedrive/collections.go b/src/internal/connector/onedrive/collections.go index 35b81f7f2..1e36b1ea3 100644 --- a/src/internal/connector/onedrive/collections.go +++ b/src/internal/connector/onedrive/collections.go @@ -65,6 +65,13 @@ type Collections struct { // for a OneDrive folder CollectionMap map[string]data.Collection + // Not the most ideal, but allows us to change the pager function for testing + // as needed. This will allow us to mock out some scenarios during testing. + itemPagerFunc func( + servicer graph.Servicer, + driveID, link string, + ) itemPager + // Track stats from drive enumeration. Represents the items backed up. NumItems int NumFiles int @@ -88,6 +95,7 @@ func NewCollections( source: source, matcher: matcher, CollectionMap: map[string]data.Collection{}, + itemPagerFunc: defaultItemPager, service: service, statusUpdater: statusUpdater, ctrl: ctrlOpts, @@ -266,7 +274,11 @@ func (c *Collections) Get( delta, paths, excluded, err := collectItems( ctx, - c.service, + c.itemPagerFunc( + c.service, + driveID, + "", + ), driveID, driveName, c.UpdateCollections, diff --git a/src/internal/connector/onedrive/drive.go b/src/internal/connector/onedrive/drive.go index 6270ec08a..8e1578caf 100644 --- a/src/internal/connector/onedrive/drive.go +++ b/src/internal/connector/onedrive/drive.go @@ -7,7 +7,6 @@ import ( "time" msdrive "github.com/microsoftgraph/msgraph-sdk-go/drive" - msdrives "github.com/microsoftgraph/msgraph-sdk-go/drives" "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors" "github.com/pkg/errors" @@ -135,11 +134,42 @@ type itemCollector func( excluded map[string]struct{}, ) error +type itemPager interface { + GetPage(context.Context) (gapi.DeltaPageLinker, error) + SetNext(nextLink string) + ValuesIn(gapi.DeltaPageLinker) ([]models.DriveItemable, error) +} + +func defaultItemPager( + servicer graph.Servicer, + driveID, link string, +) itemPager { + return api.NewItemPager( + servicer, + driveID, + link, + []string{ + "content.downloadUrl", + "createdBy", + "createdDateTime", + "file", + "folder", + "id", + "lastModifiedDateTime", + "name", + "package", + "parentReference", + "root", + "size", + }, + ) +} + // collectItems will enumerate all items in the specified drive and hand them to the // provided `collector` method func collectItems( ctx context.Context, - service graph.Servicer, + pager itemPager, driveID, driveName string, collector itemCollector, ) (string, map[string]string, map[string]struct{}, error) { @@ -154,34 +184,8 @@ func collectItems( maps.Copy(newPaths, oldPaths) - // TODO: Specify a timestamp in the delta query - // https://docs.microsoft.com/en-us/graph/api/driveitem-delta? - // view=graph-rest-1.0&tabs=http#example-4-retrieving-delta-results-using-a-timestamp - builder := service.Client().DrivesById(driveID).Root().Delta() - pageCount := int32(999) // max we can do is 999 - requestFields := []string{ - "content.downloadUrl", - "createdBy", - "createdDateTime", - "file", - "folder", - "id", - "lastModifiedDateTime", - "name", - "package", - "parentReference", - "root", - "size", - } - requestConfig := &msdrives.ItemRootDeltaRequestBuilderGetRequestConfiguration{ - QueryParameters: &msdrives.ItemRootDeltaRequestBuilderGetQueryParameters{ - Top: &pageCount, - Select: requestFields, - }, - } - for { - r, err := builder.Get(ctx, requestConfig) + page, err := pager.GetPage(ctx) if err != nil { return "", nil, nil, errors.Wrapf( err, @@ -190,23 +194,29 @@ func collectItems( ) } - err = collector(ctx, driveID, driveName, r.GetValue(), oldPaths, newPaths, excluded) + vals, err := pager.ValuesIn(page) + if err != nil { + return "", nil, nil, errors.Wrap(err, "extracting items from response") + } + + err = collector(ctx, driveID, driveName, vals, oldPaths, newPaths, excluded) if err != nil { return "", nil, nil, err } - if r.GetOdataDeltaLink() != nil && len(*r.GetOdataDeltaLink()) > 0 { - newDeltaURL = *r.GetOdataDeltaLink() + nextLink, deltaLink := gapi.NextAndDeltaLink(page) + + if len(deltaLink) > 0 { + newDeltaURL = deltaLink } // Check if there are more items - nextLink := r.GetOdataNextLink() - if nextLink == nil { + if len(nextLink) == 0 { break } - logger.Ctx(ctx).Debugf("Found %s nextLink", *nextLink) - builder = msdrives.NewItemRootDeltaRequestBuilder(*nextLink, service.Adapter()) + logger.Ctx(ctx).Debugw("Found nextLink", "link", nextLink) + pager.SetNext(nextLink) } return newDeltaURL, newPaths, excluded, nil @@ -318,7 +328,11 @@ func GetAllFolders( for _, d := range drives { _, _, _, err = collectItems( ctx, - gs, + defaultItemPager( + gs, + *d.GetId(), + "", + ), *d.GetId(), *d.GetName(), func( diff --git a/src/internal/connector/onedrive/item_test.go b/src/internal/connector/onedrive/item_test.go index 938748ca2..6a8894ebf 100644 --- a/src/internal/connector/onedrive/item_test.go +++ b/src/internal/connector/onedrive/item_test.go @@ -115,7 +115,17 @@ func (suite *ItemIntegrationSuite) TestItemReader_oneDrive() { return nil } - _, _, _, err := collectItems(ctx, suite, suite.userDriveID, "General", itemCollector) + _, _, _, err := collectItems( + ctx, + defaultItemPager( + suite, + suite.userDriveID, + "", + ), + suite.userDriveID, + "General", + itemCollector, + ) require.NoError(suite.T(), err) // Test Requirement 2: Need a file From 24c918e99c09ae481628a5e98466e5563bc530bb Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Wed, 1 Feb 2023 10:57:38 -0800 Subject: [PATCH 15/40] Patch and test some edge cases for deserializing OneDrive metadata (#2341) ## Description Currently no folder path entry is stored for the root folder. This leads to not having a folders map but having a valid delta token if all items are in the root of the drive. Update the code to add at least an empty folders map entry so we don't think we have missing metadata. Add tests to check for these edge cases. Long-term we may want to add a folder entry for the root folder. Whether we do or not may depend on how we decide to set the state field for that collection. ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * #2122 ## Test Plan - [ ] :muscle: Manual - [x] :zap: Unit test - [ ] :green_heart: E2E --- .../connector/onedrive/collections.go | 28 +++++++--- .../connector/onedrive/collections_test.go | 54 +++++++++++++++++++ 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/internal/connector/onedrive/collections.go b/src/internal/connector/onedrive/collections.go index 1e36b1ea3..e3d60fe2b 100644 --- a/src/internal/connector/onedrive/collections.go +++ b/src/internal/connector/onedrive/collections.go @@ -180,7 +180,15 @@ func deserializeMetadata( // Go through and remove partial results (i.e. path mapping but no delta URL // or vice-versa). - for k := range prevDeltas { + for k, v := range prevDeltas { + // Remove entries with an empty delta token as it's not useful. + if len(v) == 0 { + delete(prevDeltas, k) + delete(prevFolders, k) + } + + // Remove entries without a folders map as we can't tell kopia the + // hierarchy changes. if _, ok := prevFolders[k]; !ok { delete(prevDeltas, k) } @@ -287,17 +295,21 @@ func (c *Collections) Get( return nil, nil, err } + // It's alright to have an empty folders map (i.e. no folders found) but not + // an empty delta token. This is because when deserializing the metadata we + // remove entries for which there is no corresponding delta token/folder. If + // we leave empty delta tokens then we may end up setting the State field + // for collections when not actually getting delta results. if len(delta) > 0 { deltaURLs[driveID] = delta } - if len(paths) > 0 { - folderPaths[driveID] = map[string]string{} - - for id, p := range paths { - folderPaths[driveID][id] = p - } - } + // Avoid the edge case where there's no paths but we do have a valid delta + // token. We can accomplish this by adding an empty paths map for this + // drive. If we don't have this then the next backup won't use the delta + // token because it thinks the folder paths weren't persisted. + folderPaths[driveID] = map[string]string{} + maps.Copy(folderPaths[driveID], paths) maps.Copy(excludedItems, excluded) } diff --git a/src/internal/connector/onedrive/collections_test.go b/src/internal/connector/onedrive/collections_test.go index c250afe2a..39e094cd6 100644 --- a/src/internal/connector/onedrive/collections_test.go +++ b/src/internal/connector/onedrive/collections_test.go @@ -711,6 +711,60 @@ func (suite *OneDriveCollectionsSuite) TestDeserializeMetadata() { expectedPaths: map[string]map[string]string{}, errCheck: assert.NoError, }, + { + // An empty path map but valid delta results in metadata being returned + // since it's possible to have a drive with no folders other than the + // root. + name: "EmptyPaths", + cols: []func() []graph.MetadataCollectionEntry{ + func() []graph.MetadataCollectionEntry { + return []graph.MetadataCollectionEntry{ + graph.NewMetadataEntry( + graph.DeltaURLsFileName, + map[string]string{driveID1: deltaURL1}, + ), + graph.NewMetadataEntry( + graph.PreviousPathFileName, + map[string]map[string]string{ + driveID1: {}, + }, + ), + } + }, + }, + expectedDeltas: map[string]string{driveID1: deltaURL1}, + expectedPaths: map[string]map[string]string{driveID1: {}}, + errCheck: assert.NoError, + }, + { + // An empty delta map but valid path results in no metadata for that drive + // being returned since the path map is only useful if we have a valid + // delta. + name: "EmptyDeltas", + cols: []func() []graph.MetadataCollectionEntry{ + func() []graph.MetadataCollectionEntry { + return []graph.MetadataCollectionEntry{ + graph.NewMetadataEntry( + graph.DeltaURLsFileName, + map[string]string{ + driveID1: "", + }, + ), + graph.NewMetadataEntry( + graph.PreviousPathFileName, + map[string]map[string]string{ + driveID1: { + folderID1: path1, + }, + }, + ), + } + }, + }, + expectedDeltas: map[string]string{}, + expectedPaths: map[string]map[string]string{}, + errCheck: assert.NoError, + }, { name: "SuccessTwoDrivesTwoCollections", cols: []func() []graph.MetadataCollectionEntry{ From 93e8b67d156993281fe061966520c3a4a140ef58 Mon Sep 17 00:00:00 2001 From: Danny Date: Wed, 1 Feb 2023 16:07:14 -0500 Subject: [PATCH 16/40] GC: Backup: ItemAttachment Complete (#2358) ## Description Bug fix: Ensures that `item.Attachment` content is completely backed up. - Exchange Changes affect the behavior of Mail and Events. Downloads can be potentially bigger if the user have these types of attachments. - `item.Attachments` are specific to Outlook objects such as `Events` or `Messages` are included as an attachment rather than inline. ## Does this PR need a docs update or release note? - [x] :white_check_mark: Yes, it's included ## Type of change - [x] :bug: Bugfix ## Issue(s) * related to #2353 ## Test Plan - [x] :muscle: Manual --- src/internal/connector/exchange/api/events.go | 11 +++++++++-- src/internal/connector/exchange/api/mail.go | 7 ++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/internal/connector/exchange/api/events.go b/src/internal/connector/exchange/api/events.go index 962b2e576..810c1a2ff 100644 --- a/src/internal/connector/exchange/api/events.go +++ b/src/internal/connector/exchange/api/events.go @@ -108,7 +108,14 @@ func (c Events) GetItem( return nil, nil, err } - var errs *multierror.Error + var ( + errs *multierror.Error + options = &users.ItemEventsItemAttachmentsRequestBuilderGetRequestConfiguration{ + QueryParameters: &users.ItemEventsItemAttachmentsRequestBuilderGetQueryParameters{ + Expand: []string{"microsoft.graph.itemattachment/item"}, + }, + } + ) if *event.GetHasAttachments() || HasAttachments(event.GetBody()) { for count := 0; count < numberOfRetries; count++ { @@ -117,7 +124,7 @@ func (c Events) GetItem( UsersById(user). EventsById(itemID). Attachments(). - Get(ctx, nil) + Get(ctx, options) if err == nil { event.SetAttachments(attached.GetValue()) break diff --git a/src/internal/connector/exchange/api/mail.go b/src/internal/connector/exchange/api/mail.go index 29c03b82f..6a863322e 100644 --- a/src/internal/connector/exchange/api/mail.go +++ b/src/internal/connector/exchange/api/mail.go @@ -130,13 +130,18 @@ func (c Mail) GetItem( var errs *multierror.Error if *mail.GetHasAttachments() || HasAttachments(mail.GetBody()) { + options := &users.ItemMessagesItemAttachmentsRequestBuilderGetRequestConfiguration{ + QueryParameters: &users.ItemMessagesItemAttachmentsRequestBuilderGetQueryParameters{ + Expand: []string{"microsoft.graph.itemattachment/item"}, + }, + } for count := 0; count < numberOfRetries; count++ { attached, err := c.largeItem. Client(). UsersById(user). MessagesById(itemID). Attachments(). - Get(ctx, nil) + Get(ctx, options) if err == nil { mail.SetAttachments(attached.GetValue()) break From cf1c80271f7a3416db7fb83febbc0c29bcc6662f Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Wed, 1 Feb 2023 13:34:16 -0800 Subject: [PATCH 17/40] OneDrive tests for Get() (#2342) ## Description Create a test case that allows checking most of the Get call (getting item data is not mocked right now). This allows better checking on things like: * having multiple drives to backup * having items split across multiple delta query pages * errors returned by the API * aggregation of delta URLs and folder paths in metadata Eventually these tests could help with checking: * consumption of metadata for incremental backups * consumption of delta tokens for incremental backups Long-term, we may want to merge these with the UpdateCollections tests as there is overlap between the two. We may also want to consider a more stable API for checking the returned items. Right now it partly relies on being able to cast to a onedrive.Collection struct instead of using `Items()` ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [x] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * #2264 ## Test Plan - [ ] :muscle: Manual - [x] :zap: Unit test - [ ] :green_heart: E2E --- .../connector/onedrive/collections.go | 29 +- .../connector/onedrive/collections_test.go | 423 +++++++++++++++++- 2 files changed, 439 insertions(+), 13 deletions(-) diff --git a/src/internal/connector/onedrive/collections.go b/src/internal/connector/onedrive/collections.go index e3d60fe2b..200e51e23 100644 --- a/src/internal/connector/onedrive/collections.go +++ b/src/internal/connector/onedrive/collections.go @@ -67,6 +67,12 @@ type Collections struct { // Not the most ideal, but allows us to change the pager function for testing // as needed. This will allow us to mock out some scenarios during testing. + drivePagerFunc func( + source driveSource, + servicer graph.Servicer, + resourceOwner string, + fields []string, + ) (drivePager, error) itemPagerFunc func( servicer graph.Servicer, driveID, link string, @@ -89,16 +95,17 @@ func NewCollections( ctrlOpts control.Options, ) *Collections { return &Collections{ - itemClient: itemClient, - tenant: tenant, - resourceOwner: resourceOwner, - source: source, - matcher: matcher, - CollectionMap: map[string]data.Collection{}, - itemPagerFunc: defaultItemPager, - service: service, - statusUpdater: statusUpdater, - ctrl: ctrlOpts, + itemClient: itemClient, + tenant: tenant, + resourceOwner: resourceOwner, + source: source, + matcher: matcher, + CollectionMap: map[string]data.Collection{}, + drivePagerFunc: PagerForSource, + itemPagerFunc: defaultItemPager, + service: service, + statusUpdater: statusUpdater, + ctrl: ctrlOpts, } } @@ -250,7 +257,7 @@ func (c *Collections) Get( } // Enumerate drives for the specified resourceOwner - pager, err := PagerForSource(c.source, c.service, c.resourceOwner, nil) + pager, err := c.drivePagerFunc(c.source, c.service, c.resourceOwner, nil) if err != nil { return nil, nil, err } diff --git a/src/internal/connector/onedrive/collections_test.go b/src/internal/connector/onedrive/collections_test.go index 39e094cd6..21dae9549 100644 --- a/src/internal/connector/onedrive/collections_test.go +++ b/src/internal/connector/onedrive/collections_test.go @@ -1,9 +1,11 @@ package onedrive import ( + "context" "strings" "testing" + "github.com/google/uuid" "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -11,6 +13,7 @@ import ( "golang.org/x/exp/maps" "github.com/alcionai/corso/src/internal/connector/graph" + gapi "github.com/alcionai/corso/src/internal/connector/graph/api" "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/tester" @@ -969,7 +972,419 @@ func (suite *OneDriveCollectionsSuite) TestDeserializeMetadata() { } } -func driveItem(id string, name string, parentPath string, isFile, isFolder, isPackage bool) models.DriveItemable { +type mockDeltaPageLinker struct { + link *string + delta *string +} + +func (pl *mockDeltaPageLinker) GetOdataNextLink() *string { + return pl.link +} + +func (pl *mockDeltaPageLinker) GetOdataDeltaLink() *string { + return pl.delta +} + +type deltaPagerResult struct { + items []models.DriveItemable + nextLink *string + deltaLink *string + err error +} + +type mockItemPager struct { + // DriveID -> set of return values for queries for that drive. + toReturn []deltaPagerResult + getIdx int +} + +func (p *mockItemPager) GetPage(context.Context) (gapi.DeltaPageLinker, error) { + if len(p.toReturn) <= p.getIdx { + return nil, assert.AnError + } + + idx := p.getIdx + p.getIdx++ + + return &mockDeltaPageLinker{ + p.toReturn[idx].nextLink, + p.toReturn[idx].deltaLink, + }, p.toReturn[idx].err +} + +func (p *mockItemPager) SetNext(string) {} + +func (p *mockItemPager) ValuesIn(gapi.DeltaPageLinker) ([]models.DriveItemable, error) { + idx := p.getIdx + if idx > 0 { + // Return values lag by one since we increment in GetPage(). + idx-- + } + + if len(p.toReturn) <= idx { + return nil, assert.AnError + } + + return p.toReturn[idx].items, nil +} + +func (suite *OneDriveCollectionsSuite) TestGet() { + anyFolder := (&selectors.OneDriveBackup{}).Folders(selectors.Any())[0] + + tenant := "a-tenant" + user := "a-user" + + metadataPath, err := path.Builder{}.ToServiceCategoryMetadataPath( + tenant, + user, + path.OneDriveService, + path.FilesCategory, + false, + ) + require.NoError(suite.T(), err, "making metadata path") + + folderPath := expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath+"/folder", + )[0] + + empty := "" + next := "next" + delta := "delta1" + delta2 := "delta2" + + driveID1 := uuid.NewString() + drive1 := models.NewDrive() + drive1.SetId(&driveID1) + drive1.SetName(&driveID1) + + driveID2 := uuid.NewString() + drive2 := models.NewDrive() + drive2.SetId(&driveID2) + drive2.SetName(&driveID2) + + driveBasePath2 := "drive/driveID2/root:" + + folderPath2 := expectedPathAsSlice( + suite.T(), + tenant, + user, + driveBasePath2+"/folder", + )[0] + + table := []struct { + name string + drives []models.Driveable + items map[string][]deltaPagerResult + errCheck assert.ErrorAssertionFunc + // Collection name -> set of item IDs. We can't check item data because + // that's not mocked out. Metadata is checked separately. + expectedCollections map[string][]string + expectedDeltaURLs map[string]string + expectedFolderPaths map[string]map[string]string + expectedDelList map[string]struct{} + }{ + { + name: "OneDrive_OneItemPage_DelFileOnly_NoFolders_NoErrors", + drives: []models.Driveable{drive1}, + items: map[string][]deltaPagerResult{ + driveID1: { + { + items: []models.DriveItemable{ + delItem("file", testBaseDrivePath, true, false, false), + }, + deltaLink: &delta, + }, + }, + }, + errCheck: assert.NoError, + expectedCollections: map[string][]string{}, + expectedDeltaURLs: map[string]string{ + driveID1: delta, + }, + expectedFolderPaths: map[string]map[string]string{ + // We need an empty map here so deserializing metadata knows the delta + // token for this drive is valid. + driveID1: {}, + }, + expectedDelList: map[string]struct{}{ + "file": {}, + }, + }, + { + name: "OneDrive_OneItemPage_NoFolders_NoErrors", + drives: []models.Driveable{drive1}, + items: map[string][]deltaPagerResult{ + driveID1: { + { + items: []models.DriveItemable{ + driveItem("file", "file", testBaseDrivePath, true, false, false), + }, + deltaLink: &delta, + }, + }, + }, + errCheck: assert.NoError, + expectedCollections: map[string][]string{ + expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath, + )[0]: {"file"}, + }, + expectedDeltaURLs: map[string]string{ + driveID1: delta, + }, + expectedFolderPaths: map[string]map[string]string{ + // We need an empty map here so deserializing metadata knows the delta + // token for this drive is valid. + driveID1: {}, + }, + expectedDelList: map[string]struct{}{}, + }, + { + name: "OneDrive_OneItemPage_NoErrors", + drives: []models.Driveable{drive1}, + items: map[string][]deltaPagerResult{ + driveID1: { + { + items: []models.DriveItemable{ + driveItem("folder", "folder", testBaseDrivePath, false, true, false), + driveItem("file", "file", testBaseDrivePath+"/folder", true, false, false), + }, + deltaLink: &delta, + }, + }, + }, + errCheck: assert.NoError, + expectedCollections: map[string][]string{ + folderPath: {"file"}, + }, + expectedDeltaURLs: map[string]string{ + driveID1: delta, + }, + expectedFolderPaths: map[string]map[string]string{ + driveID1: { + "folder": folderPath, + }, + }, + expectedDelList: map[string]struct{}{}, + }, + { + name: "OneDrive_OneItemPage_EmptyDelta_NoErrors", + drives: []models.Driveable{drive1}, + items: map[string][]deltaPagerResult{ + driveID1: { + { + items: []models.DriveItemable{ + driveItem("folder", "folder", testBaseDrivePath, false, true, false), + driveItem("file", "file", testBaseDrivePath+"/folder", true, false, false), + }, + deltaLink: &empty, + }, + }, + }, + errCheck: assert.NoError, + expectedCollections: map[string][]string{ + folderPath: {"file"}, + }, + expectedDeltaURLs: map[string]string{}, + expectedFolderPaths: map[string]map[string]string{}, + expectedDelList: map[string]struct{}{}, + }, + { + name: "OneDrive_TwoItemPages_NoErrors", + drives: []models.Driveable{drive1}, + items: map[string][]deltaPagerResult{ + driveID1: { + { + items: []models.DriveItemable{ + driveItem("folder", "folder", testBaseDrivePath, false, true, false), + driveItem("file", "file", testBaseDrivePath+"/folder", true, false, false), + }, + nextLink: &next, + }, + { + items: []models.DriveItemable{ + driveItem("folder", "folder", testBaseDrivePath, false, true, false), + driveItem("file2", "file2", testBaseDrivePath+"/folder", true, false, false), + }, + deltaLink: &delta, + }, + }, + }, + errCheck: assert.NoError, + expectedCollections: map[string][]string{ + folderPath: {"file", "file2"}, + }, + expectedDeltaURLs: map[string]string{ + driveID1: delta, + }, + expectedFolderPaths: map[string]map[string]string{ + driveID1: { + "folder": folderPath, + }, + }, + expectedDelList: map[string]struct{}{}, + }, + { + name: "TwoDrives_OneItemPageEach_NoErrors", + drives: []models.Driveable{ + drive1, + drive2, + }, + items: map[string][]deltaPagerResult{ + driveID1: { + { + items: []models.DriveItemable{ + driveItem("folder", "folder", testBaseDrivePath, false, true, false), + driveItem("file", "file", testBaseDrivePath+"/folder", true, false, false), + }, + deltaLink: &delta, + }, + }, + driveID2: { + { + items: []models.DriveItemable{ + driveItem("folder", "folder", driveBasePath2, false, true, false), + driveItem("file", "file", driveBasePath2+"/folder", true, false, false), + }, + deltaLink: &delta2, + }, + }, + }, + errCheck: assert.NoError, + expectedCollections: map[string][]string{ + folderPath: {"file"}, + folderPath2: {"file"}, + }, + expectedDeltaURLs: map[string]string{ + driveID1: delta, + driveID2: delta2, + }, + expectedFolderPaths: map[string]map[string]string{ + driveID1: { + "folder": folderPath, + }, + driveID2: { + "folder": folderPath2, + }, + }, + expectedDelList: map[string]struct{}{}, + }, + { + name: "OneDrive_OneItemPage_Errors", + drives: []models.Driveable{drive1}, + items: map[string][]deltaPagerResult{ + driveID1: { + { + err: assert.AnError, + }, + }, + }, + errCheck: assert.Error, + expectedCollections: nil, + expectedDeltaURLs: nil, + expectedFolderPaths: nil, + expectedDelList: nil, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + ctx, flush := tester.NewContext() + defer flush() + + drivePagerFunc := func( + source driveSource, + servicer graph.Servicer, + resourceOwner string, + fields []string, + ) (drivePager, error) { + return &mockDrivePager{ + toReturn: []pagerResult{ + { + drives: test.drives, + }, + }, + }, nil + } + + itemPagerFunc := func( + servicer graph.Servicer, + driveID, link string, + ) itemPager { + return &mockItemPager{ + toReturn: test.items[driveID], + } + } + + c := NewCollections( + graph.HTTPClient(graph.NoTimeout()), + tenant, + user, + OneDriveSource, + testFolderMatcher{anyFolder}, + &MockGraphService{}, + func(*support.ConnectorOperationStatus) {}, + control.Options{}, + ) + c.drivePagerFunc = drivePagerFunc + c.itemPagerFunc = itemPagerFunc + + // TODO(ashmrtn): Allow passing previous metadata. + cols, _, err := c.Get(ctx, nil) + test.errCheck(t, err) + + if err != nil { + return + } + + for _, baseCol := range cols { + folderPath := baseCol.FullPath().String() + if folderPath == metadataPath.String() { + deltas, paths, err := deserializeMetadata(ctx, []data.Collection{baseCol}) + if !assert.NoError(t, err, "deserializing metadata") { + continue + } + + assert.Equal(t, test.expectedDeltaURLs, deltas) + assert.Equal(t, test.expectedFolderPaths, paths) + + continue + } + + // TODO(ashmrtn): We should really be getting items in the collection + // via the Items() channel, but we don't have a way to mock out the + // actual item fetch yet (mostly wiring issues). The lack of that makes + // this check a bit more bittle since internal details can change. + col, ok := baseCol.(*Collection) + require.True(t, ok, "getting onedrive.Collection handle") + + itemIDs := make([]string, 0, len(col.driveItems)) + + for id := range col.driveItems { + itemIDs = append(itemIDs, id) + } + + assert.ElementsMatch(t, test.expectedCollections[folderPath], itemIDs) + } + + // TODO(ashmrtn): Uncomment this when we begin return the set of items to + // remove from the upcoming backup. + // assert.Equal(t, test.expectedDelList, delList) + }) + } +} + +func driveItem( + id string, + name string, + parentPath string, + isFile, isFolder, isPackage bool, +) models.DriveItemable { item := models.NewDriveItem() item.SetName(&name) item.SetId(&id) @@ -992,7 +1407,11 @@ func driveItem(id string, name string, parentPath string, isFile, isFolder, isPa // delItem creates a DriveItemable that is marked as deleted. path must be set // to the base drive path. -func delItem(id string, parentPath string, isFile, isFolder, isPackage bool) models.DriveItemable { +func delItem( + id string, + parentPath string, + isFile, isFolder, isPackage bool, +) models.DriveItemable { item := models.NewDriveItem() item.SetId(&id) item.SetDeleted(models.NewDeleted()) From 46d584ec7502b6727e419d44cac22ba444cefb80 Mon Sep 17 00:00:00 2001 From: Danny Date: Wed, 1 Feb 2023 18:29:33 -0500 Subject: [PATCH 18/40] GC: Restore: Disable Item.Attachment Restore (#2359) ## Description `Item.Attachments` have unique properties that require transformation to be uploaded into the M365 system. All objects of this type are temporarily prevented from uploading via this patch. As Issue #2353 is addressed, there will be more coverage for the various types of item attachments. ## Does this PR need a docs update or release note? - [x] :white_check_mark: Yes, it's included ## Type of change - [x] :sunflower: Feature ## Issue(s) *related to #2353 ## Test Plan - [x] :muscle: Manual --- CHANGELOG.md | 2 +- src/internal/connector/exchange/attachment.go | 11 +++++++++ .../connector/exchange/restore_test.go | 14 ++++++++++- .../mockconnector/mock_data_message.go | 23 +++++++++++++++++++ 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3333c7ad6..50a5f3270 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Check if the user specified for an exchange backup operation has a mailbox. ### Changed - +- Item.Attachments are disabled from being restored for the patching of ([#2353](https://github.com/alcionai/corso/issues/2353)) - BetaClient introduced. Enables Corso to be able to interact with SharePoint Page objects. Package located `/internal/connector/graph/betasdk` - Handle case where user's drive has not been initialized - Inline attachments (e.g. copy/paste ) are discovered and backed up correctly ([#2163](https://github.com/alcionai/corso/issues/2163)) diff --git a/src/internal/connector/exchange/attachment.go b/src/internal/connector/exchange/attachment.go index 5cbce271c..5b6334e21 100644 --- a/src/internal/connector/exchange/attachment.go +++ b/src/internal/connector/exchange/attachment.go @@ -53,6 +53,17 @@ func uploadAttachment( return nil } + // item Attachments to be skipped until the completion of Issue #2353 + if attachmentType == models.ITEM_ATTACHMENTTYPE { + logger.Ctx(ctx).Infow("item attachment uploads are not supported ", + "attachment_name", *attachment.GetName(), // TODO: Update to support PII protection + "attachment_type", attachmentType, + "attachment_id", *attachment.GetId(), + ) + + return nil + } + // For Item/Reference attachments *or* file attachments < 3MB, use the attachments endpoint if attachmentType != models.FILE_ATTACHMENTTYPE || *attachment.GetSize() < largeAttachmentSize { err := uploader.uploadSmallAttachment(ctx, attachment) diff --git a/src/internal/connector/exchange/restore_test.go b/src/internal/connector/exchange/restore_test.go index 9c32fd530..187d0c127 100644 --- a/src/internal/connector/exchange/restore_test.go +++ b/src/internal/connector/exchange/restore_test.go @@ -175,6 +175,18 @@ func (suite *ExchangeRestoreSuite) TestRestoreExchangeObject() { return *folder.GetId() }, }, + { + name: "Test Mail: Item Attachment", + bytes: mockconnector.GetMockMessageWithItemAttachmentEvent("Event Item Attachment"), + category: path.EmailCategory, + destination: func(t *testing.T, ctx context.Context) string { + folderName := "TestRestoreMailItemAttachment: " + common.FormatSimpleDateTime(now) + folder, err := suite.ac.Mail().CreateMailFolder(ctx, userID, folderName) + require.NoError(t, err) + + return *folder.GetId() + }, + }, { name: "Test Mail: One Large Attachment", bytes: mockconnector.GetMockMessageWithLargeAttachment("Restore Large Attachment"), @@ -266,7 +278,7 @@ func (suite *ExchangeRestoreSuite) TestRestoreExchangeObject() { userID, ) assert.NoError(t, err, support.ConnectorStackErrorTrace(err)) - assert.NotNil(t, info, "item info is populated") + assert.NotNil(t, info, "item info was not populated") assert.NoError(t, deleters[test.category].DeleteContainer(ctx, userID, destination)) }) } diff --git a/src/internal/connector/mockconnector/mock_data_message.go b/src/internal/connector/mockconnector/mock_data_message.go index 597447492..da06cbaab 100644 --- a/src/internal/connector/mockconnector/mock_data_message.go +++ b/src/internal/connector/mockconnector/mock_data_message.go @@ -336,3 +336,26 @@ func GetMockEventMessageRequest(subject string) []byte { return []byte(message) } + +func GetMockMessageWithItemAttachmentEvent(subject string) []byte { + //nolint:lll + message := "{\"id\":\"AAMkAGQ1NzViZTdhLTEwMTMtNGJjNi05YWI2LTg4NWRlZDA2Y2UxOABGAAAAAAAPvVwUramXT7jlSGpVU8_7BwB8wYc0thTTTYl3RpEYIUq_AAAAAAEMAAB8wYc0thTTTYl3RpEYIUq_AADFfThMAAA=\",\"@odata.type\":\"#microsoft.graph.message\"," + + "\"@odata.etag\":\"W/\\\"CQAAABYAAAB8wYc0thTTTYl3RpEYIUq+AADFK3BH\\\"\",\"@odata.context\":\"https://graph.microsoft.com/v1.0/$metadata#users('dustina%408qzvrj.onmicrosoft.com')/messages/$entity\",\"categories\":[]," + + "\"changeKey\":\"CQAAABYAAAB8wYc0thTTTYl3RpEYIUq+AADFK3BH\",\"createdDateTime\":\"2023-02-01T13:48:43Z\",\"lastModifiedDateTime\":\"2023-02-01T18:27:03Z\"," + + "\"attachments\":[{\"id\":\"AAMkAGQ1NzViZTdhLTEwMTMtNGJjNi05YWI2LTg4NWRlZDA2Y2UxOABGAAAAAAAPvVwUramXT7jlSGpVU8_7BwB8wYc0thTTTYl3RpEYIUq_AAAAAAEMAAB8wYc0thTTTYl3RpEYIUq_AADFfThMAAABEgAQAKHxTL6mNCZPo71dbwrfKYM=\"," + + "\"@odata.type\":\"#microsoft.graph.itemAttachment\",\"isInline\":false,\"lastModifiedDateTime\":\"2023-02-01T13:52:56Z\",\"name\":\"Holidayevent\",\"size\":2059,\"item\":{\"id\":\"\",\"@odata.type\":\"#microsoft.graph.event\"," + + "\"createdDateTime\":\"2023-02-01T13:52:56Z\",\"lastModifiedDateTime\":\"2023-02-01T13:52:56Z\",\"body\":{\"content\":\"\\r\\nLet'slookforfunding!\"," + + "\"contentType\":\"html\"},\"end\":{\"dateTime\":\"2016-12-02T19:00:00.0000000Z\",\"timeZone\":\"UTC\"}," + + "\"hasAttachments\":false,\"isAllDay\":false,\"isCancelled\":false,\"isDraft\":true,\"isOnlineMeeting\":false,\"isOrganizer\":true,\"isReminderOn\":false,\"organizer\":{\"emailAddress\":{\"address\":\"" + defaultMessageFrom + "\",\"name\":\"" + defaultAlias + "\"}}," + + "\"originalEndTimeZone\":\"tzone://Microsoft/Utc\",\"originalStartTimeZone\":\"tzone://Microsoft/Utc\",\"reminderMinutesBeforeStart\":0,\"responseRequested\":true,\"start\":{\"dateTime\":\"2016-12-02T18:00:00.0000000Z\",\"timeZone\":\"UTC\"}," + + "\"subject\":\"Discussgiftsforchildren\",\"type\":\"singleInstance\"}}],\"bccRecipients\":[],\"body\":{\"content\":\"\\r\\n\\r\\n\\r\\nLookingtodothis \",\"contentType\":\"html\"}," + + "\"bodyPreview\":\"Lookingtodothis\",\"ccRecipients\":[],\"conversationId\":\"AAQkAGQ1NzViZTdhLTEwMTMtNGJjNi05YWI2LTg4NWRlZDA2Y2UxOAAQADGvj5ACBMdGpESX4xSOxCo=\",\"conversationIndex\":\"AQHZNkPmMa+PkAIEx0akRJfjFI7EKg==\",\"flag\":{\"flagStatus\":\"notFlagged\"}," + + "\"from\":{\"emailAddress\":{\"address\":\"" + defaultMessageFrom + "\",\"name\":\"" + defaultAlias + "\"}},\"hasAttachments\":true,\"importance\":\"normal\",\"inferenceClassification\":\"focused\"," + + "\"internetMessageId\":\"\",\"isDeliveryReceiptRequested\":false,\"isDraft\":false,\"isRead\":true,\"isReadReceiptRequested\":false," + + "\"parentFolderId\":\"AQMkAGQ1NzViZTdhLTEwMTMtNGJjNi05YWI2LTg4ADVkZWQwNmNlMTgALgAAAw_9XBStqZdPuOVIalVTz7sBAHzBhzS2FNNNiXdGkRghSr4AAAIBDAAAAA==\",\"receivedDateTime\":\"2023-02-01T13:48:47Z\",\"replyTo\":[]," + + "\"sender\":{\"emailAddress\":{\"address\":\"" + defaultMessageSender + "\",\"name\":\"" + defaultAlias + "\"}},\"sentDateTime\":\"2023-02-01T13:48:46Z\"," + + "\"subject\":\"" + subject + "\",\"toRecipients\":[{\"emailAddress\":{\"address\":\"" + defaultMessageTo + "\",\"name\":\"" + defaultAlias + "\"}}]," + + "\"webLink\":\"https://outlook.office365.com/owa/?ItemID=AAMkAGQ1NzViZTdhLTEwMTMtNGJjNi05YWI2LTg4NWRlZDA2Y2UxOABGAAAAAAAPvVwUramXT7jlSGpVU8%2B7BwB8wYc0thTTTYl3RpEYIUq%2BAAAAAAEMAAB8wYc0thTTTYl3RpEYIUq%2BAADFfThMAAA%3D&exvsurl=1&viewmodel=ReadMessageItem\"}" + + return []byte(message) +} From 2a0640f9f371f0347c6b822ac0eb83df7a84bba2 Mon Sep 17 00:00:00 2001 From: Vaibhav Kamra Date: Wed, 1 Feb 2023 19:35:34 -0800 Subject: [PATCH 19/40] [chore] Remove redundant module use (#2363) ## Description Standardize on `go-humanize` ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [x] :broom: Tech Debt/Cleanup ## Issue(s) * #1038 ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- src/go.mod | 1 - src/go.sum | 2 -- src/internal/connector/support/status.go | 4 ++-- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/go.mod b/src/go.mod index f947ed45a..481fd7fe2 100644 --- a/src/go.mod +++ b/src/go.mod @@ -71,7 +71,6 @@ require ( github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect - github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.12 // indirect diff --git a/src/go.sum b/src/go.sum index 02f39a5a5..0befca54a 100644 --- a/src/go.sum +++ b/src/go.sum @@ -209,8 +209,6 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s= -github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= diff --git a/src/internal/connector/support/status.go b/src/internal/connector/support/status.go index 3f2435263..7e38758d3 100644 --- a/src/internal/connector/support/status.go +++ b/src/internal/connector/support/status.go @@ -4,8 +4,8 @@ import ( "context" "fmt" + "github.com/dustin/go-humanize" multierror "github.com/hashicorp/go-multierror" - bytesize "github.com/inhies/go-bytesize" "github.com/alcionai/corso/src/pkg/logger" ) @@ -142,7 +142,7 @@ func (cos *ConnectorOperationStatus) String() string { cos.lastOperation.String(), cos.Successful, cos.ObjectCount, - bytesize.New(float64(cos.bytes)), + humanize.Bytes(uint64(cos.bytes)), cos.FolderCount, ) From 19dbad4c460d947cbeb78315240a21f95aff20f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Feb 2023 03:45:51 +0000 Subject: [PATCH 20/40] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20http-cache-se?= =?UTF-8?q?mantics=20from=204.1.0=20to=204.1.1=20in=20/website=20(#2367)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/package-lock.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/website/package-lock.json b/website/package-lock.json index 100008906..632819d60 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -7999,10 +7999,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "license": "BSD-2-Clause" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, "node_modules/http-deceiver": { "version": "1.2.7", @@ -19958,9 +19957,9 @@ } }, "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, "http-deceiver": { "version": "1.2.7", From 6f44a507b57d9f616465884a1f830043fa031d3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Feb 2023 06:38:11 +0000 Subject: [PATCH 21/40] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20github.com/aw?= =?UTF-8?q?s/aws-sdk-go=20from=201.44.191=20to=201.44.192=20in=20/src=20(#?= =?UTF-8?q?2369)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.44.191 to 1.44.192.
Release notes

Sourced from github.com/aws/aws-sdk-go's releases.

Release v1.44.192 (2023-02-01)

Service Client Updates

  • service/devops-guru: Updates service API and documentation
  • service/forecast: Updates service API and documentation
  • service/iam: Updates service documentation
    • Documentation updates for AWS Identity and Access Management (IAM).
  • service/mediatailor: Updates service API and documentation
  • service/sns: Updates service documentation
    • Additional attributes added for set-topic-attributes.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/aws/aws-sdk-go&package-manager=go_modules&previous-version=1.44.191&new-version=1.44.192)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- src/go.mod | 2 +- src/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/go.mod b/src/go.mod index 481fd7fe2..bf43f6494 100644 --- a/src/go.mod +++ b/src/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 github.com/alcionai/clues v0.0.0-20230131232239-cee86233b005 - github.com/aws/aws-sdk-go v1.44.191 + github.com/aws/aws-sdk-go v1.44.192 github.com/aws/aws-xray-sdk-go v1.8.0 github.com/google/uuid v1.3.0 github.com/hashicorp/go-multierror v1.1.1 diff --git a/src/go.sum b/src/go.sum index 0befca54a..17d6f25bd 100644 --- a/src/go.sum +++ b/src/go.sum @@ -62,8 +62,8 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5 github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/aws/aws-sdk-go v1.44.191 h1:GnbkalCx/AgobaorDMFCa248acmk+91+aHBQOk7ljzU= -github.com/aws/aws-sdk-go v1.44.191/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.192 h1:KL54vCxRd5v5XBGjnF3FelzXXwl+aWHDmDTihFmRNgM= +github.com/aws/aws-sdk-go v1.44.192/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-xray-sdk-go v1.8.0 h1:0xncHZ588wB/geLjbM/esoW3FOEThWy2TJyb4VXfLFY= github.com/aws/aws-xray-sdk-go v1.8.0/go.mod h1:7LKe47H+j3evfvS1+q0wzpoaGXGrF3mUsfM+thqVO+A= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= From 844dcae9b6c351e8688f7e9536c400053b313ae6 Mon Sep 17 00:00:00 2001 From: Danny Date: Thu, 2 Feb 2023 12:22:04 -0500 Subject: [PATCH 22/40] GC: Logging: Expand `ItemAttachment` object details for coverage (#2366) ## Description Updates within /internal/connector/exchange package. Adds helper function for displaying the internal object item type that is included as an attachment. This will help overall with supporting additional `ItemAttachment` objects. ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :broom: Tech Debt/Cleanup ## Issue(s) * related to #2353 ## Test Plan - [x] :muscle: Manual --- src/internal/connector/exchange/attachment.go | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/internal/connector/exchange/attachment.go b/src/internal/connector/exchange/attachment.go index 5b6334e21..6ed05b5df 100644 --- a/src/internal/connector/exchange/attachment.go +++ b/src/internal/connector/exchange/attachment.go @@ -55,9 +55,15 @@ func uploadAttachment( // item Attachments to be skipped until the completion of Issue #2353 if attachmentType == models.ITEM_ATTACHMENTTYPE { + name := "" + if attachment.GetName() != nil { + name = *attachment.GetName() + } + logger.Ctx(ctx).Infow("item attachment uploads are not supported ", - "attachment_name", *attachment.GetName(), // TODO: Update to support PII protection + "attachment_name", name, // TODO: Update to support PII protection "attachment_type", attachmentType, + "internal_item_type", getItemAttachmentItemType(attachment), "attachment_id", *attachment.GetId(), ) @@ -101,3 +107,19 @@ func uploadLargeAttachment(ctx context.Context, uploader attachmentUploadable, return nil } + +func getItemAttachmentItemType(query models.Attachmentable) string { + empty := "" + attachment, ok := query.(models.ItemAttachmentable) + + if !ok { + return empty + } + + item := attachment.GetItem() + if item.GetOdataType() == nil { + return empty + } + + return *item.GetOdataType() +} From a3aa3bcad03a84a05b092a3f72ac20fc2e118819 Mon Sep 17 00:00:00 2001 From: Keepers Date: Thu, 2 Feb 2023 13:34:53 -0700 Subject: [PATCH 23/40] Issue 2329 logfile 1 (#2344) ## Description Prevents showing the log file location when calling commands that defer to help output or env details. Certain cases aren't caught, such as when calling a command with no flags (ex: `corso backup create exchange`). In those cases we catch the lack of flags and manually display the usage within the command handler. ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :broom: Tech Debt/Cleanup ## Issue(s) * #2329 ## Test Plan - [x] :muscle: Manual --- .gitignore | 3 +++ src/cli/cli.go | 8 ++++++++ src/pkg/logger/logger.go | 3 +++ 3 files changed, 14 insertions(+) diff --git a/.gitignore b/.gitignore index 46f5189b8..911d91a10 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,9 @@ .corso_test.toml .corso.toml +# Logging +.corso.log + # Build directories /bin /docker/bin diff --git a/src/cli/cli.go b/src/cli/cli.go index f202e4953..77a03b1a7 100644 --- a/src/cli/cli.go +++ b/src/cli/cli.go @@ -8,6 +8,7 @@ import ( "github.com/alcionai/clues" "github.com/spf13/cobra" + "golang.org/x/exp/slices" "github.com/alcionai/corso/src/cli/backup" "github.com/alcionai/corso/src/cli/config" @@ -51,6 +52,13 @@ func preRun(cc *cobra.Command, args []string) error { flagSl = append(flagSl, f) } + avoidTheseCommands := []string{ + "corso", "env", "help", "backup", "details", "list", "restore", "delete", "repo", "init", "connect", + } + if len(logger.LogFile) > 0 && !slices.Contains(avoidTheseCommands, cc.Use) { + print.Info(cc.Context(), "Logging to file: "+logger.LogFile) + } + log.Infow("cli command", "command", cc.CommandPath(), "flags", flagSl, "version", version.CurrentVersion()) return nil diff --git a/src/pkg/logger/logger.go b/src/pkg/logger/logger.go index 923af44c8..abbce9119 100644 --- a/src/pkg/logger/logger.go +++ b/src/pkg/logger/logger.go @@ -29,6 +29,8 @@ var ( DebugAPI bool readableOutput bool + + LogFile string ) type logLevel int @@ -118,6 +120,7 @@ func PreloadLoggingFlags() (string, string) { } if logfile != "stdout" && logfile != "stderr" { + LogFile = logfile logdir := filepath.Dir(logfile) print.Info(context.Background(), "Logging to file: "+logfile) From b00b41a6bda909faf85d65e58ab98443aa3284c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C4=8Dnica=20Mellifera?= Date: Thu, 2 Feb 2023 17:12:18 -0800 Subject: [PATCH 24/40] new blog post on storage options for corso (#2362) ## Description new blog post on storage options for corso ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [ ] :bug: Bugfix - [x] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * # ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- website/blog/2023-2-4-where-to-store-corso.md | 118 ++++++++++++++++++ website/blog/images/boxes_web.jpeg | Bin 0 -> 354646 bytes website/styles/Vocab/Base/accept.txt | 4 +- 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 website/blog/2023-2-4-where-to-store-corso.md create mode 100644 website/blog/images/boxes_web.jpeg diff --git a/website/blog/2023-2-4-where-to-store-corso.md b/website/blog/2023-2-4-where-to-store-corso.md new file mode 100644 index 000000000..cdde919b4 --- /dev/null +++ b/website/blog/2023-2-4-where-to-store-corso.md @@ -0,0 +1,118 @@ +--- +slug: where-to-store-corso +title: "Where to store your Corso Repository" +description: "Storage Options for Corso" +authors: nica +tags: [corso, microsoft 365, backups, S3] +date: 2023-2-4 +image: ./images/boxes_web.jpeg +--- + +![image of a large number of packing boxes](./images/boxes_web.jpeg) + +We all know that Corso is a free and open-source tool for creating backups of your Microsoft 365 data. But where does +that data go? + +Corso creates a repository to store your backups, and the default in our documentation is to send that data to AWS S3. +It's possible however to back up to any object storage system that has an S3-compatible API. Let’s talk about some options. + + +## S3-Compatible Object Storage + +A number of other cloud providers aren’t the 500-pound gorilla of AWS but still offer an S3-compatible API. +Some of them include: + +- Google Cloud: One of the largest cloud providers in the world, Google offers +[an S3-compatible API](https://cloud.google.com/storage/docs/interoperability) on top of its Google Cloud Storage (GCS) offering. +- Backblaze: Known for its deep analysis of hard drive failure statistics, Backblaze offers an S3-compatible API for its +B2 Cloud Storage product. They also make the bold claim of costing [significantly less than AWS S3](https://www.backblaze.com/b2/cloud-storage-pricing.html) +(I haven’t evaluated these claims) but Glacier is still cheaper (see below for more details) +- HPE: HPE Greenlake offers S3 compatibility and claims superior performance over S3. If you want to get a sense of how +‘Enterprise’ HPE is, the best writeup I could find of their offerings is [available only as a PDF](https://www.hpe.com/us/en/collaterals/collateral.a50006216.Create-value-from-data-2C-at-scale-E2-80-93-HPE-GreenLake-for-Scality-solution-brief.html). +- Wasabi: Another popular offering, Wasabi has great integration with existing AWS components at a reduced +cost but watch out for the minimum monthly storage charge and the minimum storage duration policy! + +This is an incomplete list, but any S3-compliant storage with immediate retrieval is expected to work with Corso today. + +## Local S3 Testing + +In my own testing, I use [MinIO](https://min.io/) to create a local S3 server and bucket. This has some great advantages +including extremely low latency for testing. Unless you have a significant hardware and software investment to ensure +reliable storage and compute infrastructure, you probably don't want to rely on a MinIO setup as your primary +backup location, but it’s a great way to do a zero-cost test backup that you totally control. + +While there are a number of in-depth tutorials on how to use +[MinIO to run a local S3 server](https://simonjcarr.medium.com/running-s3-object-storage-locally-with-minio-f50540ffc239), +here’s the single script that can run a non-production instance of MinIO within a Docker container (you’ll need Docker +and the AWS CLI as prerequisites) and get you started with Corso quickly: + +```bash +mkdir -p ~\s/minio/data + +docker run \ + -p 9000:9000 \ + -p 9090:9090 \ + --name minio \ + -v ~/minio/data:/data \ + -e "MINIO_ROOT_USER=ROOTNAME" \ + -e "MINIO_ROOT_PASSWORD=CHANGEME123" \ + quay.io/minio/minio server /data --console-address ":9090" +``` + +In a separate window, create a bucket (`corso-backup`) for use with Corso. + +```bash +export AWS_ACCESS_KEY_ID=ROOTNAME +export AWS_SECRET_ACCESS_KEY=CHANGEME123 + +aws s3api create-bucket --bucket corso-backup --endpoint=http://127.0.0.1:9000 +``` + +To connect Corso to a local MinIO server with `[corso repo init](https://corsobackup.io/docs/cli/corso-repo-init-s3/)` +you’ll want to pass the `--disable-tls` flag so that it will accept an `http` connection + +## Reducing Cost With S3 Storage Classes + +AWS S3 offers [storage classes](https://aws.amazon.com/s3/storage-classes/) for a variety of different use cases and +Corso can leverage a number of them, but not all, to reduce the cost of storing data in the cloud. + +By default, Corso works hard to reduce its data footprint. It will compress and deduplicate data at source to reduce the +amount of storage used as well as the amount of network traffic when writing to object storage. Corso also combines +different emails, attachments, etc. into larger objects to make it more cost-effective by reducing the number of API +calls and increasing network throughput as well as making Corso data eligible and cost-effective for some of the other +storage classes described below. + +Stepping away from the default S3 offering (S3 Standard), S3 offers a number of different Glacier (cheap and deep) +storage classes that can help to further reduce the cost for backup and archival workloads. Within the storage classes, +Corso today supports Glacier Instant Retrieval but, because of user responsiveness and metadata requirements, not the +other Glacier variants. + +Glacier Instant Retrieval should provide the best price performance for a backup workload as backup data blobs are +typically written once, with occasional re-compacting, and read infrequently in the case of restore. One should note +that recommendations such as these are always workload dependent and should be verified for your use case. For example, +we would not recommend Glacier Instant Retrieval if you are constantly testing large restores or have heavy +churn in your backups and +limited retention. However, for most typical backup workloads (write mostly, read rarely), Glacier Instant Retrieval +should work just fine and deliver the best price-performance ratio. + +You can configure your storage to use Glacier Instant Retrieval by adding a `.storageconfig` file to the root of your +bucket. If you have configured Corso to store the repository in a sub-folder within your bucket by adding a +`prefix = '[folder name]'` configuration, the `.storageconfig` should go within that folder in the bucket. + +Here’s an example: + +```json +{ + "blobOptions": [ + { "prefix": "p", "storageClass": "GLACIER_IR" }, + { "storageClass": "STANDARD" } + ] +} +``` + +The `"prefix": "p"` parameter is unrelated to the subfolder `prefix` setting mentioned above. It tells Corso to +use the selected storage class for data blobs (named with a `p` prefix). By default, all other objects including +metadata and indices will use the standard storage tier. + +We would love to hear from you if you’ve deployed Corso with a different storage class, an object storage provider not +listed above, or have questions about how to best cost-optimize your setup. Come [find us on Discord](https://discord.gg/63DTTSnuhT)! diff --git a/website/blog/images/boxes_web.jpeg b/website/blog/images/boxes_web.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..840e96526e084922750b1e72f6a5975df6425781 GIT binary patch literal 354646 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

KtlxK0ka$1u4q*hr8$oZw2n(BENq)xU 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%0Dw7Yf39(- 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%0Dod$WIvdLC_V9P~h1P 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

)oA@jH4VfAfe@;4(o4RiI3rXL3ePH9vU2 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

kdPuI9PILi=my6O}&yWzLsV@2SsIhMpP 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 literal 0 HcmV?d00001 diff --git a/website/styles/Vocab/Base/accept.txt b/website/styles/Vocab/Base/accept.txt index 69565b8a5..8831b2ca6 100644 --- a/website/styles/Vocab/Base/accept.txt +++ b/website/styles/Vocab/Base/accept.txt @@ -37,4 +37,6 @@ SLAs runbooks stdout stderr -backoff \ No newline at end of file +backoff +Greenlake +subfolder \ No newline at end of file From 0436e0d128cb5ed85b8f99560ab876b8c36bef3a Mon Sep 17 00:00:00 2001 From: Keepers Date: Thu, 2 Feb 2023 19:01:42 -0700 Subject: [PATCH 25/40] extra panic protection in operations (#2383) ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :broom: Tech Debt/Cleanup ## Test Plan - [x] :green_heart: E2E --- src/internal/operations/backup.go | 33 +++++++++++++++++++++++++----- src/internal/operations/restore.go | 19 ++++++++++++++--- src/pkg/fault/fault.go | 7 ++++--- src/pkg/fault/fault_test.go | 4 ++++ 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/internal/operations/backup.go b/src/internal/operations/backup.go index 8e40d820f..e0df72d55 100644 --- a/src/internal/operations/backup.go +++ b/src/internal/operations/backup.go @@ -2,6 +2,7 @@ package operations import ( "context" + "runtime/debug" "time" "github.com/alcionai/clues" @@ -106,7 +107,13 @@ type detailsWriter interface { // --------------------------------------------------------------------------- // Run begins a synchronous backup operation. -func (op *BackupOperation) Run(ctx context.Context) error { +func (op *BackupOperation) Run(ctx context.Context) (err error) { + defer func() { + if r := recover(); r != nil { + err = clues.Wrap(r.(error), "panic recovery").WithClues(ctx).With("stacktrace", debug.Stack()) + } + }() + ctx, end := D.Span(ctx, "operations:backup:run") defer func() { end() @@ -189,6 +196,8 @@ func (op *BackupOperation) do(ctx context.Context) (err error) { op.Errors.Fail(errors.Wrap(err, "collecting manifest heuristics")) opStats.readErr = op.Errors.Err() + logger.Ctx(ctx).With("err", err).Errorw("producing manifests and metadata", clues.InErr(err).Slice()...) + return opStats.readErr } @@ -197,6 +206,8 @@ func (op *BackupOperation) do(ctx context.Context) (err error) { op.Errors.Fail(errors.Wrap(err, "connecting to m365")) opStats.readErr = op.Errors.Err() + logger.Ctx(ctx).With("err", err).Errorw("connectng to m365", clues.InErr(err).Slice()...) + return opStats.readErr } @@ -205,6 +216,8 @@ func (op *BackupOperation) do(ctx context.Context) (err error) { op.Errors.Fail(errors.Wrap(err, "retrieving data to backup")) opStats.readErr = op.Errors.Err() + logger.Ctx(ctx).With("err", err).Errorw("producing backup data collections", clues.InErr(err).Slice()...) + return opStats.readErr } @@ -223,6 +236,8 @@ func (op *BackupOperation) do(ctx context.Context) (err error) { op.Errors.Fail(errors.Wrap(err, "backing up service data")) opStats.writeErr = op.Errors.Err() + logger.Ctx(ctx).With("err", err).Errorw("persisting collection backups", clues.InErr(err).Slice()...) + return opStats.writeErr } @@ -237,6 +252,8 @@ func (op *BackupOperation) do(ctx context.Context) (err error) { op.Errors.Fail(errors.Wrap(err, "merging backup details")) opStats.writeErr = op.Errors.Err() + logger.Ctx(ctx).With("err", err).Errorw("merging details", clues.InErr(err).Slice()...) + return opStats.writeErr } @@ -589,15 +606,21 @@ func (op *BackupOperation) persistResults( opStats.writeErr) } + op.Results.BytesRead = opStats.k.TotalHashedBytes + op.Results.BytesUploaded = opStats.k.TotalUploadedBytes + op.Results.ItemsWritten = opStats.k.TotalFileCount + op.Results.ResourceOwners = opStats.resourceCount + + if opStats.gc == nil { + op.Status = Failed + return errors.New("data population never completed") + } + if opStats.readErr == nil && opStats.writeErr == nil && opStats.gc.Successful == 0 { op.Status = NoData } - op.Results.BytesRead = opStats.k.TotalHashedBytes - op.Results.BytesUploaded = opStats.k.TotalUploadedBytes op.Results.ItemsRead = opStats.gc.Successful - op.Results.ItemsWritten = opStats.k.TotalFileCount - op.Results.ResourceOwners = opStats.resourceCount return nil } diff --git a/src/internal/operations/restore.go b/src/internal/operations/restore.go index 30bc303a9..aa9229336 100644 --- a/src/internal/operations/restore.go +++ b/src/internal/operations/restore.go @@ -3,6 +3,7 @@ package operations import ( "context" "fmt" + "runtime/debug" "time" "github.com/alcionai/clues" @@ -106,6 +107,12 @@ type restorer interface { // Run begins a synchronous restore operation. func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.Details, err error) { + defer func() { + if r := recover(); r != nil { + err = clues.Wrap(r.(error), "panic recovery").WithClues(ctx).With("stacktrace", debug.Stack()) + } + }() + ctx, end := D.Span(ctx, "operations:restore:run") defer func() { end() @@ -250,14 +257,20 @@ func (op *RestoreOperation) persistResults( opStats.writeErr) } + op.Results.BytesRead = opStats.bytesRead.NumBytes + op.Results.ItemsRead = len(opStats.cs) // TODO: file count, not collection count + op.Results.ResourceOwners = opStats.resourceCount + + if opStats.gc == nil { + op.Status = Failed + return errors.New("data restoration never completed") + } + if opStats.readErr == nil && opStats.writeErr == nil && opStats.gc.Successful == 0 { op.Status = NoData } - op.Results.BytesRead = opStats.bytesRead.NumBytes - op.Results.ItemsRead = len(opStats.cs) // TODO: file count, not collection count op.Results.ItemsWritten = opStats.gc.Successful - op.Results.ResourceOwners = opStats.resourceCount dur := op.Results.CompletedAt.Sub(op.Results.StartedAt) diff --git a/src/pkg/fault/fault.go b/src/pkg/fault/fault.go index 69017029c..b8f57108b 100644 --- a/src/pkg/fault/fault.go +++ b/src/pkg/fault/fault.go @@ -87,11 +87,12 @@ func (e *Errors) Fail(err error) *Errors { // setErr handles setting errors.err. Sync locking gets // handled upstream of this call. func (e *Errors) setErr(err error) *Errors { - if e.err != nil { - return e.addErr(err) + if e.err == nil { + e.err = err + return e } - e.err = err + e.errs = append(e.errs, err) return e } diff --git a/src/pkg/fault/fault_test.go b/src/pkg/fault/fault_test.go index 3f5ad127c..8fb2981d7 100644 --- a/src/pkg/fault/fault_test.go +++ b/src/pkg/fault/fault_test.go @@ -73,6 +73,8 @@ func (suite *FaultErrorsUnitSuite) TestErr() { suite.T().Run(test.name, func(t *testing.T) { n := fault.New(test.failFast) require.NotNil(t, n) + require.NoError(t, n.Err()) + require.Empty(t, n.Errs()) e := n.Fail(test.fail) require.NotNil(t, e) @@ -90,6 +92,8 @@ func (suite *FaultErrorsUnitSuite) TestFail() { n := fault.New(false) require.NotNil(t, n) + require.NoError(t, n.Err()) + require.Empty(t, n.Errs()) n.Fail(assert.AnError) assert.Error(t, n.Err()) From b8bc85deba3b0c9e386b6e98545b087809a04025 Mon Sep 17 00:00:00 2001 From: Abin Simon Date: Fri, 3 Feb 2023 08:55:51 +0530 Subject: [PATCH 26/40] Metadata backup for OneDrive (#2148) ## Description This PR adds option to backup and restore additional metadata(currently permissions) for OneDrive. **This PR also adds the ability to restore empty folders for OneDrive along with their permissions.** Breaking change: Any old backups will not work as we expect both `.data` and `.meta`/`.dirmeta` files to be available for OneDrive backups. ## Does this PR need a docs update or release note? *Added changelog, docs pending.* - [x] :white_check_mark: Yes, it's included - [x] :clock1: Yes, but in a later PR - [ ] :no_entry: No ## Type of change - [x] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * https://github.com/alcionai/corso/issues/1774 ## Test Plan - [ ] :muscle: Manual - [x] :zap: Unit test - [x] :green_heart: E2E --- CHANGELOG.md | 8 + src/cli/backup/onedrive.go | 1 + src/cli/options/options.go | 45 +- src/cli/restore/onedrive.go | 6 + src/cmd/factory/impl/common.go | 5 +- src/cmd/factory/impl/exchange.go | 4 + src/internal/connector/graph_connector.go | 3 +- .../connector/graph_connector_helper_test.go | 62 ++- .../connector/graph_connector_test.go | 510 +++++++++++++++++- src/internal/connector/onedrive/collection.go | 275 +++++++--- .../connector/onedrive/collection_test.go | 163 +++++- .../connector/onedrive/collections.go | 15 +- .../connector/onedrive/collections_test.go | 177 +++--- src/internal/connector/onedrive/item.go | 80 ++- src/internal/connector/onedrive/item_test.go | 2 +- src/internal/connector/onedrive/restore.go | 415 ++++++++++++-- .../sharepoint/data_collections_test.go | 2 +- src/internal/connector/sharepoint/restore.go | 8 +- src/internal/connector/support/status.go | 1 + .../operations/backup_integration_test.go | 2 +- src/internal/operations/restore.go | 16 +- src/pkg/control/options.go | 14 +- 22 files changed, 1540 insertions(+), 274 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50a5f3270..33485d926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Document Corso's fault-tolerance and restartability features - Add retries on timeouts and status code 500 for Exchange - Increase page size preference for delta requests for Exchange to reduce number of roundtrips +- OneDrive file/folder permissions can now be backed up and restored +- Add `--restore-permissions` flag to toggle restoration of OneDrive permissions + +### Known Issues + +- When the same user has permissions to a file and the containing + folder, we only restore folder level permissions for the user and no + separate file only permission is restored. ## [v0.2.0] (alpha) - 2023-1-29 diff --git a/src/cli/backup/onedrive.go b/src/cli/backup/onedrive.go index 60a055dce..f74f98916 100644 --- a/src/cli/backup/onedrive.go +++ b/src/cli/backup/onedrive.go @@ -79,6 +79,7 @@ func addOneDriveCommands(cmd *cobra.Command) *cobra.Command { switch cmd.Use { case createCommand: c, fs = utils.AddCommand(cmd, oneDriveCreateCmd()) + options.AddFeatureToggle(cmd, options.DisablePermissionsBackup()) c.Use = c.Use + " " + oneDriveServiceCommandCreateUseSuffix c.Example = oneDriveServiceCommandCreateExamples diff --git a/src/cli/options/options.go b/src/cli/options/options.go index 4988c29ca..32defc5bb 100644 --- a/src/cli/options/options.go +++ b/src/cli/options/options.go @@ -11,17 +11,11 @@ import ( func Control() control.Options { opt := control.Defaults() - if fastFail { - opt.FailFast = true - } - - if noStats { - opt.DisableMetrics = true - } - - if disableIncrementals { - opt.ToggleFeatures.DisableIncrementals = true - } + opt.FailFast = fastFail + opt.DisableMetrics = noStats + opt.RestorePermissions = restorePermissions + opt.ToggleFeatures.DisableIncrementals = disableIncrementals + opt.ToggleFeatures.DisablePermissionsBackup = disablePermissionsBackup return opt } @@ -31,8 +25,9 @@ func Control() control.Options { // --------------------------------------------------------------------------- var ( - fastFail bool - noStats bool + fastFail bool + noStats bool + restorePermissions bool ) // AddOperationFlags adds command-local operation flags @@ -49,11 +44,20 @@ func AddGlobalOperationFlags(cmd *cobra.Command) { fs.BoolVar(&noStats, "no-stats", false, "disable anonymous usage statistics gathering") } +// AddRestorePermissionsFlag adds OneDrive flag for restoring permissions +func AddRestorePermissionsFlag(cmd *cobra.Command) { + fs := cmd.Flags() + fs.BoolVar(&restorePermissions, "restore-permissions", false, "Restore permissions for files and folders") +} + // --------------------------------------------------------------------------- // Feature Flags // --------------------------------------------------------------------------- -var disableIncrementals bool +var ( + disableIncrementals bool + disablePermissionsBackup bool +) type exposeFeatureFlag func(*pflag.FlagSet) @@ -78,3 +82,16 @@ func DisableIncrementals() func(*pflag.FlagSet) { cobra.CheckErr(fs.MarkHidden("disable-incrementals")) } } + +// Adds the hidden '--disable-permissions-backup' cli flag which, when +// set, disables backing up permissions. +func DisablePermissionsBackup() func(*pflag.FlagSet) { + return func(fs *pflag.FlagSet) { + fs.BoolVar( + &disablePermissionsBackup, + "disable-permissions-backup", + false, + "Disable backing up item permissions for OneDrive") + cobra.CheckErr(fs.MarkHidden("disable-permissions-backup")) + } +} diff --git a/src/cli/restore/onedrive.go b/src/cli/restore/onedrive.go index 526db414b..bd8dc7816 100644 --- a/src/cli/restore/onedrive.go +++ b/src/cli/restore/onedrive.go @@ -63,6 +63,9 @@ func addOneDriveCommands(cmd *cobra.Command) *cobra.Command { utils.FileFN, nil, "Restore items by file name or ID") + // permissions restore flag + options.AddRestorePermissionsFlag(c) + // onedrive info flags fs.StringVar( @@ -97,6 +100,9 @@ const ( oneDriveServiceCommandRestoreExamples = `# Restore file with ID 98765abcdef corso restore onedrive --backup 1234abcd-12ab-cd34-56de-1234abcd --file 98765abcdef +# Restore file with ID 98765abcdef along with its associated permissions +corso restore onedrive --backup 1234abcd-12ab-cd34-56de-1234abcd --file 98765abcdef --restore-permissions + # Restore Alice's file named "FY2021 Planning.xlsx in "Documents/Finance Reports" from a specific backup corso restore onedrive --backup 1234abcd-12ab-cd34-56de-1234abcd \ --user alice@example.com --file "FY2021 Planning.xlsx" --folder "Documents/Finance Reports" diff --git a/src/cmd/factory/impl/common.go b/src/cmd/factory/impl/common.go index 0ea6835dd..3ed3831fc 100644 --- a/src/cmd/factory/impl/common.go +++ b/src/cmd/factory/impl/common.go @@ -50,6 +50,7 @@ func generateAndRestoreItems( tenantID, userID, destFldr string, howMany int, dbf dataBuilderFunc, + opts control.Options, ) (*details.Details, error) { items := make([]item, 0, howMany) @@ -74,7 +75,7 @@ func generateAndRestoreItems( items: items, }} - // TODO: fit the desination to the containers + // TODO: fit the destination to the containers dest := control.DefaultRestoreDestination(common.SimpleTimeTesting) dest.ContainerName = destFldr @@ -90,7 +91,7 @@ func generateAndRestoreItems( Infof(ctx, "Generating %d %s items in %s\n", howMany, cat, Destination) - return gc.RestoreDataCollections(ctx, acct, sel, dest, dataColls) + return gc.RestoreDataCollections(ctx, acct, sel, dest, opts, dataColls) } // ------------------------------------------------------------------------------------------ diff --git a/src/cmd/factory/impl/exchange.go b/src/cmd/factory/impl/exchange.go index 26f7eef09..39e3c13a1 100644 --- a/src/cmd/factory/impl/exchange.go +++ b/src/cmd/factory/impl/exchange.go @@ -6,6 +6,7 @@ import ( . "github.com/alcionai/corso/src/cli/print" "github.com/alcionai/corso/src/cli/utils" "github.com/alcionai/corso/src/internal/connector/mockconnector" + "github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/selectors" ) @@ -67,6 +68,7 @@ func handleExchangeEmailFactory(cmd *cobra.Command, args []string) error { subject, body, body, now, now, now, now) }, + control.Options{}, ) if err != nil { return Only(ctx, err) @@ -107,6 +109,7 @@ func handleExchangeCalendarEventFactory(cmd *cobra.Command, args []string) error User, subject, body, body, now, now, false) }, + control.Options{}, ) if err != nil { return Only(ctx, err) @@ -152,6 +155,7 @@ func handleExchangeContactFactory(cmd *cobra.Command, args []string) error { "123-456-7890", ) }, + control.Options{}, ) if err != nil { return Only(ctx, err) diff --git a/src/internal/connector/graph_connector.go b/src/internal/connector/graph_connector.go index 3dbc0e60c..370948639 100644 --- a/src/internal/connector/graph_connector.go +++ b/src/internal/connector/graph_connector.go @@ -269,6 +269,7 @@ func (gc *GraphConnector) RestoreDataCollections( acct account.Account, selector selectors.Selector, dest control.RestoreDestination, + opts control.Options, dcs []data.Collection, ) (*details.Details, error) { ctx, end := D.Span(ctx, "connector:restore") @@ -289,7 +290,7 @@ func (gc *GraphConnector) RestoreDataCollections( case selectors.ServiceExchange: status, err = exchange.RestoreExchangeDataCollections(ctx, creds, gc.Service, dest, dcs, deets) case selectors.ServiceOneDrive: - status, err = onedrive.RestoreCollections(ctx, gc.Service, dest, dcs, deets) + status, err = onedrive.RestoreCollections(ctx, gc.Service, dest, opts, dcs, deets) case selectors.ServiceSharePoint: status, err = sharepoint.RestoreCollections(ctx, gc.Service, dest, dcs, deets) default: diff --git a/src/internal/connector/graph_connector_helper_test.go b/src/internal/connector/graph_connector_helper_test.go index 698ee8527..8a0e22a26 100644 --- a/src/internal/connector/graph_connector_helper_test.go +++ b/src/internal/connector/graph_connector_helper_test.go @@ -2,9 +2,11 @@ package connector import ( "context" + "encoding/json" "io" "net/http" "reflect" + "strings" "testing" "github.com/microsoftgraph/msgraph-sdk-go/models" @@ -14,6 +16,7 @@ import ( "golang.org/x/exp/maps" "github.com/alcionai/corso/src/internal/connector/mockconnector" + "github.com/alcionai/corso/src/internal/connector/onedrive" "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/tester" @@ -645,21 +648,52 @@ func compareOneDriveItem( t *testing.T, expected map[string][]byte, item data.Stream, + restorePermissions bool, ) { + name := item.UUID() + expectedData := expected[item.UUID()] if !assert.NotNil(t, expectedData, "unexpected file with name %s", item.UUID) { return } - // OneDrive items are just byte buffers of the data. Nothing special to - // interpret. May need to do chunked comparisons in the future if we test - // large item equality. buf, err := io.ReadAll(item.ToReader()) if !assert.NoError(t, err) { return } - assert.Equal(t, expectedData, buf) + if !strings.HasSuffix(name, onedrive.MetaFileSuffix) && !strings.HasSuffix(name, onedrive.DirMetaFileSuffix) { + // OneDrive data items are just byte buffers of the data. Nothing special to + // interpret. May need to do chunked comparisons in the future if we test + // large item equality. + assert.Equal(t, expectedData, buf) + return + } + + var ( + itemMeta onedrive.Metadata + expectedMeta onedrive.Metadata + ) + + err = json.Unmarshal(buf, &itemMeta) + assert.Nil(t, err) + + err = json.Unmarshal(expectedData, &expectedMeta) + assert.Nil(t, err) + + if !restorePermissions { + assert.Equal(t, 0, len(itemMeta.Permissions)) + return + } + + assert.Equal(t, len(expectedMeta.Permissions), len(itemMeta.Permissions), "number of permissions after restore") + + // FIXME(meain): The permissions before and after might not be in the same order. + for i, p := range expectedMeta.Permissions { + assert.Equal(t, p.Email, itemMeta.Permissions[i].Email) + assert.Equal(t, p.Roles, itemMeta.Permissions[i].Roles) + assert.Equal(t, p.Expiration, itemMeta.Permissions[i].Expiration) + } } func compareItem( @@ -668,6 +702,7 @@ func compareItem( service path.ServiceType, category path.CategoryType, item data.Stream, + restorePermissions bool, ) { if mt, ok := item.(data.StreamModTime); ok { assert.NotZero(t, mt.ModTime()) @@ -687,7 +722,7 @@ func compareItem( } case path.OneDriveService: - compareOneDriveItem(t, expected, item) + compareOneDriveItem(t, expected, item, restorePermissions) default: assert.FailNowf(t, "unexpected service: %s", service.String()) @@ -720,6 +755,7 @@ func checkCollections( expectedItems int, expected map[string]map[string][]byte, got []data.Collection, + restorePermissions bool, ) int { collectionsWithItems := []data.Collection{} @@ -754,7 +790,7 @@ func checkCollections( continue } - compareItem(t, expectedColData, service, category, item) + compareItem(t, expectedColData, service, category, item, restorePermissions) } if gotItems != startingItems { @@ -906,10 +942,11 @@ func collectionsForInfo( tenant, user string, dest control.RestoreDestination, allInfo []colInfo, -) (int, []data.Collection, map[string]map[string][]byte) { +) (int, int, []data.Collection, map[string]map[string][]byte) { collections := make([]data.Collection, 0, len(allInfo)) expectedData := make(map[string]map[string][]byte, len(allInfo)) totalItems := 0 + kopiaEntries := 0 for _, info := range allInfo { pth := mustToDataLayerPath( @@ -935,13 +972,20 @@ func collectionsForInfo( c.Data[i] = info.items[i].data baseExpected[info.items[i].lookupKey] = info.items[i].data + + // We do not count metadata files against item count + if service != path.OneDriveService || + (service == path.OneDriveService && + strings.HasSuffix(info.items[i].name, onedrive.DataFileSuffix)) { + totalItems++ + } } collections = append(collections, c) - totalItems += len(info.items) + kopiaEntries += len(info.items) } - return totalItems, collections, expectedData + return totalItems, kopiaEntries, collections, expectedData } //nolint:deadcode diff --git a/src/internal/connector/graph_connector_test.go b/src/internal/connector/graph_connector_test.go index be1439c35..2c2280e37 100644 --- a/src/internal/connector/graph_connector_test.go +++ b/src/internal/connector/graph_connector_test.go @@ -2,6 +2,8 @@ package connector import ( "context" + "encoding/base64" + "encoding/json" "strings" "testing" "time" @@ -15,6 +17,7 @@ import ( "github.com/alcionai/corso/src/internal/connector/discovery/api" "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/mockconnector" + "github.com/alcionai/corso/src/internal/connector/onedrive" "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/tester" @@ -135,9 +138,10 @@ func (suite *GraphConnectorUnitSuite) TestUnionSiteIDsAndWebURLs() { type GraphConnectorIntegrationSuite struct { suite.Suite - connector *GraphConnector - user string - acct account.Account + connector *GraphConnector + user string + secondaryUser string + acct account.Account } func TestGraphConnectorIntegrationSuite(t *testing.T) { @@ -158,6 +162,7 @@ func (suite *GraphConnectorIntegrationSuite) SetupSuite() { suite.connector = loadConnector(ctx, suite.T(), graph.HTTPClient(graph.NoTimeout()), Users) suite.user = tester.M365UserID(suite.T()) + suite.secondaryUser = tester.SecondaryM365UserID(suite.T()) suite.acct = tester.NewM365Account(suite.T()) tester.LogTimeOfTest(suite.T()) @@ -226,7 +231,7 @@ func (suite *GraphConnectorIntegrationSuite) TestRestoreFailsBadService() { } ) - deets, err := suite.connector.RestoreDataCollections(ctx, acct, sel, dest, nil) + deets, err := suite.connector.RestoreDataCollections(ctx, acct, sel, dest, control.Options{}, nil) assert.Error(t, err) assert.NotNil(t, deets) @@ -297,7 +302,9 @@ func (suite *GraphConnectorIntegrationSuite) TestEmptyCollections() { suite.acct, test.sel, dest, - test.col) + control.Options{RestorePermissions: true}, + test.col, + ) require.NoError(t, err) assert.NotNil(t, deets) @@ -344,11 +351,13 @@ func runRestoreBackupTest( test restoreBackupInfo, tenant string, resourceOwners []string, + opts control.Options, ) { var ( - collections []data.Collection - expectedData = map[string]map[string][]byte{} - totalItems = 0 + collections []data.Collection + expectedData = map[string]map[string][]byte{} + totalItems = 0 + totalKopiaItems = 0 // Get a dest per test so they're independent. dest = tester.DefaultTestRestoreDestination() ) @@ -357,7 +366,7 @@ func runRestoreBackupTest( defer flush() for _, owner := range resourceOwners { - numItems, ownerCollections, userExpectedData := collectionsForInfo( + numItems, kopiaItems, ownerCollections, userExpectedData := collectionsForInfo( t, test.service, tenant, @@ -368,6 +377,7 @@ func runRestoreBackupTest( collections = append(collections, ownerCollections...) totalItems += numItems + totalKopiaItems += kopiaItems maps.Copy(expectedData, userExpectedData) } @@ -386,7 +396,9 @@ func runRestoreBackupTest( acct, restoreSel, dest, - collections) + opts, + collections, + ) require.NoError(t, err) assert.NotNil(t, deets) @@ -425,7 +437,7 @@ func runRestoreBackupTest( t.Logf("Selective backup of %s\n", backupSel) start = time.Now() - dcs, excludes, err := backupGC.DataCollections(ctx, backupSel, nil, control.Options{}) + dcs, excludes, err := backupGC.DataCollections(ctx, backupSel, nil, control.Options{RestorePermissions: true}) require.NoError(t, err) // No excludes yet because this isn't an incremental backup. assert.Empty(t, excludes) @@ -434,7 +446,7 @@ func runRestoreBackupTest( // Pull the data prior to waiting for the status as otherwise it will // deadlock. - skipped := checkCollections(t, totalItems, expectedData, dcs) + skipped := checkCollections(t, totalKopiaItems, expectedData, dcs, opts.RestorePermissions) status = backupGC.AwaitStatus() @@ -446,6 +458,20 @@ func runRestoreBackupTest( "backup status.Successful; wanted %d items + %d skipped", totalItems, skipped) } +func getTestMetaJSON(t *testing.T, user string, roles []string) []byte { + id := base64.StdEncoding.EncodeToString([]byte(user + strings.Join(roles, "+"))) + testMeta := onedrive.Metadata{Permissions: []onedrive.UserPermission{ + {ID: id, Roles: roles, Email: user}, + }} + + testMetaJSON, err := json.Marshal(testMeta) + if err != nil { + t.Fatal("unable to marshall test permissions", err) + } + + return testMetaJSON +} + func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackup() { bodyText := "This email has some text. However, all the text is on the same line." subjectText := "Test message for restore" @@ -564,7 +590,7 @@ func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackup() { }, }, { - name: "MultipleContactsMutlipleFolders", + name: "MultipleContactsMultipleFolders", service: path.ExchangeService, resource: Users, collections: []colInfo{ @@ -691,9 +717,24 @@ func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackup() { category: path.FilesCategory, items: []itemInfo{ { - name: "test-file.txt", + name: "test-file.txt" + onedrive.DataFileSuffix, data: []byte(strings.Repeat("a", 33)), - lookupKey: "test-file.txt", + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: []byte("{}"), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + { + name: "folder-a" + onedrive.DirMetaFileSuffix, + data: []byte("{}"), + lookupKey: "folder-a" + onedrive.DirMetaFileSuffix, + }, + { + name: "b" + onedrive.DirMetaFileSuffix, + data: []byte("{}"), + lookupKey: "b" + onedrive.DirMetaFileSuffix, }, }, }, @@ -707,9 +748,19 @@ func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackup() { category: path.FilesCategory, items: []itemInfo{ { - name: "test-file.txt", + name: "test-file.txt" + onedrive.DataFileSuffix, data: []byte(strings.Repeat("b", 65)), - lookupKey: "test-file.txt", + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: []byte("{}"), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + { + name: "b" + onedrive.DirMetaFileSuffix, + data: []byte("{}"), + lookupKey: "b" + onedrive.DirMetaFileSuffix, }, }, }, @@ -724,9 +775,19 @@ func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackup() { category: path.FilesCategory, items: []itemInfo{ { - name: "test-file.txt", + name: "test-file.txt" + onedrive.DataFileSuffix, data: []byte(strings.Repeat("c", 129)), - lookupKey: "test-file.txt", + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: []byte("{}"), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + { + name: "folder-a" + onedrive.DirMetaFileSuffix, + data: []byte("{}"), + lookupKey: "folder-a" + onedrive.DirMetaFileSuffix, }, }, }, @@ -742,9 +803,14 @@ func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackup() { category: path.FilesCategory, items: []itemInfo{ { - name: "test-file.txt", + name: "test-file.txt" + onedrive.DataFileSuffix, data: []byte(strings.Repeat("d", 257)), - lookupKey: "test-file.txt", + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: []byte("{}"), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, }, }, }, @@ -758,9 +824,67 @@ func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackup() { category: path.FilesCategory, items: []itemInfo{ { - name: "test-file.txt", + name: "test-file.txt" + onedrive.DataFileSuffix, data: []byte(strings.Repeat("e", 257)), - lookupKey: "test-file.txt", + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: []byte("{}"), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + }, + }, + }, + }, + { + name: "OneDriveFoldersAndFilesWithMetadata", + service: path.OneDriveService, + resource: Users, + collections: []colInfo{ + { + pathElements: []string{ + "drives", + driveID, + "root:", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt" + onedrive.DataFileSuffix, + data: []byte(strings.Repeat("a", 33)), + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: getTestMetaJSON(suite.T(), suite.secondaryUser, []string{"write"}), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + { + name: "b" + onedrive.DirMetaFileSuffix, + data: getTestMetaJSON(suite.T(), suite.secondaryUser, []string{"read"}), + lookupKey: "b" + onedrive.DirMetaFileSuffix, + }, + }, + }, + { + pathElements: []string{ + "drives", + driveID, + "root:", + "b", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt" + onedrive.DataFileSuffix, + data: []byte(strings.Repeat("e", 66)), + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: getTestMetaJSON(suite.T(), suite.secondaryUser, []string{"read"}), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, }, }, }, @@ -770,7 +894,14 @@ func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackup() { for _, test := range table { suite.T().Run(test.name, func(t *testing.T) { - runRestoreBackupTest(t, suite.acct, test, suite.connector.tenant, []string{suite.user}) + runRestoreBackupTest( + t, + suite.acct, + test, + suite.connector.tenant, + []string{suite.user}, + control.Options{RestorePermissions: true}, + ) }) } } @@ -857,7 +988,7 @@ func (suite *GraphConnectorIntegrationSuite) TestMultiFolderBackupDifferentNames }, }) - totalItems, collections, expectedData := collectionsForInfo( + totalItems, _, collections, expectedData := collectionsForInfo( t, test.service, suite.connector.tenant, @@ -879,7 +1010,14 @@ func (suite *GraphConnectorIntegrationSuite) TestMultiFolderBackupDifferentNames ) restoreGC := loadConnector(ctx, t, graph.HTTPClient(graph.NoTimeout()), test.resource) - deets, err := restoreGC.RestoreDataCollections(ctx, suite.acct, restoreSel, dest, collections) + deets, err := restoreGC.RestoreDataCollections( + ctx, + suite.acct, + restoreSel, + dest, + control.Options{RestorePermissions: true}, + collections, + ) require.NoError(t, err) require.NotNil(t, deets) @@ -900,7 +1038,7 @@ func (suite *GraphConnectorIntegrationSuite) TestMultiFolderBackupDifferentNames backupSel := backupSelectorForExpected(t, test.service, expectedDests) t.Log("Selective backup of", backupSel) - dcs, excludes, err := backupGC.DataCollections(ctx, backupSel, nil, control.Options{}) + dcs, excludes, err := backupGC.DataCollections(ctx, backupSel, nil, control.Options{RestorePermissions: true}) require.NoError(t, err) // No excludes yet because this isn't an incremental backup. assert.Empty(t, excludes) @@ -909,7 +1047,7 @@ func (suite *GraphConnectorIntegrationSuite) TestMultiFolderBackupDifferentNames // Pull the data prior to waiting for the status as otherwise it will // deadlock. - skipped := checkCollections(t, allItems, allExpectedData, dcs) + skipped := checkCollections(t, allItems, allExpectedData, dcs, true) status := backupGC.AwaitStatus() assert.Equal(t, allItems+skipped, status.ObjectCount, "status.ObjectCount") @@ -918,6 +1056,313 @@ func (suite *GraphConnectorIntegrationSuite) TestMultiFolderBackupDifferentNames } } +func (suite *GraphConnectorIntegrationSuite) TestPermissionsRestoreAndBackup() { + ctx, flush := tester.NewContext() + defer flush() + + // Get the default drive ID for the test user. + driveID := mustGetDefaultDriveID( + suite.T(), + ctx, + suite.connector.Service, + suite.user, + ) + + table := []restoreBackupInfo{ + { + name: "FilePermissionsResote", + service: path.OneDriveService, + resource: Users, + collections: []colInfo{ + { + pathElements: []string{ + "drives", + driveID, + "root:", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt" + onedrive.DataFileSuffix, + data: []byte(strings.Repeat("a", 33)), + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: getTestMetaJSON(suite.T(), suite.secondaryUser, []string{"write"}), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + }, + }, + }, + }, + + { + name: "FileInsideFolderPermissionsRestore", + service: path.OneDriveService, + resource: Users, + collections: []colInfo{ + { + pathElements: []string{ + "drives", + driveID, + "root:", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt" + onedrive.DataFileSuffix, + data: []byte(strings.Repeat("a", 33)), + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: []byte("{}"), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + { + name: "b" + onedrive.DirMetaFileSuffix, + data: []byte("{}"), + lookupKey: "b" + onedrive.DirMetaFileSuffix, + }, + }, + }, + { + pathElements: []string{ + "drives", + driveID, + "root:", + "b", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt" + onedrive.DataFileSuffix, + data: []byte(strings.Repeat("e", 66)), + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: getTestMetaJSON(suite.T(), suite.secondaryUser, []string{"read"}), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + }, + }, + }, + }, + + { + name: "FileAndFolderPermissionsResote", + service: path.OneDriveService, + resource: Users, + collections: []colInfo{ + { + pathElements: []string{ + "drives", + driveID, + "root:", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt" + onedrive.DataFileSuffix, + data: []byte(strings.Repeat("a", 33)), + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: getTestMetaJSON(suite.T(), suite.secondaryUser, []string{"write"}), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + { + name: "b" + onedrive.DirMetaFileSuffix, + data: getTestMetaJSON(suite.T(), suite.secondaryUser, []string{"read"}), + lookupKey: "b" + onedrive.DirMetaFileSuffix, + }, + }, + }, + { + pathElements: []string{ + "drives", + driveID, + "root:", + "b", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt" + onedrive.DataFileSuffix, + data: []byte(strings.Repeat("e", 66)), + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: getTestMetaJSON(suite.T(), suite.secondaryUser, []string{"read"}), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + }, + }, + }, + }, + + { + name: "FileAndFolderSeparatePermissionsResote", + service: path.OneDriveService, + resource: Users, + collections: []colInfo{ + { + pathElements: []string{ + "drives", + driveID, + "root:", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "b" + onedrive.DirMetaFileSuffix, + data: getTestMetaJSON(suite.T(), suite.secondaryUser, []string{"read"}), + lookupKey: "b" + onedrive.DirMetaFileSuffix, + }, + }, + }, + { + pathElements: []string{ + "drives", + driveID, + "root:", + "b", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt" + onedrive.DataFileSuffix, + data: []byte(strings.Repeat("e", 66)), + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: getTestMetaJSON(suite.T(), suite.secondaryUser, []string{"write"}), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + }, + }, + }, + }, + + { + name: "FolderAndNoChildPermissionsResote", + service: path.OneDriveService, + resource: Users, + collections: []colInfo{ + { + pathElements: []string{ + "drives", + driveID, + "root:", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "b" + onedrive.DirMetaFileSuffix, + data: getTestMetaJSON(suite.T(), suite.secondaryUser, []string{"read"}), + lookupKey: "b" + onedrive.DirMetaFileSuffix, + }, + }, + }, + { + pathElements: []string{ + "drives", + driveID, + "root:", + "b", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt" + onedrive.DataFileSuffix, + data: []byte(strings.Repeat("e", 66)), + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: []byte("{}"), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + }, + }, + }, + }, + } + + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + runRestoreBackupTest(t, + suite.acct, + test, + suite.connector.tenant, + []string{suite.user}, + control.Options{RestorePermissions: true}, + ) + }) + } +} + +func (suite *GraphConnectorIntegrationSuite) TestPermissionsBackupAndNoRestore() { + ctx, flush := tester.NewContext() + defer flush() + + // Get the default drive ID for the test user. + driveID := mustGetDefaultDriveID( + suite.T(), + ctx, + suite.connector.Service, + suite.user, + ) + + table := []restoreBackupInfo{ + { + name: "FilePermissionsResote", + service: path.OneDriveService, + resource: Users, + collections: []colInfo{ + { + pathElements: []string{ + "drives", + driveID, + "root:", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt" + onedrive.DataFileSuffix, + data: []byte(strings.Repeat("a", 33)), + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: getTestMetaJSON(suite.T(), suite.secondaryUser, []string{"write"}), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + }, + }, + }, + }, + } + + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + runRestoreBackupTest( + t, + suite.acct, + test, + suite.connector.tenant, + []string{suite.user}, + control.Options{RestorePermissions: false}, + ) + }) + } +} + // TODO: this should only be run during smoke tests, not part of the standard CI. // That's why it's set aside instead of being included in the other test set. func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackup_largeMailAttachment() { @@ -942,5 +1387,12 @@ func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackup_largeMailAttac }, } - runRestoreBackupTest(suite.T(), suite.acct, test, suite.connector.tenant, []string{suite.user}) + runRestoreBackupTest( + suite.T(), + suite.acct, + test, + suite.connector.tenant, + []string{suite.user}, + control.Options{RestorePermissions: true}, + ) } diff --git a/src/internal/connector/onedrive/collection.go b/src/internal/connector/onedrive/collection.go index c4e1825bd..3b8ff5cbb 100644 --- a/src/internal/connector/onedrive/collection.go +++ b/src/internal/connector/onedrive/collection.go @@ -5,6 +5,7 @@ import ( "context" "io" "net/http" + "strings" "sync" "sync/atomic" "time" @@ -34,6 +35,10 @@ const ( // Max number of retries to get doc from M365 // Seems to timeout at times because of multiple requests maxRetries = 4 // 1 + 3 retries + + MetaFileSuffix = ".meta" + DirMetaFileSuffix = ".dirmeta" + DataFileSuffix = ".data" ) var ( @@ -56,12 +61,13 @@ type Collection struct { // M365 IDs of file items within this collection driveItems map[string]models.DriveItemable // M365 ID of the drive this collection was created from - driveID string - source driveSource - service graph.Servicer - statusUpdater support.StatusUpdater - itemReader itemReaderFunc - ctrl control.Options + driveID string + source driveSource + service graph.Servicer + statusUpdater support.StatusUpdater + itemReader itemReaderFunc + itemMetaReader itemMetaReaderFunc + ctrl control.Options // should only be true if the old delta token expired doNotMergeItems bool @@ -73,6 +79,15 @@ type itemReaderFunc func( item models.DriveItemable, ) (itemInfo details.ItemInfo, itemData io.ReadCloser, err error) +// itemMetaReaderFunc returns a reader for the metadata of the +// specified item +type itemMetaReaderFunc func( + ctx context.Context, + service graph.Servicer, + driveID string, + item models.DriveItemable, +) (io.ReadCloser, int, error) + // NewCollection creates a Collection func NewCollection( itemClient *http.Client, @@ -101,6 +116,7 @@ func NewCollection( c.itemReader = sharePointItemReader default: c.itemReader = oneDriveItemReader + c.itemMetaReader = oneDriveItemMetaReader } return c @@ -138,6 +154,21 @@ func (oc Collection) DoNotMergeItems() bool { return oc.doNotMergeItems } +// FilePermission is used to store permissions of a specific user to a +// OneDrive item. +type UserPermission struct { + ID string `json:"id,omitempty"` + Roles []string `json:"role,omitempty"` + Email string `json:"email,omitempty"` + Expiration *time.Time `json:"expiration,omitempty"` +} + +// ItemMeta contains metadata about the Item. It gets stored in a +// separate file in kopia +type Metadata struct { + Permissions []UserPermission `json:"permissions,omitempty"` +} + // Item represents a single item retrieved from OneDrive type Item struct { id string @@ -173,18 +204,21 @@ func (od *Item) ModTime() time.Time { // and uses the collection `itemReader` to read the item func (oc *Collection) populateItems(ctx context.Context) { var ( - errs error - byteCount int64 - itemsRead int64 - wg sync.WaitGroup - m sync.Mutex + errs error + byteCount int64 + itemsRead int64 + dirsRead int64 + itemsFound int64 + dirsFound int64 + wg sync.WaitGroup + m sync.Mutex ) // Retrieve the OneDrive folder path to set later in // `details.OneDriveInfo` parentPathString, err := path.GetDriveFolderPath(oc.folderPath) if err != nil { - oc.reportAsCompleted(ctx, 0, 0, err) + oc.reportAsCompleted(ctx, 0, 0, 0, err) return } @@ -205,16 +239,11 @@ func (oc *Collection) populateItems(ctx context.Context) { m.Unlock() } - for id, item := range oc.driveItems { + for _, item := range oc.driveItems { if oc.ctrl.FailFast && errs != nil { break } - if item == nil { - errUpdater(id, errors.New("nil item")) - continue - } - semaphoreCh <- struct{}{} wg.Add(1) @@ -223,13 +252,61 @@ func (oc *Collection) populateItems(ctx context.Context) { defer wg.Done() defer func() { <-semaphoreCh }() + // Read the item var ( - itemID = *item.GetId() - itemName = *item.GetName() - itemSize = *item.GetSize() - itemInfo details.ItemInfo + itemID = *item.GetId() + itemName = *item.GetName() + itemSize = *item.GetSize() + itemInfo details.ItemInfo + itemMeta io.ReadCloser + itemMetaSize int + metaSuffix string + err error ) + isFile := item.GetFile() != nil + + if isFile { + atomic.AddInt64(&itemsFound, 1) + + metaSuffix = MetaFileSuffix + } else { + atomic.AddInt64(&dirsFound, 1) + + metaSuffix = DirMetaFileSuffix + } + + if oc.source == OneDriveSource { + // Fetch metadata for the file + for i := 1; i <= maxRetries; i++ { + if oc.ctrl.ToggleFeatures.DisablePermissionsBackup { + // We are still writing the metadata file but with + // empty permissions as we are not sure how the + // restore will be called. + itemMeta = io.NopCloser(strings.NewReader("{}")) + itemMetaSize = 2 + + break + } + + itemMeta, itemMetaSize, err = oc.itemMetaReader(ctx, oc.service, oc.driveID, item) + + // retry on Timeout type errors, break otherwise. + if err == nil || !graph.IsErrTimeout(err) { + break + } + + if i < maxRetries { + time.Sleep(1 * time.Second) + } + } + + if err != nil { + errUpdater(*item.GetId(), err) + return + } + } + switch oc.source { case SharePointSource: itemInfo.SharePoint = sharePointItemInfo(item, itemSize) @@ -239,101 +316,127 @@ func (oc *Collection) populateItems(ctx context.Context) { itemInfo.OneDrive.ParentPath = parentPathString } - // Construct a new lazy readCloser to feed to the collection consumer. - // This ensures that downloads won't be attempted unless that consumer - // attempts to read bytes. Assumption is that kopia will check things - // like file modtimes before attempting to read. - itemReader := lazy.NewLazyReadCloser(func() (io.ReadCloser, error) { - // Read the item - var ( - itemData io.ReadCloser - err error - ) + if isFile { + dataSuffix := "" + if oc.source == OneDriveSource { + dataSuffix = DataFileSuffix + } - for i := 1; i <= maxRetries; i++ { - _, itemData, err = oc.itemReader(oc.itemClient, item) - if err == nil { - break - } + // Construct a new lazy readCloser to feed to the collection consumer. + // This ensures that downloads won't be attempted unless that consumer + // attempts to read bytes. Assumption is that kopia will check things + // like file modtimes before attempting to read. + itemReader := lazy.NewLazyReadCloser(func() (io.ReadCloser, error) { + // Read the item + var ( + itemData io.ReadCloser + err error + ) - if graph.IsErrUnauthorized(err) { - // assume unauthorized requests are a sign of an expired - // jwt token, and that we've overrun the available window - // to download the actual file. Re-downloading the item - // will refresh that download url. - di, diErr := getDriveItem(ctx, oc.service, oc.driveID, itemID) - if diErr != nil { - err = errors.Wrap(diErr, "retrieving expired item") + for i := 1; i <= maxRetries; i++ { + _, itemData, err = oc.itemReader(oc.itemClient, item) + if err == nil { break } - item = di + if graph.IsErrUnauthorized(err) { + // assume unauthorized requests are a sign of an expired + // jwt token, and that we've overrun the available window + // to download the actual file. Re-downloading the item + // will refresh that download url. + di, diErr := getDriveItem(ctx, oc.service, oc.driveID, itemID) + if diErr != nil { + err = errors.Wrap(diErr, "retrieving expired item") + break + } - continue + item = di - } else if !graph.IsErrTimeout(err) && - !graph.IsInternalServerError(err) { - // Don't retry for non-timeout, on-unauth, as - // we are already retrying it in the default - // retry middleware - break + continue + + } else if !graph.IsErrTimeout(err) && + !graph.IsInternalServerError(err) { + // Don't retry for non-timeout, on-unauth, as + // we are already retrying it in the default + // retry middleware + break + } + + if i < maxRetries { + time.Sleep(1 * time.Second) + } } - if i < maxRetries { - time.Sleep(1 * time.Second) + // check for errors following retries + if err != nil { + errUpdater(itemID, err) + return nil, err } + + // display/log the item download + progReader, closer := observe.ItemProgress( + ctx, + itemData, + observe.ItemBackupMsg, + observe.PII(itemName+dataSuffix), + itemSize, + ) + go closer() + + return progReader, nil + }) + + oc.data <- &Item{ + id: itemName + dataSuffix, + data: itemReader, + info: itemInfo, } + } - // check for errors following retries - if err != nil { - errUpdater(itemID, err) - return nil, err + if oc.source == OneDriveSource { + metaReader := lazy.NewLazyReadCloser(func() (io.ReadCloser, error) { + progReader, closer := observe.ItemProgress( + ctx, itemMeta, observe.ItemBackupMsg, + observe.PII(itemName+metaSuffix), int64(itemMetaSize)) + go closer() + return progReader, nil + }) + + oc.data <- &Item{ + id: itemName + metaSuffix, + data: metaReader, + info: itemInfo, } + } - // display/log the item download - progReader, closer := observe.ItemProgress(ctx, itemData, observe.ItemBackupMsg, observe.PII(itemName), itemSize) - go closer() - - return progReader, nil - }) - - // This can cause inaccurate counts. Right now it counts all the items - // we intend to read. Errors within the lazy readCloser will create a - // conflict: an item is both successful and erroneous. But the async - // control to fix that is more error-prone than helpful. - // - // TODO: transform this into a stats bus so that async control of stats - // aggregation is handled at the backup level, not at the item iteration - // level. - // // Item read successfully, add to collection - atomic.AddInt64(&itemsRead, 1) + if isFile { + atomic.AddInt64(&itemsRead, 1) + } else { + atomic.AddInt64(&dirsRead, 1) + } + // byteCount iteration atomic.AddInt64(&byteCount, itemSize) - oc.data <- &Item{ - id: itemName, - data: itemReader, - info: itemInfo, - } folderProgress <- struct{}{} }(item) } wg.Wait() - oc.reportAsCompleted(ctx, int(itemsRead), byteCount, errs) + oc.reportAsCompleted(ctx, int(itemsFound), int(itemsRead), byteCount, errs) } -func (oc *Collection) reportAsCompleted(ctx context.Context, itemsRead int, byteCount int64, errs error) { +func (oc *Collection) reportAsCompleted(ctx context.Context, itemsFound, itemsRead int, byteCount int64, errs error) { close(oc.data) status := support.CreateStatus(ctx, support.Backup, 1, // num folders (always 1) support.CollectionMetrics{ - Objects: len(oc.driveItems), // items to read, - Successes: itemsRead, // items read successfully, - TotalBytes: byteCount, // Number of bytes read in the operation, + Objects: itemsFound, // items to read, + Successes: itemsRead, // items read successfully, + TotalBytes: byteCount, // Number of bytes read in the operation, }, errs, oc.folderPath.Folder(), // Additional details diff --git a/src/internal/connector/onedrive/collection_test.go b/src/internal/connector/onedrive/collection_test.go index b608e9068..b8e5fe446 100644 --- a/src/internal/connector/onedrive/collection_test.go +++ b/src/internal/connector/onedrive/collection_test.go @@ -2,8 +2,11 @@ package onedrive import ( "bytes" + "context" + "encoding/json" "io" "net/http" + "strings" "sync" "testing" "time" @@ -60,6 +63,14 @@ func (suite *CollectionUnitTestSuite) TestCollection() { testItemName = "itemName" testItemData = []byte("testdata") now = time.Now() + testItemMeta = Metadata{Permissions: []UserPermission{ + { + ID: "testMetaID", + Roles: []string{"read", "write"}, + Email: "email@provider.com", + Expiration: &now, + }, + }} ) type nst struct { @@ -164,6 +175,7 @@ func (suite *CollectionUnitTestSuite) TestCollection() { // Set a item reader, add an item and validate we get the item back mockItem := models.NewDriveItem() mockItem.SetId(&testItemID) + mockItem.SetFile(models.NewFile()) mockItem.SetName(&test.itemDeets.name) mockItem.SetSize(&test.itemDeets.size) mockItem.SetCreatedDateTime(&test.itemDeets.time) @@ -174,6 +186,18 @@ func (suite *CollectionUnitTestSuite) TestCollection() { } coll.itemReader = test.itemReader + coll.itemMetaReader = func(_ context.Context, + _ graph.Servicer, + _ string, + _ models.DriveItemable, + ) (io.ReadCloser, int, error) { + metaJSON, err := json.Marshal(testItemMeta) + if err != nil { + return nil, 0, err + } + + return io.NopCloser(bytes.NewReader(metaJSON)), len(metaJSON), nil + } // Read items from the collection wg.Add(1) @@ -184,28 +208,54 @@ func (suite *CollectionUnitTestSuite) TestCollection() { wg.Wait() + if test.source == OneDriveSource { + require.Len(t, readItems, 2) // .data and .meta + } else { + require.Len(t, readItems, 1) + } + + // Expect only 1 item + require.Equal(t, 1, collStatus.ObjectCount) + require.Equal(t, 1, collStatus.Successful) + // Validate item info and data readItem := readItems[0] readItemInfo := readItem.(data.StreamInfo) - readData, err := io.ReadAll(readItem.ToReader()) - require.NoError(t, err) - assert.Equal(t, testItemData, readData) - - // Expect only 1 item - require.Len(t, readItems, 1) - require.Equal(t, 1, collStatus.ObjectCount, "items iterated") - require.Equal(t, 1, collStatus.Successful, "items successful") - - assert.Equal(t, testItemName, readItem.UUID()) + if test.source == OneDriveSource { + assert.Equal(t, testItemName+DataFileSuffix, readItem.UUID()) + } else { + assert.Equal(t, testItemName, readItem.UUID()) + } require.Implements(t, (*data.StreamModTime)(nil), readItem) mt := readItem.(data.StreamModTime) assert.Equal(t, now, mt.ModTime()) + readData, err := io.ReadAll(readItem.ToReader()) + require.NoError(t, err) + name, parentPath := test.infoFrom(t, readItemInfo.Info()) + + assert.Equal(t, testItemData, readData) assert.Equal(t, testItemName, name) assert.Equal(t, driveFolderPath, parentPath) + + if test.source == OneDriveSource { + readItemMeta := readItems[1] + + assert.Equal(t, testItemName+MetaFileSuffix, readItemMeta.UUID()) + + readMetaData, err := io.ReadAll(readItemMeta.ToReader()) + require.NoError(t, err) + + tm, err := json.Marshal(testItemMeta) + if err != nil { + t.Fatal("unable to marshall test permissions", err) + } + + assert.Equal(t, tm, readMetaData) + } }) } } @@ -255,6 +305,7 @@ func (suite *CollectionUnitTestSuite) TestCollectionReadError() { mockItem := models.NewDriveItem() mockItem.SetId(&testItemID) + mockItem.SetFile(models.NewFile()) mockItem.SetName(&name) mockItem.SetSize(&size) mockItem.SetCreatedDateTime(&now) @@ -265,6 +316,14 @@ func (suite *CollectionUnitTestSuite) TestCollectionReadError() { return details.ItemInfo{}, nil, assert.AnError } + coll.itemMetaReader = func(_ context.Context, + _ graph.Servicer, + _ string, + _ models.DriveItemable, + ) (io.ReadCloser, int, error) { + return io.NopCloser(strings.NewReader(`{}`)), 2, nil + } + collItem, ok := <-coll.Items() assert.True(t, ok) @@ -279,3 +338,87 @@ func (suite *CollectionUnitTestSuite) TestCollectionReadError() { }) } } + +func (suite *CollectionUnitTestSuite) TestCollectionDisablePermissionsBackup() { + table := []struct { + name string + source driveSource + }{ + { + name: "oneDrive", + source: OneDriveSource, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + var ( + testItemID = "fakeItemID" + testItemName = "Fake Item" + testItemSize = int64(10) + + collStatus = support.ConnectorOperationStatus{} + wg = sync.WaitGroup{} + ) + + wg.Add(1) + + folderPath, err := GetCanonicalPath("drive/driveID1/root:/folderPath", "a-tenant", "a-user", test.source) + require.NoError(t, err) + + coll := NewCollection( + graph.HTTPClient(graph.NoTimeout()), + folderPath, + "fakeDriveID", + suite, + suite.testStatusUpdater(&wg, &collStatus), + test.source, + control.Options{ToggleFeatures: control.Toggles{DisablePermissionsBackup: true}}) + + now := time.Now() + mockItem := models.NewDriveItem() + mockItem.SetFile(models.NewFile()) + mockItem.SetId(&testItemID) + mockItem.SetName(&testItemName) + mockItem.SetSize(&testItemSize) + mockItem.SetCreatedDateTime(&now) + mockItem.SetLastModifiedDateTime(&now) + coll.Add(mockItem) + + coll.itemReader = func( + *http.Client, + models.DriveItemable, + ) (details.ItemInfo, io.ReadCloser, error) { + return details.ItemInfo{OneDrive: &details.OneDriveInfo{ItemName: "fakeName", Modified: time.Now()}}, + io.NopCloser(strings.NewReader("Fake Data!")), + nil + } + + coll.itemMetaReader = func(_ context.Context, + _ graph.Servicer, + _ string, + _ models.DriveItemable, + ) (io.ReadCloser, int, error) { + return io.NopCloser(strings.NewReader(`{"key": "value"}`)), 16, nil + } + + readItems := []data.Stream{} + for item := range coll.Items() { + readItems = append(readItems, item) + } + + wg.Wait() + + // Expect no items + require.Equal(t, 1, collStatus.ObjectCount) + require.Equal(t, 1, collStatus.Successful) + + for _, i := range readItems { + if strings.HasSuffix(i.UUID(), MetaFileSuffix) { + content, err := io.ReadAll(i.ToReader()) + require.NoError(t, err) + require.Equal(t, content, []byte("{}")) + } + } + }) + } +} diff --git a/src/internal/connector/onedrive/collections.go b/src/internal/connector/onedrive/collections.go index 200e51e23..50c5323d9 100644 --- a/src/internal/connector/onedrive/collections.go +++ b/src/internal/connector/onedrive/collections.go @@ -430,6 +430,12 @@ func (c *Collections) UpdateCollections( // already created and partially populated. updatePath(newPaths, *item.GetId(), folderPath.String()) + if c.source != OneDriveSource { + continue + } + + fallthrough + case item.GetFile() != nil: if item.GetDeleted() != nil { excluded[*item.GetId()] = struct{}{} @@ -445,6 +451,7 @@ func (c *Collections) UpdateCollections( // the exclude list. col, found := c.CollectionMap[collectionPath.String()] + if !found { // TODO(ashmrtn): Compare old and new path and set collection state // accordingly. @@ -459,13 +466,17 @@ func (c *Collections) UpdateCollections( c.CollectionMap[collectionPath.String()] = col c.NumContainers++ - c.NumItems++ } collection := col.(*Collection) collection.Add(item) - c.NumFiles++ + c.NumItems++ + if item.GetFile() != nil { + // This is necessary as we have a fallthrough for + // folders and packages + c.NumFiles++ + } default: return errors.Errorf("item type not supported. item name : %s", *item.GetName()) diff --git a/src/internal/connector/onedrive/collections_test.go b/src/internal/connector/onedrive/collections_test.go index 21dae9549..3316a10c5 100644 --- a/src/internal/connector/onedrive/collections_test.go +++ b/src/internal/connector/onedrive/collections_test.go @@ -139,7 +139,7 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { user, testBaseDrivePath, ), - expectedItemCount: 2, + expectedItemCount: 1, expectedFileCount: 1, expectedContainerCount: 1, // Root folder is skipped since it's always present. @@ -151,10 +151,15 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { items: []models.DriveItemable{ driveItem("folder", "folder", testBaseDrivePath, false, true, false), }, - inputFolderMap: map[string]string{}, - scope: anyFolder, - expect: assert.NoError, - expectedCollectionPaths: []string{}, + inputFolderMap: map[string]string{}, + scope: anyFolder, + expect: assert.NoError, + expectedCollectionPaths: expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath, + ), expectedMetadataPaths: map[string]string{ "folder": expectedPathAsSlice( suite.T(), @@ -163,17 +168,24 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { testBaseDrivePath+"/folder", )[0], }, - expectedExcludes: map[string]struct{}{}, + expectedItemCount: 1, + expectedContainerCount: 1, + expectedExcludes: map[string]struct{}{}, }, { testCase: "Single Package", items: []models.DriveItemable{ driveItem("package", "package", testBaseDrivePath, false, false, true), }, - inputFolderMap: map[string]string{}, - scope: anyFolder, - expect: assert.NoError, - expectedCollectionPaths: []string{}, + inputFolderMap: map[string]string{}, + scope: anyFolder, + expect: assert.NoError, + expectedCollectionPaths: expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath, + ), expectedMetadataPaths: map[string]string{ "package": expectedPathAsSlice( suite.T(), @@ -182,7 +194,9 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { testBaseDrivePath+"/package", )[0], }, - expectedExcludes: map[string]struct{}{}, + expectedItemCount: 1, + expectedContainerCount: 1, + expectedExcludes: map[string]struct{}{}, }, { testCase: "1 root file, 1 folder, 1 package, 2 files, 3 collections", @@ -204,7 +218,7 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { testBaseDrivePath+folder, testBaseDrivePath+pkg, ), - expectedItemCount: 6, + expectedItemCount: 5, expectedFileCount: 3, expectedContainerCount: 3, expectedMetadataPaths: map[string]string{ @@ -238,23 +252,17 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { inputFolderMap: map[string]string{}, scope: (&selectors.OneDriveBackup{}).Folders([]string{"folder"})[0], expect: assert.NoError, - expectedCollectionPaths: append( - expectedPathAsSlice( - suite.T(), - tenant, - user, - testBaseDrivePath+"/folder", - ), - expectedPathAsSlice( - suite.T(), - tenant, - user, - testBaseDrivePath+folderSub+folder, - )..., + expectedCollectionPaths: expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath+"/folder", + testBaseDrivePath+folderSub, + testBaseDrivePath+folderSub+folder, ), expectedItemCount: 4, expectedFileCount: 2, - expectedContainerCount: 2, + expectedContainerCount: 3, // just "folder" isn't added here because the include check is done on the // parent path since we only check later if something is a folder or not. expectedMetadataPaths: map[string]string{ @@ -293,11 +301,12 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { suite.T(), tenant, user, + testBaseDrivePath+folderSub, testBaseDrivePath+folderSub+folder, ), expectedItemCount: 2, expectedFileCount: 1, - expectedContainerCount: 1, + expectedContainerCount: 2, expectedMetadataPaths: map[string]string{ "folder2": expectedPathAsSlice( suite.T(), @@ -328,7 +337,7 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { user, testBaseDrivePath+folderSub, ), - expectedItemCount: 2, + expectedItemCount: 1, expectedFileCount: 1, expectedContainerCount: 1, // No child folders for subfolder so nothing here. @@ -354,12 +363,17 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { testBaseDrivePath+"/folder/subfolder", )[0], }, - scope: anyFolder, - expect: assert.NoError, - expectedCollectionPaths: []string{}, - expectedItemCount: 0, - expectedFileCount: 0, - expectedContainerCount: 0, + scope: anyFolder, + expect: assert.NoError, + expectedCollectionPaths: expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath, + ), + expectedItemCount: 1, + expectedFileCount: 0, + expectedContainerCount: 1, expectedMetadataPaths: map[string]string{ "folder": expectedPathAsSlice( suite.T(), @@ -395,12 +409,17 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { testBaseDrivePath+"/a-folder/subfolder", )[0], }, - scope: anyFolder, - expect: assert.NoError, - expectedCollectionPaths: []string{}, - expectedItemCount: 0, - expectedFileCount: 0, - expectedContainerCount: 0, + scope: anyFolder, + expect: assert.NoError, + expectedCollectionPaths: expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath, + ), + expectedItemCount: 1, + expectedFileCount: 0, + expectedContainerCount: 1, expectedMetadataPaths: map[string]string{ "folder": expectedPathAsSlice( suite.T(), @@ -437,12 +456,17 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { testBaseDrivePath+"/a-folder/subfolder", )[0], }, - scope: anyFolder, - expect: assert.NoError, - expectedCollectionPaths: []string{}, - expectedItemCount: 0, - expectedFileCount: 0, - expectedContainerCount: 0, + scope: anyFolder, + expect: assert.NoError, + expectedCollectionPaths: expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath, + ), + expectedItemCount: 2, + expectedFileCount: 0, + expectedContainerCount: 1, expectedMetadataPaths: map[string]string{ "folder": expectedPathAsSlice( suite.T(), @@ -479,12 +503,17 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { testBaseDrivePath+"/a-folder/subfolder", )[0], }, - scope: anyFolder, - expect: assert.NoError, - expectedCollectionPaths: []string{}, - expectedItemCount: 0, - expectedFileCount: 0, - expectedContainerCount: 0, + scope: anyFolder, + expect: assert.NoError, + expectedCollectionPaths: expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath, + ), + expectedItemCount: 2, + expectedFileCount: 0, + expectedContainerCount: 1, expectedMetadataPaths: map[string]string{ "folder": expectedPathAsSlice( suite.T(), @@ -550,12 +579,17 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { testBaseDrivePath+"/folder/subfolder", )[0], }, - scope: anyFolder, - expect: assert.NoError, - expectedCollectionPaths: []string{}, - expectedItemCount: 0, - expectedFileCount: 0, - expectedContainerCount: 0, + scope: anyFolder, + expect: assert.NoError, + expectedCollectionPaths: expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath, + ), + expectedItemCount: 1, + expectedFileCount: 0, + expectedContainerCount: 1, expectedMetadataPaths: map[string]string{ "subfolder": expectedPathAsSlice( suite.T(), @@ -1043,6 +1077,12 @@ func (suite *OneDriveCollectionsSuite) TestGet() { ) require.NoError(suite.T(), err, "making metadata path") + rootFolderPath := expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath, + )[0] folderPath := expectedPathAsSlice( suite.T(), tenant, @@ -1067,6 +1107,12 @@ func (suite *OneDriveCollectionsSuite) TestGet() { driveBasePath2 := "drive/driveID2/root:" + rootFolderPath2 := expectedPathAsSlice( + suite.T(), + tenant, + user, + driveBasePath2, + )[0] folderPath2 := expectedPathAsSlice( suite.T(), tenant, @@ -1161,7 +1207,8 @@ func (suite *OneDriveCollectionsSuite) TestGet() { }, errCheck: assert.NoError, expectedCollections: map[string][]string{ - folderPath: {"file"}, + folderPath: {"file"}, + rootFolderPath: {"folder"}, }, expectedDeltaURLs: map[string]string{ driveID1: delta, @@ -1189,7 +1236,8 @@ func (suite *OneDriveCollectionsSuite) TestGet() { }, errCheck: assert.NoError, expectedCollections: map[string][]string{ - folderPath: {"file"}, + folderPath: {"file"}, + rootFolderPath: {"folder"}, }, expectedDeltaURLs: map[string]string{}, expectedFolderPaths: map[string]map[string]string{}, @@ -1218,7 +1266,8 @@ func (suite *OneDriveCollectionsSuite) TestGet() { }, errCheck: assert.NoError, expectedCollections: map[string][]string{ - folderPath: {"file", "file2"}, + folderPath: {"file", "file2"}, + rootFolderPath: {"folder"}, }, expectedDeltaURLs: map[string]string{ driveID1: delta, @@ -1258,8 +1307,10 @@ func (suite *OneDriveCollectionsSuite) TestGet() { }, errCheck: assert.NoError, expectedCollections: map[string][]string{ - folderPath: {"file"}, - folderPath2: {"file"}, + folderPath: {"file"}, + folderPath2: {"file"}, + rootFolderPath: {"folder"}, + rootFolderPath2: {"folder"}, }, expectedDeltaURLs: map[string]string{ driveID1: delta, diff --git a/src/internal/connector/onedrive/item.go b/src/internal/connector/onedrive/item.go index b1027de9d..1526f1401 100644 --- a/src/internal/connector/onedrive/item.go +++ b/src/internal/connector/onedrive/item.go @@ -1,7 +1,9 @@ package onedrive import ( + "bytes" "context" + "encoding/json" "fmt" "io" "net/http" @@ -37,6 +39,7 @@ func getDriveItem( // sharePointItemReader will return a io.ReadCloser for the specified item // It crafts this by querying M365 for a download URL for the item // and using a http client to initialize a reader +// TODO: Add metadata fetching to SharePoint func sharePointItemReader( hc *http.Client, item models.DriveItemable, @@ -53,6 +56,25 @@ func sharePointItemReader( return dii, resp.Body, nil } +func oneDriveItemMetaReader( + ctx context.Context, + service graph.Servicer, + driveID string, + item models.DriveItemable, +) (io.ReadCloser, int, error) { + meta, err := oneDriveItemMetaInfo(ctx, service, driveID, item) + if err != nil { + return nil, 0, err + } + + metaJSON, err := json.Marshal(meta) + if err != nil { + return nil, 0, err + } + + return io.NopCloser(bytes.NewReader(metaJSON)), len(metaJSON), nil +} + // oneDriveItemReader will return a io.ReadCloser for the specified item // It crafts this by querying M365 for a download URL for the item // and using a http client to initialize a reader @@ -60,16 +82,25 @@ func oneDriveItemReader( hc *http.Client, item models.DriveItemable, ) (details.ItemInfo, io.ReadCloser, error) { - resp, err := downloadItem(hc, item) - if err != nil { - return details.ItemInfo{}, nil, errors.Wrap(err, "downloading item") + var ( + rc io.ReadCloser + isFile = item.GetFile() != nil + ) + + if isFile { + resp, err := downloadItem(hc, item) + if err != nil { + return details.ItemInfo{}, nil, errors.Wrap(err, "downloading item") + } + + rc = resp.Body } dii := details.ItemInfo{ OneDrive: oneDriveItemInfo(item, *item.GetSize()), } - return dii, resp.Body, nil + return dii, rc, nil } func downloadItem(hc *http.Client, item models.DriveItemable) (*http.Response, error) { @@ -149,6 +180,47 @@ func oneDriveItemInfo(di models.DriveItemable, itemSize int64) *details.OneDrive } } +// oneDriveItemMetaInfo will fetch the meta information for a drive +// item. As of now, it only adds the permissions applicable for a +// onedrive item. +func oneDriveItemMetaInfo( + ctx context.Context, service graph.Servicer, + driveID string, di models.DriveItemable, +) (Metadata, error) { + itemID := di.GetId() + + perm, err := service.Client().DrivesById(driveID).ItemsById(*itemID).Permissions().Get(ctx, nil) + if err != nil { + return Metadata{}, errors.Wrapf(err, "failed to get item permissions %s", *itemID) + } + + up := []UserPermission{} + + for _, p := range perm.GetValue() { + roles := []string{} + + for _, r := range p.GetRoles() { + // Skip if the only role available in owner + if r != "owner" { + roles = append(roles, r) + } + } + + if len(roles) == 0 { + continue + } + + up = append(up, UserPermission{ + ID: *p.GetId(), + Roles: roles, + Email: *p.GetGrantedToV2().GetUser().GetAdditionalData()["email"].(*string), + Expiration: p.GetExpirationDateTime(), + }) + } + + return Metadata{Permissions: up}, nil +} + // sharePointItemInfo will populate a details.SharePointInfo struct // with properties from the drive item. ItemSize is specified // separately for restore processes because the local itemable diff --git a/src/internal/connector/onedrive/item_test.go b/src/internal/connector/onedrive/item_test.go index 6a8894ebf..b0e42943a 100644 --- a/src/internal/connector/onedrive/item_test.go +++ b/src/internal/connector/onedrive/item_test.go @@ -138,8 +138,8 @@ func (suite *ItemIntegrationSuite) TestItemReader_oneDrive() { ) // Read data for the file - itemInfo, itemData, err := oneDriveItemReader(graph.HTTPClient(graph.NoTimeout()), driveItem) + require.NoError(suite.T(), err) require.NotNil(suite.T(), itemInfo.OneDrive) require.NotEmpty(suite.T(), itemInfo.OneDrive.ItemName) diff --git a/src/internal/connector/onedrive/restore.go b/src/internal/connector/onedrive/restore.go index 00ed855b7..af591cd86 100644 --- a/src/internal/connector/onedrive/restore.go +++ b/src/internal/connector/onedrive/restore.go @@ -2,9 +2,15 @@ package onedrive import ( "context" + "encoding/json" + "fmt" "io" "runtime/trace" + "sort" + "strings" + msdrive "github.com/microsoftgraph/msgraph-sdk-go/drive" + "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/pkg/errors" "github.com/alcionai/corso/src/internal/connector/graph" @@ -25,28 +31,92 @@ const ( copyBufferSize = 5 * 1024 * 1024 ) +func getParentPermissions( + parentPath path.Path, + parentPermissions map[string][]UserPermission, +) ([]UserPermission, error) { + parentPerms, ok := parentPermissions[parentPath.String()] + if !ok { + onedrivePath, err := path.ToOneDrivePath(parentPath) + if err != nil { + return nil, errors.Wrap(err, "invalid restore path") + } + + if len(onedrivePath.Folders) != 0 { + return nil, errors.Wrap(err, "unable to compute item permissions") + } + + parentPerms = []UserPermission{} + } + + return parentPerms, nil +} + // RestoreCollections will restore the specified data collections into OneDrive func RestoreCollections( ctx context.Context, service graph.Servicer, dest control.RestoreDestination, + opts control.Options, dcs []data.Collection, deets *details.Builder, ) (*support.ConnectorOperationStatus, error) { var ( restoreMetrics support.CollectionMetrics restoreErrors error + metrics support.CollectionMetrics + folderPerms map[string][]UserPermission + canceled bool + + // permissionIDMappings is used to map between old and new id + // of permissions as we restore them + permissionIDMappings = map[string]string{} ) errUpdater := func(id string, err error) { restoreErrors = support.WrapAndAppend(id, err, restoreErrors) } + // Reorder collections so that the parents directories are created + // before the child directories + sort.Slice(dcs, func(i, j int) bool { + return dcs[i].FullPath().String() < dcs[j].FullPath().String() + }) + + parentPermissions := map[string][]UserPermission{} + // Iterate through the data collections and restore the contents of each for _, dc := range dcs { - temp, canceled := RestoreCollection(ctx, service, dc, OneDriveSource, dest.ContainerName, deets, errUpdater) + var ( + parentPerms []UserPermission + err error + ) - restoreMetrics.Combine(temp) + if opts.RestorePermissions { + parentPerms, err = getParentPermissions(dc.FullPath(), parentPermissions) + if err != nil { + errUpdater(dc.FullPath().String(), err) + } + } + + metrics, folderPerms, permissionIDMappings, canceled = RestoreCollection( + ctx, + service, + dc, + parentPerms, + OneDriveSource, + dest.ContainerName, + deets, + errUpdater, + permissionIDMappings, + opts.RestorePermissions, + ) + + for k, v := range folderPerms { + parentPermissions[k] = v + } + + restoreMetrics.Combine(metrics) if canceled { break @@ -66,29 +136,36 @@ func RestoreCollections( // RestoreCollection handles restoration of an individual collection. // returns: // - the collection's item and byte count metrics -// - the context cancellation state (true if the context is cancelled) +// - the context cancellation state (true if the context is canceled) func RestoreCollection( ctx context.Context, service graph.Servicer, dc data.Collection, + parentPerms []UserPermission, source driveSource, restoreContainerName string, deets *details.Builder, errUpdater func(string, error), -) (support.CollectionMetrics, bool) { + permissionIDMappings map[string]string, + restorePerms bool, +) (support.CollectionMetrics, map[string][]UserPermission, map[string]string, bool) { ctx, end := D.Span(ctx, "gc:oneDrive:restoreCollection", D.Label("path", dc.FullPath())) defer end() var ( - metrics = support.CollectionMetrics{} - copyBuffer = make([]byte, copyBufferSize) - directory = dc.FullPath() + metrics = support.CollectionMetrics{} + copyBuffer = make([]byte, copyBufferSize) + directory = dc.FullPath() + restoredIDs = map[string]string{} + itemInfo details.ItemInfo + itemID string + folderPerms = map[string][]UserPermission{} ) drivePath, err := path.ToOneDrivePath(directory) if err != nil { errUpdater(directory.String(), err) - return metrics, false + return metrics, folderPerms, permissionIDMappings, false } // Assemble folder hierarchy we're going to restore into (we recreate the folder hierarchy @@ -108,7 +185,7 @@ func RestoreCollection( restoreFolderID, err := CreateRestoreFolders(ctx, service, drivePath.DriveID, restoreFolderElements) if err != nil { errUpdater(directory.String(), errors.Wrapf(err, "failed to create folders %v", restoreFolderElements)) - return metrics, false + return metrics, folderPerms, permissionIDMappings, false } // Restore items from the collection @@ -118,50 +195,175 @@ func RestoreCollection( select { case <-ctx.Done(): errUpdater("context canceled", ctx.Err()) - return metrics, true + return metrics, folderPerms, permissionIDMappings, true case itemData, ok := <-items: if !ok { - return metrics, false - } - metrics.Objects++ - - metrics.TotalBytes += int64(len(copyBuffer)) - - itemInfo, err := restoreItem(ctx, - service, - itemData, - drivePath.DriveID, - restoreFolderID, - copyBuffer, - source) - if err != nil { - errUpdater(itemData.UUID(), err) - continue + return metrics, folderPerms, permissionIDMappings, false } itemPath, err := dc.FullPath().Append(itemData.UUID(), true) if err != nil { logger.Ctx(ctx).DPanicw("transforming item to full path", "error", err) + errUpdater(itemData.UUID(), err) continue } - deets.Add( - itemPath.String(), - itemPath.ShortRef(), - "", - true, - itemInfo) + if source == OneDriveSource { + name := itemData.UUID() + if strings.HasSuffix(name, DataFileSuffix) { + metrics.Objects++ + metrics.TotalBytes += int64(len(copyBuffer)) + trimmedName := strings.TrimSuffix(name, DataFileSuffix) - metrics.Successes++ + itemID, itemInfo, err = restoreData(ctx, service, trimmedName, itemData, + drivePath.DriveID, restoreFolderID, copyBuffer, source) + if err != nil { + errUpdater(itemData.UUID(), err) + continue + } + + restoredIDs[trimmedName] = itemID + + deets.Add(itemPath.String(), itemPath.ShortRef(), "", true, itemInfo) + + // Mark it as success without processing .meta + // file if we are not restoring permissions + if !restorePerms { + metrics.Successes++ + } + } else if strings.HasSuffix(name, MetaFileSuffix) { + if !restorePerms { + continue + } + + meta, err := getMetadata(itemData.ToReader()) + if err != nil { + errUpdater(itemData.UUID(), err) + continue + } + + trimmedName := strings.TrimSuffix(name, MetaFileSuffix) + restoreID, ok := restoredIDs[trimmedName] + if !ok { + errUpdater(itemData.UUID(), fmt.Errorf("item not available to restore permissions")) + continue + } + + permissionIDMappings, err = restorePermissions( + ctx, + service, + drivePath.DriveID, + restoreID, + parentPerms, + meta.Permissions, + permissionIDMappings, + ) + if err != nil { + errUpdater(itemData.UUID(), err) + continue + } + + // Objects count is incremented when we restore a + // data file and success count is incremented when + // we restore a meta file as every data file + // should have an associated meta file + metrics.Successes++ + } else if strings.HasSuffix(name, DirMetaFileSuffix) { + trimmedName := strings.TrimSuffix(name, DirMetaFileSuffix) + folderID, err := createRestoreFolder( + ctx, + service, + drivePath.DriveID, + trimmedName, + restoreFolderID, + ) + if err != nil { + errUpdater(itemData.UUID(), err) + continue + } + + if !restorePerms { + continue + } + + meta, err := getMetadata(itemData.ToReader()) + if err != nil { + errUpdater(itemData.UUID(), err) + continue + } + + permissionIDMappings, err = restorePermissions( + ctx, + service, + drivePath.DriveID, + folderID, + parentPerms, + meta.Permissions, + permissionIDMappings, + ) + if err != nil { + errUpdater(itemData.UUID(), err) + continue + } + + trimmedPath := strings.TrimSuffix(itemPath.String(), DirMetaFileSuffix) + folderPerms[trimmedPath] = meta.Permissions + } else { + if !ok { + errUpdater(itemData.UUID(), fmt.Errorf("invalid backup format, you might be using an old backup")) + continue + } + } + } else { + metrics.Objects++ + metrics.TotalBytes += int64(len(copyBuffer)) + + // No permissions stored at the moment for SharePoint + _, itemInfo, err = restoreData(ctx, + service, + itemData.UUID(), + itemData, + drivePath.DriveID, + restoreFolderID, + copyBuffer, + source) + if err != nil { + errUpdater(itemData.UUID(), err) + continue + } + + deets.Add(itemPath.String(), itemPath.ShortRef(), "", true, itemInfo) + metrics.Successes++ + } } } } -// createRestoreFolders creates the restore folder hieararchy in the specified drive and returns the folder ID -// of the last folder entry in the hiearchy +// Creates a folder with its permissions +func createRestoreFolder( + ctx context.Context, + service graph.Servicer, + driveID, folder, parentFolderID string, +) (string, error) { + folderItem, err := createItem(ctx, service, driveID, parentFolderID, newItem(folder, true)) + if err != nil { + return "", errors.Wrapf( + err, + "failed to create folder %s/%s. details: %s", parentFolderID, folder, + support.ConnectorStackErrorTrace(err), + ) + } + + logger.Ctx(ctx).Debugf("Resolved %s in %s to %s", folder, parentFolderID, *folderItem.GetId()) + + return *folderItem.GetId(), nil +} + +// createRestoreFolders creates the restore folder hierarchy in the specified drive and returns the folder ID +// of the last folder entry in the hierarchy func CreateRestoreFolders(ctx context.Context, service graph.Servicer, driveID string, restoreFolders []string, ) (string, error) { driveRoot, err := service.Client().DrivesById(driveID).Root().Get(ctx, nil) @@ -209,15 +411,16 @@ func CreateRestoreFolders(ctx context.Context, service graph.Servicer, driveID s return parentFolderID, nil } -// restoreItem will create a new item in the specified `parentFolderID` and upload the data.Stream -func restoreItem( +// restoreData will create a new item in the specified `parentFolderID` and upload the data.Stream +func restoreData( ctx context.Context, service graph.Servicer, + name string, itemData data.Stream, driveID, parentFolderID string, copyBuffer []byte, source driveSource, -) (details.ItemInfo, error) { +) (string, details.ItemInfo, error) { ctx, end := D.Span(ctx, "gc:oneDrive:restoreItem", D.Label("item_uuid", itemData.UUID())) defer end() @@ -227,19 +430,19 @@ func restoreItem( // Get the stream size (needed to create the upload session) ss, ok := itemData.(data.StreamSize) if !ok { - return details.ItemInfo{}, errors.Errorf("item %q does not implement DataStreamInfo", itemName) + return "", details.ItemInfo{}, errors.Errorf("item %q does not implement DataStreamInfo", itemName) } // Create Item - newItem, err := createItem(ctx, service, driveID, parentFolderID, newItem(itemData.UUID(), false)) + newItem, err := createItem(ctx, service, driveID, parentFolderID, newItem(name, false)) if err != nil { - return details.ItemInfo{}, errors.Wrapf(err, "failed to create item %s", itemName) + return "", details.ItemInfo{}, errors.Wrapf(err, "failed to create item %s", itemName) } // Get a drive item writer w, err := driveItemWriter(ctx, service, driveID, *newItem.GetId(), ss.Size()) if err != nil { - return details.ItemInfo{}, errors.Wrapf(err, "failed to create item upload session %s", itemName) + return "", details.ItemInfo{}, errors.Wrapf(err, "failed to create item upload session %s", itemName) } iReader := itemData.ToReader() @@ -250,7 +453,7 @@ func restoreItem( // Upload the stream data written, err := io.CopyBuffer(w, progReader, copyBuffer) if err != nil { - return details.ItemInfo{}, errors.Wrapf(err, "failed to upload data: item %s", itemName) + return "", details.ItemInfo{}, errors.Wrapf(err, "failed to upload data: item %s", itemName) } dii := details.ItemInfo{} @@ -262,5 +465,129 @@ func restoreItem( dii.OneDrive = oneDriveItemInfo(newItem, written) } - return dii, nil + return *newItem.GetId(), dii, nil +} + +// getMetadata read and parses the metadata info for an item +func getMetadata(metar io.ReadCloser) (Metadata, error) { + var meta Metadata + // `metar` will be nil for the top level container folder + if metar != nil { + metaraw, err := io.ReadAll(metar) + if err != nil { + return Metadata{}, err + } + + err = json.Unmarshal(metaraw, &meta) + if err != nil { + return Metadata{}, err + } + } + + return meta, nil +} + +// getChildPermissions is to filter out permissions present in the +// parent from the ones that are available for child. This is +// necessary as we store the nested permissions in the child. We +// cannot avoid storing the nested permissions as it is possible that +// a file in a folder can remove the nested permission that is present +// on itself. +func getChildPermissions(childPermissions, parentPermissions []UserPermission) ([]UserPermission, []UserPermission) { + addedPermissions := []UserPermission{} + removedPermissions := []UserPermission{} + + for _, cp := range childPermissions { + found := false + + for _, pp := range parentPermissions { + if cp.ID == pp.ID { + found = true + break + } + } + + if !found { + addedPermissions = append(addedPermissions, cp) + } + } + + for _, pp := range parentPermissions { + found := false + + for _, cp := range childPermissions { + if pp.ID == cp.ID { + found = true + break + } + } + + if !found { + removedPermissions = append(removedPermissions, pp) + } + } + + return addedPermissions, removedPermissions +} + +// restorePermissions takes in the permissions that were added and the +// removed(ones present in parent but not in child) and adds/removes +// the necessary permissions on onedrive objects. +func restorePermissions( + ctx context.Context, + service graph.Servicer, + driveID string, + itemID string, + parentPerms []UserPermission, + childPerms []UserPermission, + permissionIDMappings map[string]string, +) (map[string]string, error) { + permAdded, permRemoved := getChildPermissions(childPerms, parentPerms) + + for _, p := range permRemoved { + err := service.Client().DrivesById(driveID).ItemsById(itemID). + PermissionsById(permissionIDMappings[p.ID]).Delete(ctx, nil) + if err != nil { + return permissionIDMappings, errors.Wrapf( + err, + "failed to remove permission for item %s. details: %s", + itemID, + support.ConnectorStackErrorTrace(err), + ) + } + } + + for _, p := range permAdded { + pbody := msdrive.NewItemsItemInvitePostRequestBody() + pbody.SetRoles(p.Roles) + + if p.Expiration != nil { + expiry := p.Expiration.String() + pbody.SetExpirationDateTime(&expiry) + } + + si := false + pbody.SetSendInvitation(&si) + + rs := true + pbody.SetRequireSignIn(&rs) + + rec := models.NewDriveRecipient() + rec.SetEmail(&p.Email) + pbody.SetRecipients([]models.DriveRecipientable{rec}) + + np, err := service.Client().DrivesById(driveID).ItemsById(itemID).Invite().Post(ctx, pbody, nil) + if err != nil { + return permissionIDMappings, errors.Wrapf( + err, + "failed to set permission for item %s. details: %s", + itemID, + support.ConnectorStackErrorTrace(err), + ) + } + + permissionIDMappings[p.ID] = *np.GetValue()[0].GetId() + } + + return permissionIDMappings, nil } diff --git a/src/internal/connector/sharepoint/data_collections_test.go b/src/internal/connector/sharepoint/data_collections_test.go index 87aaa5c84..11d05156c 100644 --- a/src/internal/connector/sharepoint/data_collections_test.go +++ b/src/internal/connector/sharepoint/data_collections_test.go @@ -77,7 +77,7 @@ func (suite *SharePointLibrariesSuite) TestUpdateCollections() { site, testBaseDrivePath, ), - expectedItemCount: 2, + expectedItemCount: 1, expectedFileCount: 1, expectedContainerCount: 1, }, diff --git a/src/internal/connector/sharepoint/restore.go b/src/internal/connector/sharepoint/restore.go index ef2b940bb..4784ed209 100644 --- a/src/internal/connector/sharepoint/restore.go +++ b/src/internal/connector/sharepoint/restore.go @@ -59,14 +59,18 @@ func RestoreCollections( switch dc.FullPath().Category() { case path.LibrariesCategory: - metrics, canceled = onedrive.RestoreCollection( + metrics, _, _, canceled = onedrive.RestoreCollection( ctx, service, dc, + []onedrive.UserPermission{}, // Currently permission data is not stored for sharepoint onedrive.OneDriveSource, dest.ContainerName, deets, - errUpdater) + errUpdater, + map[string]string{}, + false, + ) case path.ListsCategory: metrics, canceled = RestoreCollection( ctx, diff --git a/src/internal/connector/support/status.go b/src/internal/connector/support/status.go index 7e38758d3..dcf5f32c5 100644 --- a/src/internal/connector/support/status.go +++ b/src/internal/connector/support/status.go @@ -66,6 +66,7 @@ func CreateStatus( hasErrors := err != nil numErr := GetNumberOfErrors(err) + status := ConnectorOperationStatus{ lastOperation: op, ObjectCount: cm.Objects, diff --git a/src/internal/operations/backup_integration_test.go b/src/internal/operations/backup_integration_test.go index 21d4009e0..a57a9d2be 100644 --- a/src/internal/operations/backup_integration_test.go +++ b/src/internal/operations/backup_integration_test.go @@ -339,7 +339,7 @@ func generateContainerOfItems( dest, collections) - deets, err := gc.RestoreDataCollections(ctx, acct, sel, dest, dataColls) + deets, err := gc.RestoreDataCollections(ctx, acct, sel, dest, control.Options{RestorePermissions: true}, dataColls) require.NoError(t, err) return deets diff --git a/src/internal/operations/restore.go b/src/internal/operations/restore.go index aa9229336..206eb8026 100644 --- a/src/internal/operations/restore.go +++ b/src/internal/operations/restore.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "runtime/debug" + "sort" "time" "github.com/alcionai/clues" @@ -221,7 +222,9 @@ func (op *RestoreOperation) do(ctx context.Context) (restoreDetails *details.Det op.account, op.Selectors, op.Destination, - dcs) + op.Options, + dcs, + ) if err != nil { opStats.writeErr = errors.Wrap(err, "restoring service data") return nil, opStats.writeErr @@ -327,6 +330,17 @@ func formatDetailsForRestoration( paths[i] = p } + // TODO(meain): Move this to onedrive specific component, but as + // of now the paths can technically be from multiple services + + // This sort is done primarily to order `.meta` files after `.data` + // files. This is only a necessity for OneDrive as we are storing + // metadata for files/folders in separate meta files and we the + // data to be restored before we can restore the metadata. + sort.Slice(paths, func(i, j int) bool { + return paths[i].String() < paths[j].String() + }) + if errs != nil { return nil, errs } diff --git a/src/pkg/control/options.go b/src/pkg/control/options.go index 9cc5a334a..6f53839ca 100644 --- a/src/pkg/control/options.go +++ b/src/pkg/control/options.go @@ -6,10 +6,11 @@ import ( // Options holds the optional configurations for a process type Options struct { - Collision CollisionPolicy `json:"-"` - DisableMetrics bool `json:"disableMetrics"` - FailFast bool `json:"failFast"` - ToggleFeatures Toggles `json:"ToggleFeatures"` + Collision CollisionPolicy `json:"-"` + DisableMetrics bool `json:"disableMetrics"` + FailFast bool `json:"failFast"` + RestorePermissions bool `json:"restorePermissions"` + ToggleFeatures Toggles `json:"ToggleFeatures"` } // Defaults provides an Options with the default values set. @@ -74,4 +75,9 @@ type Toggles struct { // DisableIncrementals prevents backups from using incremental lookups, // forcing a new, complete backup of all data regardless of prior state. DisableIncrementals bool `json:"exchangeIncrementals,omitempty"` + + // DisablePermissionsBackup is used to disable backups of item + // permissions. Permission metadata increases graph api call count, + // so disabling their retrieval when not needed is advised. + DisablePermissionsBackup bool `json:"disablePermissionsBackup,omitempty"` } From 9da0a7878b5d01cf7750a30ff3a4a9566a098b5e Mon Sep 17 00:00:00 2001 From: Abin Simon Date: Fri, 3 Feb 2023 09:33:14 +0530 Subject: [PATCH 27/40] Backup versioning (#2324) ## Description Add backup format version information to the backups so that we can distinguish between backups which use a single file vs the ones that use both .data and .meta files. Overrides https://github.com/alcionai/corso/pull/2297. I've also set it against `main` so that the diff shows up properly. Ref: https://github.com/alcionai/corso/pull/2324#issuecomment-1409709118 ## Does this PR need a docs update or release note? - [x] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [ ] :no_entry: No ## Type of change - [x] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * fixes https://github.com/alcionai/corso/issues/2230 * https://github.com/alcionai/corso/issues/2253 ## Test Plan - [x] :muscle: Manual (Tested manually, will add an e2e test in a followup) - [ ] :zap: Unit test - [ ] :green_heart: E2E --- CHANGELOG.md | 1 + src/cmd/factory/impl/common.go | 3 +- src/internal/connector/graph_connector.go | 5 +- .../connector/graph_connector_helper_test.go | 56 ++- .../connector/graph_connector_test.go | 378 +++++++++++++++++- src/internal/connector/onedrive/restore.go | 10 +- src/internal/connector/sharepoint/restore.go | 2 + .../operations/backup_integration_test.go | 10 +- src/internal/operations/restore.go | 1 + src/pkg/backup/backup.go | 6 + 10 files changed, 465 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33485d926..09d3845da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Increase page size preference for delta requests for Exchange to reduce number of roundtrips - OneDrive file/folder permissions can now be backed up and restored - Add `--restore-permissions` flag to toggle restoration of OneDrive permissions +- Add versions to backups so that we can understand/handle older backup formats ### Known Issues diff --git a/src/cmd/factory/impl/common.go b/src/cmd/factory/impl/common.go index 3ed3831fc..78a5dca0e 100644 --- a/src/cmd/factory/impl/common.go +++ b/src/cmd/factory/impl/common.go @@ -16,6 +16,7 @@ import ( "github.com/alcionai/corso/src/internal/connector/mockconnector" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/pkg/account" + "github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/credentials" @@ -91,7 +92,7 @@ func generateAndRestoreItems( Infof(ctx, "Generating %d %s items in %s\n", howMany, cat, Destination) - return gc.RestoreDataCollections(ctx, acct, sel, dest, opts, dataColls) + return gc.RestoreDataCollections(ctx, backup.Version, acct, sel, dest, opts, dataColls) } // ------------------------------------------------------------------------------------------ diff --git a/src/internal/connector/graph_connector.go b/src/internal/connector/graph_connector.go index 370948639..def430f14 100644 --- a/src/internal/connector/graph_connector.go +++ b/src/internal/connector/graph_connector.go @@ -266,6 +266,7 @@ func (gc *GraphConnector) UnionSiteIDsAndWebURLs(ctx context.Context, ids, urls // SideEffect: gc.status is updated at the completion of operation func (gc *GraphConnector) RestoreDataCollections( ctx context.Context, + backupVersion int, acct account.Account, selector selectors.Selector, dest control.RestoreDestination, @@ -290,9 +291,9 @@ func (gc *GraphConnector) RestoreDataCollections( case selectors.ServiceExchange: status, err = exchange.RestoreExchangeDataCollections(ctx, creds, gc.Service, dest, dcs, deets) case selectors.ServiceOneDrive: - status, err = onedrive.RestoreCollections(ctx, gc.Service, dest, opts, dcs, deets) + status, err = onedrive.RestoreCollections(ctx, backupVersion, gc.Service, dest, opts, dcs, deets) case selectors.ServiceSharePoint: - status, err = sharepoint.RestoreCollections(ctx, gc.Service, dest, dcs, deets) + status, err = sharepoint.RestoreCollections(ctx, backupVersion, gc.Service, dest, dcs, deets) default: err = errors.Errorf("restore data from service %s not supported", selector.Service.String()) } diff --git a/src/internal/connector/graph_connector_helper_test.go b/src/internal/connector/graph_connector_helper_test.go index 8a0e22a26..539cbf501 100644 --- a/src/internal/connector/graph_connector_helper_test.go +++ b/src/internal/connector/graph_connector_helper_test.go @@ -172,6 +172,14 @@ type restoreBackupInfo struct { resource resource } +type restoreBackupInfoMultiVersion struct { + name string + service path.ServiceType + collectionsLatest []colInfo + collectionsPrevious []colInfo + resource resource +} + func attachmentEqual( expected models.Attachmentable, got models.Attachmentable, @@ -653,7 +661,7 @@ func compareOneDriveItem( name := item.UUID() expectedData := expected[item.UUID()] - if !assert.NotNil(t, expectedData, "unexpected file with name %s", item.UUID) { + if !assert.NotNil(t, expectedData, "unexpected file with name %s", item.UUID()) { return } @@ -988,6 +996,52 @@ func collectionsForInfo( return totalItems, kopiaEntries, collections, expectedData } +func collectionsForInfoVersion0( + t *testing.T, + service path.ServiceType, + tenant, user string, + dest control.RestoreDestination, + allInfo []colInfo, +) (int, int, []data.Collection, map[string]map[string][]byte) { + collections := make([]data.Collection, 0, len(allInfo)) + expectedData := make(map[string]map[string][]byte, len(allInfo)) + totalItems := 0 + kopiaEntries := 0 + + for _, info := range allInfo { + pth := mustToDataLayerPath( + t, + service, + tenant, + user, + info.category, + info.pathElements, + false, + ) + c := mockconnector.NewMockExchangeCollection(pth, len(info.items)) + baseDestPath := backupOutputPathFromRestore(t, dest, pth) + + baseExpected := expectedData[baseDestPath.String()] + if baseExpected == nil { + expectedData[baseDestPath.String()] = make(map[string][]byte, len(info.items)) + baseExpected = expectedData[baseDestPath.String()] + } + + for i := 0; i < len(info.items); i++ { + c.Names[i] = info.items[i].name + c.Data[i] = info.items[i].data + + baseExpected[info.items[i].lookupKey] = info.items[i].data + } + + collections = append(collections, c) + totalItems += len(info.items) + kopiaEntries += len(info.items) + } + + return totalItems, kopiaEntries, collections, expectedData +} + //nolint:deadcode func getSelectorWith( t *testing.T, diff --git a/src/internal/connector/graph_connector_test.go b/src/internal/connector/graph_connector_test.go index 2c2280e37..1cdb0b59e 100644 --- a/src/internal/connector/graph_connector_test.go +++ b/src/internal/connector/graph_connector_test.go @@ -22,6 +22,7 @@ import ( "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/account" + "github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/selectors" @@ -231,7 +232,15 @@ func (suite *GraphConnectorIntegrationSuite) TestRestoreFailsBadService() { } ) - deets, err := suite.connector.RestoreDataCollections(ctx, acct, sel, dest, control.Options{}, nil) + deets, err := suite.connector.RestoreDataCollections( + ctx, + backup.Version, + acct, + sel, + dest, + control.Options{}, + nil, + ) assert.Error(t, err) assert.NotNil(t, deets) @@ -299,6 +308,7 @@ func (suite *GraphConnectorIntegrationSuite) TestEmptyCollections() { deets, err := suite.connector.RestoreDataCollections( ctx, + backup.Version, suite.acct, test.sel, dest, @@ -393,6 +403,7 @@ func runRestoreBackupTest( restoreSel := getSelectorWith(t, test.service, resourceOwners, true) deets, err := restoreGC.RestoreDataCollections( ctx, + backup.Version, acct, restoreSel, dest, @@ -458,6 +469,121 @@ func runRestoreBackupTest( "backup status.Successful; wanted %d items + %d skipped", totalItems, skipped) } +// runRestoreBackupTestVersion0 restores with data from an older +// version of the backup and check the restored data against the +// something that would be in the form of a newer backup. +func runRestoreBackupTestVersion0( + t *testing.T, + acct account.Account, + test restoreBackupInfoMultiVersion, + tenant string, + resourceOwners []string, + opts control.Options, +) { + var ( + collections []data.Collection + expectedData = map[string]map[string][]byte{} + totalItems = 0 + totalKopiaItems = 0 + // Get a dest per test so they're independent. + dest = tester.DefaultTestRestoreDestination() + ) + + ctx, flush := tester.NewContext() + defer flush() + + for _, owner := range resourceOwners { + _, _, ownerCollections, _ := collectionsForInfoVersion0( + t, + test.service, + tenant, + owner, + dest, + test.collectionsPrevious, + ) + + collections = append(collections, ownerCollections...) + } + + t.Logf( + "Restoring collections to %s for resourceOwners(s) %v\n", + dest.ContainerName, + resourceOwners, + ) + + start := time.Now() + + restoreGC := loadConnector(ctx, t, graph.HTTPClient(graph.NoTimeout()), test.resource) + restoreSel := getSelectorWith(t, test.service, resourceOwners, true) + deets, err := restoreGC.RestoreDataCollections( + ctx, + 0, // The OG version ;) + acct, + restoreSel, + dest, + opts, + collections, + ) + require.NoError(t, err) + assert.NotNil(t, deets) + + assert.NotNil(t, restoreGC.AwaitStatus()) + + runTime := time.Since(start) + + t.Logf("Restore complete in %v\n", runTime) + + // Run a backup and compare its output with what we put in. + for _, owner := range resourceOwners { + numItems, kopiaItems, _, userExpectedData := collectionsForInfo( + t, + test.service, + tenant, + owner, + dest, + test.collectionsLatest, + ) + + totalItems += numItems + totalKopiaItems += kopiaItems + + maps.Copy(expectedData, userExpectedData) + } + + cats := make(map[path.CategoryType]struct{}, len(test.collectionsLatest)) + for _, c := range test.collectionsLatest { + cats[c.category] = struct{}{} + } + + expectedDests := make([]destAndCats, 0, len(resourceOwners)) + for _, ro := range resourceOwners { + expectedDests = append(expectedDests, destAndCats{ + resourceOwner: ro, + dest: dest.ContainerName, + cats: cats, + }) + } + + backupGC := loadConnector(ctx, t, graph.HTTPClient(graph.NoTimeout()), test.resource) + backupSel := backupSelectorForExpected(t, test.service, expectedDests) + + start = time.Now() + dcs, excludes, err := backupGC.DataCollections(ctx, backupSel, nil, control.Options{RestorePermissions: true}) + require.NoError(t, err) + // No excludes yet because this isn't an incremental backup. + assert.Empty(t, excludes) + + t.Logf("Backup enumeration complete in %v\n", time.Since(start)) + + // Pull the data prior to waiting for the status as otherwise it will + // deadlock. + skipped := checkCollections(t, totalKopiaItems, expectedData, dcs, opts.RestorePermissions) + + status := backupGC.AwaitStatus() + assert.Equal(t, totalItems+skipped, status.ObjectCount, "status.ObjectCount") + assert.Equal(t, totalItems+skipped, status.Successful, "status.Successful") +} + func getTestMetaJSON(t *testing.T, user string, roles []string) []byte { id := base64.StdEncoding.EncodeToString([]byte(user + strings.Join(roles, "+"))) testMeta := onedrive.Metadata{Permissions: []onedrive.UserPermission{ @@ -906,6 +1032,255 @@ func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackup() { } } +func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackupVersion0() { + ctx, flush := tester.NewContext() + defer flush() + + // Get the default drive ID for the test user. + driveID := mustGetDefaultDriveID( + suite.T(), + ctx, + suite.connector.Service, + suite.user, + ) + + table := []restoreBackupInfoMultiVersion{ + { + name: "OneDriveMultipleFoldersAndFiles", + service: path.OneDriveService, + resource: Users, + + collectionsPrevious: []colInfo{ + { + pathElements: []string{ + "drives", + driveID, + "root:", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt", + data: []byte(strings.Repeat("a", 33)), + lookupKey: "test-file.txt", + }, + }, + }, + { + pathElements: []string{ + "drives", + driveID, + "root:", + "folder-a", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt", + data: []byte(strings.Repeat("b", 65)), + lookupKey: "test-file.txt", + }, + }, + }, + { + pathElements: []string{ + "drives", + driveID, + "root:", + "folder-a", + "b", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt", + data: []byte(strings.Repeat("c", 129)), + lookupKey: "test-file.txt", + }, + }, + }, + { + pathElements: []string{ + "drives", + driveID, + "root:", + "folder-a", + "b", + "folder-a", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt", + data: []byte(strings.Repeat("d", 257)), + lookupKey: "test-file.txt", + }, + }, + }, + { + pathElements: []string{ + "drives", + driveID, + "root:", + "b", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt", + data: []byte(strings.Repeat("e", 257)), + lookupKey: "test-file.txt", + }, + }, + }, + }, + + collectionsLatest: []colInfo{ + { + pathElements: []string{ + "drives", + driveID, + "root:", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt" + onedrive.DataFileSuffix, + data: []byte(strings.Repeat("a", 33)), + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: []byte("{}"), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + { + name: "folder-a" + onedrive.DirMetaFileSuffix, + data: []byte("{}"), + lookupKey: "folder-a" + onedrive.DirMetaFileSuffix, + }, + { + name: "b" + onedrive.DirMetaFileSuffix, + data: []byte("{}"), + lookupKey: "b" + onedrive.DirMetaFileSuffix, + }, + }, + }, + { + pathElements: []string{ + "drives", + driveID, + "root:", + "folder-a", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt" + onedrive.DataFileSuffix, + data: []byte(strings.Repeat("b", 65)), + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: []byte("{}"), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + { + name: "b" + onedrive.DirMetaFileSuffix, + data: []byte("{}"), + lookupKey: "b" + onedrive.DirMetaFileSuffix, + }, + }, + }, + { + pathElements: []string{ + "drives", + driveID, + "root:", + "folder-a", + "b", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt" + onedrive.DataFileSuffix, + data: []byte(strings.Repeat("c", 129)), + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: []byte("{}"), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + { + name: "folder-a" + onedrive.DirMetaFileSuffix, + data: []byte("{}"), + lookupKey: "folder-a" + onedrive.DirMetaFileSuffix, + }, + }, + }, + { + pathElements: []string{ + "drives", + driveID, + "root:", + "folder-a", + "b", + "folder-a", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt" + onedrive.DataFileSuffix, + data: []byte(strings.Repeat("d", 257)), + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: []byte("{}"), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + }, + }, + { + pathElements: []string{ + "drives", + driveID, + "root:", + "b", + }, + category: path.FilesCategory, + items: []itemInfo{ + { + name: "test-file.txt" + onedrive.DataFileSuffix, + data: []byte(strings.Repeat("e", 257)), + lookupKey: "test-file.txt" + onedrive.DataFileSuffix, + }, + { + name: "test-file.txt" + onedrive.MetaFileSuffix, + data: []byte("{}"), + lookupKey: "test-file.txt" + onedrive.MetaFileSuffix, + }, + }, + }, + }, + }, + } + + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + runRestoreBackupTestVersion0( + t, + suite.acct, + test, + suite.connector.tenant, + []string{suite.user}, + control.Options{RestorePermissions: true}, + ) + }) + } +} + func (suite *GraphConnectorIntegrationSuite) TestMultiFolderBackupDifferentNames() { table := []restoreBackupInfo{ { @@ -1012,6 +1387,7 @@ func (suite *GraphConnectorIntegrationSuite) TestMultiFolderBackupDifferentNames restoreGC := loadConnector(ctx, t, graph.HTTPClient(graph.NoTimeout()), test.resource) deets, err := restoreGC.RestoreDataCollections( ctx, + backup.Version, suite.acct, restoreSel, dest, diff --git a/src/internal/connector/onedrive/restore.go b/src/internal/connector/onedrive/restore.go index af591cd86..0014457c4 100644 --- a/src/internal/connector/onedrive/restore.go +++ b/src/internal/connector/onedrive/restore.go @@ -29,6 +29,11 @@ const ( // Microsoft recommends 5-10MB buffers // https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#best-practices copyBufferSize = 5 * 1024 * 1024 + + // versionWithDataAndMetaFiles is the corso backup format version + // in which we split from storing just the data to storing both + // the data and metadata in two files. + versionWithDataAndMetaFiles = 1 ) func getParentPermissions( @@ -55,6 +60,7 @@ func getParentPermissions( // RestoreCollections will restore the specified data collections into OneDrive func RestoreCollections( ctx context.Context, + backupVersion int, service graph.Servicer, dest control.RestoreDestination, opts control.Options, @@ -101,6 +107,7 @@ func RestoreCollections( metrics, folderPerms, permissionIDMappings, canceled = RestoreCollection( ctx, + backupVersion, service, dc, parentPerms, @@ -139,6 +146,7 @@ func RestoreCollections( // - the context cancellation state (true if the context is canceled) func RestoreCollection( ctx context.Context, + backupVersion int, service graph.Servicer, dc data.Collection, parentPerms []UserPermission, @@ -211,7 +219,7 @@ func RestoreCollection( continue } - if source == OneDriveSource { + if source == OneDriveSource && backupVersion >= versionWithDataAndMetaFiles { name := itemData.UUID() if strings.HasSuffix(name, DataFileSuffix) { metrics.Objects++ diff --git a/src/internal/connector/sharepoint/restore.go b/src/internal/connector/sharepoint/restore.go index 4784ed209..3cf35d287 100644 --- a/src/internal/connector/sharepoint/restore.go +++ b/src/internal/connector/sharepoint/restore.go @@ -36,6 +36,7 @@ import ( // RestoreCollections will restore the specified data collections into OneDrive func RestoreCollections( ctx context.Context, + backupVersion int, service graph.Servicer, dest control.RestoreDestination, dcs []data.Collection, @@ -61,6 +62,7 @@ func RestoreCollections( case path.LibrariesCategory: metrics, _, _, canceled = onedrive.RestoreCollection( ctx, + backupVersion, service, dc, []onedrive.UserPermission{}, // Currently permission data is not stored for sharepoint diff --git a/src/internal/operations/backup_integration_test.go b/src/internal/operations/backup_integration_test.go index a57a9d2be..5e9af1c46 100644 --- a/src/internal/operations/backup_integration_test.go +++ b/src/internal/operations/backup_integration_test.go @@ -339,7 +339,15 @@ func generateContainerOfItems( dest, collections) - deets, err := gc.RestoreDataCollections(ctx, acct, sel, dest, control.Options{RestorePermissions: true}, dataColls) + deets, err := gc.RestoreDataCollections( + ctx, + backup.Version, + acct, + sel, + dest, + control.Options{RestorePermissions: true}, + dataColls, + ) require.NoError(t, err) return deets diff --git a/src/internal/operations/restore.go b/src/internal/operations/restore.go index 206eb8026..cd52e5be3 100644 --- a/src/internal/operations/restore.go +++ b/src/internal/operations/restore.go @@ -219,6 +219,7 @@ func (op *RestoreOperation) do(ctx context.Context) (restoreDetails *details.Det restoreDetails, err = gc.RestoreDataCollections( ctx, + bup.Version, op.account, op.Selectors, op.Destination, diff --git a/src/pkg/backup/backup.go b/src/pkg/backup/backup.go index d0d9ddffd..4422b3a47 100644 --- a/src/pkg/backup/backup.go +++ b/src/pkg/backup/backup.go @@ -14,6 +14,8 @@ import ( "github.com/alcionai/corso/src/pkg/selectors" ) +const Version = 1 + // Backup represents the result of a backup operation type Backup struct { model.BaseModel @@ -32,6 +34,9 @@ type Backup struct { // Selector used in this operation Selector selectors.Selector `json:"selectors"` + // Version represents the version of the backup format + Version int `json:"version"` + // Errors contains all errors aggregated during a backup operation. Errors fault.ErrorsData `json:"errors"` @@ -67,6 +72,7 @@ func New( Errors: errs.Data(), ReadWrites: rw, StartAndEndTime: se, + Version: Version, } } From 76c2ac628b7b34f2b532b36ba7821f9f63cbc8b0 Mon Sep 17 00:00:00 2001 From: Abin Simon Date: Fri, 3 Feb 2023 10:38:28 +0530 Subject: [PATCH 28/40] Fix some edge cases around OneDrive permissions backup (#2370) ## Description This fixes tying to parse link shares as permissions and some error/retry handling. ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * # ## Test Plan - [x] :muscle: Manual - [x] :zap: Unit test - [ ] :green_heart: E2E --- CHANGELOG.md | 1 + src/internal/connector/onedrive/collection.go | 6 +- src/internal/connector/onedrive/item.go | 18 ++++- src/internal/connector/onedrive/item_test.go | 70 +++++++++++++++++++ 4 files changed, 90 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09d3845da..53ec01a36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - When the same user has permissions to a file and the containing folder, we only restore folder level permissions for the user and no separate file only permission is restored. +- Link shares are not restored ## [v0.2.0] (alpha) - 2023-1-29 diff --git a/src/internal/connector/onedrive/collection.go b/src/internal/connector/onedrive/collection.go index 3b8ff5cbb..2de05c073 100644 --- a/src/internal/connector/onedrive/collection.go +++ b/src/internal/connector/onedrive/collection.go @@ -292,7 +292,9 @@ func (oc *Collection) populateItems(ctx context.Context) { itemMeta, itemMetaSize, err = oc.itemMetaReader(ctx, oc.service, oc.driveID, item) // retry on Timeout type errors, break otherwise. - if err == nil || !graph.IsErrTimeout(err) { + if err == nil || + !graph.IsErrTimeout(err) || + !graph.IsInternalServerError(err) { break } @@ -302,7 +304,7 @@ func (oc *Collection) populateItems(ctx context.Context) { } if err != nil { - errUpdater(*item.GetId(), err) + errUpdater(*item.GetId(), errors.Wrap(err, "failed to get item permissions")) return } } diff --git a/src/internal/connector/onedrive/item.go b/src/internal/connector/onedrive/item.go index 1526f1401..c527ce09b 100644 --- a/src/internal/connector/onedrive/item.go +++ b/src/internal/connector/onedrive/item.go @@ -191,12 +191,24 @@ func oneDriveItemMetaInfo( perm, err := service.Client().DrivesById(driveID).ItemsById(*itemID).Permissions().Get(ctx, nil) if err != nil { - return Metadata{}, errors.Wrapf(err, "failed to get item permissions %s", *itemID) + return Metadata{}, err } + uperms := filterUserPermissions(perm.GetValue()) + + return Metadata{Permissions: uperms}, nil +} + +func filterUserPermissions(perms []models.Permissionable) []UserPermission { up := []UserPermission{} - for _, p := range perm.GetValue() { + for _, p := range perms { + if p.GetGrantedToV2() == nil { + // For link shares, we get permissions without a user + // specified + continue + } + roles := []string{} for _, r := range p.GetRoles() { @@ -218,7 +230,7 @@ func oneDriveItemMetaInfo( }) } - return Metadata{Permissions: up}, nil + return up } // sharePointItemInfo will populate a details.SharePointInfo struct diff --git a/src/internal/connector/onedrive/item_test.go b/src/internal/connector/onedrive/item_test.go index b0e42943a..a2e008ec5 100644 --- a/src/internal/connector/onedrive/item_test.go +++ b/src/internal/connector/onedrive/item_test.go @@ -8,6 +8,7 @@ import ( msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" "github.com/microsoftgraph/msgraph-sdk-go/models" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -257,3 +258,72 @@ func (suite *ItemIntegrationSuite) TestDriveGetFolder() { }) } } + +func getPermsUperms(permID, userID string, scopes []string) (models.Permissionable, UserPermission) { + identity := models.NewIdentity() + identity.SetAdditionalData(map[string]any{"email": &userID}) + + sharepointIdentity := models.NewSharePointIdentitySet() + sharepointIdentity.SetUser(identity) + + perm := models.NewPermission() + perm.SetId(&permID) + perm.SetRoles([]string{"read"}) + perm.SetGrantedToV2(sharepointIdentity) + + uperm := UserPermission{ + ID: permID, + Roles: []string{"read"}, + Email: userID, + } + + return perm, uperm +} + +func TestOneDrivePermissionsFilter(t *testing.T) { + permID := "fakePermId" + userID := "fakeuser@provider.com" + userID2 := "fakeuser2@provider.com" + + readPerm, readUperm := getPermsUperms(permID, userID, []string{"read"}) + readWritePerm, readWriteUperm := getPermsUperms(permID, userID2, []string{"read", "write"}) + + noPerm, _ := getPermsUperms(permID, userID, []string{"read"}) + noPerm.SetGrantedToV2(nil) // eg: link shares + + cases := []struct { + name string + graphPermissions []models.Permissionable + parsedPermissions []UserPermission + }{ + { + name: "no perms", + graphPermissions: []models.Permissionable{}, + parsedPermissions: []UserPermission{}, + }, + { + name: "no user bound to perms", + graphPermissions: []models.Permissionable{noPerm}, + parsedPermissions: []UserPermission{}, + }, + { + name: "user with read permissions", + graphPermissions: []models.Permissionable{readPerm}, + parsedPermissions: []UserPermission{readUperm}, + }, + { + name: "user with read and write permissions", + graphPermissions: []models.Permissionable{readWritePerm}, + parsedPermissions: []UserPermission{readWriteUperm}, + }, + { + name: "multiple users with separate permissions", + graphPermissions: []models.Permissionable{readPerm, readWritePerm}, + parsedPermissions: []UserPermission{readUperm, readWriteUperm}, + }, + } + for _, tc := range cases { + actual := filterUserPermissions(tc.graphPermissions) + assert.ElementsMatch(t, tc.parsedPermissions, actual) + } +} From 0be38909dd2fc4eba13f1aba08ba0298dfb953a7 Mon Sep 17 00:00:00 2001 From: Keepers Date: Thu, 2 Feb 2023 23:18:41 -0700 Subject: [PATCH 29/40] handle error from bu.backupCollections (#2386) ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :bug: Bugfix ## Test Plan - [x] :green_heart: E2E --- src/internal/operations/backup.go | 36 +++++++++++++++++------------- src/internal/operations/restore.go | 6 +++++ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/internal/operations/backup.go b/src/internal/operations/backup.go index e0df72d55..e6b8c767a 100644 --- a/src/internal/operations/backup.go +++ b/src/internal/operations/backup.go @@ -167,6 +167,12 @@ func (op *BackupOperation) do(ctx context.Context) (err error) { // persist operation results to the model store on exit defer func() { + // panic recovery here prevents additional errors in op.persistResults() + if r := recover(); r != nil { + err = clues.Wrap(r.(error), "panic recovery").WithClues(ctx).With("stacktrace", debug.Stack()) + return + } + err = op.persistResults(startTime, &opStats) if err != nil { op.Errors.Fail(errors.Wrap(err, "persisting backup results")) @@ -445,24 +451,22 @@ func consumeBackupDataCollections( cs, nil, tags, - isIncremental, - ) + isIncremental) + if err != nil { + if kopiaStats == nil { + return nil, nil, nil, err + } + + return nil, nil, nil, errors.Wrapf( + err, + "kopia snapshot failed with %v catastrophic errors and %v ignored errors", + kopiaStats.ErrorCount, kopiaStats.IgnoredErrorCount) + } if kopiaStats.ErrorCount > 0 || kopiaStats.IgnoredErrorCount > 0 { - if err != nil { - err = errors.Wrapf( - err, - "kopia snapshot failed with %v catastrophic errors and %v ignored errors", - kopiaStats.ErrorCount, - kopiaStats.IgnoredErrorCount, - ) - } else { - err = errors.Errorf( - "kopia snapshot failed with %v catastrophic errors and %v ignored errors", - kopiaStats.ErrorCount, - kopiaStats.IgnoredErrorCount, - ) - } + err = errors.Errorf( + "kopia snapshot failed with %v catastrophic errors and %v ignored errors", + kopiaStats.ErrorCount, kopiaStats.IgnoredErrorCount) } return kopiaStats, deets, itemsSourcedFromBase, err diff --git a/src/internal/operations/restore.go b/src/internal/operations/restore.go index cd52e5be3..f90e3c3c8 100644 --- a/src/internal/operations/restore.go +++ b/src/internal/operations/restore.go @@ -151,6 +151,12 @@ func (op *RestoreOperation) do(ctx context.Context) (restoreDetails *details.Det ) defer func() { + // panic recovery here prevents additional errors in op.persistResults() + if r := recover(); r != nil { + err = clues.Wrap(r.(error), "panic recovery").WithClues(ctx).With("stacktrace", debug.Stack()) + return + } + err = op.persistResults(ctx, startTime, &opStats) if err != nil { return From 8d01d1b397f19cb965015ccb820be7a6a6899fb1 Mon Sep 17 00:00:00 2001 From: Abin Simon Date: Fri, 3 Feb 2023 12:39:05 +0530 Subject: [PATCH 30/40] Disable OneDrive permissions backup by default (#2374) ## Description Since the backup permissions increases the backup time by a lot(mostly from higher number of requests and increased throttling), it was decided to disable it by default. It is still enabled in tests so as to make sure the code does not have regressions. ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [x] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * # ## Test Plan - [ ] :muscle: Manual - [ ] :zap: Unit test - [x] :green_heart: E2E --- src/cli/backup/onedrive.go | 2 +- src/cli/backup/onedrive_integration_test.go | 16 +++- src/cli/options/options.go | 22 +++--- .../connector/graph_connector_test.go | 74 +++++++++++++++---- src/internal/connector/onedrive/collection.go | 7 +- .../connector/onedrive/collection_test.go | 6 +- .../connector/onedrive/collections_test.go | 4 +- src/internal/connector/onedrive/drive_test.go | 2 +- .../operations/backup_integration_test.go | 2 +- src/pkg/control/options.go | 4 +- 10 files changed, 101 insertions(+), 38 deletions(-) diff --git a/src/cli/backup/onedrive.go b/src/cli/backup/onedrive.go index f74f98916..9dfb20b79 100644 --- a/src/cli/backup/onedrive.go +++ b/src/cli/backup/onedrive.go @@ -79,7 +79,7 @@ func addOneDriveCommands(cmd *cobra.Command) *cobra.Command { switch cmd.Use { case createCommand: c, fs = utils.AddCommand(cmd, oneDriveCreateCmd()) - options.AddFeatureToggle(cmd, options.DisablePermissionsBackup()) + options.AddFeatureToggle(cmd, options.EnablePermissionsBackup()) c.Use = c.Use + " " + oneDriveServiceCommandCreateUseSuffix c.Example = oneDriveServiceCommandCreateExamples diff --git a/src/cli/backup/onedrive_integration_test.go b/src/cli/backup/onedrive_integration_test.go index e24cba34f..05231fd11 100644 --- a/src/cli/backup/onedrive_integration_test.go +++ b/src/cli/backup/onedrive_integration_test.go @@ -72,7 +72,13 @@ func (suite *NoBackupOneDriveIntegrationSuite) SetupSuite() { suite.m365UserID = tester.M365UserID(t) // init the repo first - suite.repo, err = repository.Initialize(ctx, suite.acct, suite.st, control.Options{}) + suite.repo, err = repository.Initialize( + ctx, + suite.acct, + suite.st, + control.Options{ + ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}, + }) require.NoError(t, err) } @@ -152,7 +158,13 @@ func (suite *BackupDeleteOneDriveIntegrationSuite) SetupSuite() { defer flush() // init the repo first - suite.repo, err = repository.Initialize(ctx, suite.acct, suite.st, control.Options{}) + suite.repo, err = repository.Initialize( + ctx, + suite.acct, + suite.st, + control.Options{ + ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}, + }) require.NoError(t, err) m365UserID := tester.M365UserID(t) diff --git a/src/cli/options/options.go b/src/cli/options/options.go index 32defc5bb..2b423836c 100644 --- a/src/cli/options/options.go +++ b/src/cli/options/options.go @@ -15,7 +15,7 @@ func Control() control.Options { opt.DisableMetrics = noStats opt.RestorePermissions = restorePermissions opt.ToggleFeatures.DisableIncrementals = disableIncrementals - opt.ToggleFeatures.DisablePermissionsBackup = disablePermissionsBackup + opt.ToggleFeatures.EnablePermissionsBackup = enablePermissionsBackup return opt } @@ -48,6 +48,8 @@ func AddGlobalOperationFlags(cmd *cobra.Command) { func AddRestorePermissionsFlag(cmd *cobra.Command) { fs := cmd.Flags() fs.BoolVar(&restorePermissions, "restore-permissions", false, "Restore permissions for files and folders") + // TODO: reveal this flag once backing up permissions becomes default + cobra.CheckErr(fs.MarkHidden("restore-permissions")) } // --------------------------------------------------------------------------- @@ -55,8 +57,8 @@ func AddRestorePermissionsFlag(cmd *cobra.Command) { // --------------------------------------------------------------------------- var ( - disableIncrementals bool - disablePermissionsBackup bool + disableIncrementals bool + enablePermissionsBackup bool ) type exposeFeatureFlag func(*pflag.FlagSet) @@ -83,15 +85,15 @@ func DisableIncrementals() func(*pflag.FlagSet) { } } -// Adds the hidden '--disable-permissions-backup' cli flag which, when -// set, disables backing up permissions. -func DisablePermissionsBackup() func(*pflag.FlagSet) { +// Adds the hidden '--enable-permissions-backup' cli flag which, when +// set, enables backing up permissions. +func EnablePermissionsBackup() func(*pflag.FlagSet) { return func(fs *pflag.FlagSet) { fs.BoolVar( - &disablePermissionsBackup, - "disable-permissions-backup", + &enablePermissionsBackup, + "enable-permissions-backup", false, - "Disable backing up item permissions for OneDrive") - cobra.CheckErr(fs.MarkHidden("disable-permissions-backup")) + "Enable backing up item permissions for OneDrive") + cobra.CheckErr(fs.MarkHidden("enable-permissions-backup")) } } diff --git a/src/internal/connector/graph_connector_test.go b/src/internal/connector/graph_connector_test.go index 1cdb0b59e..b3b55a15e 100644 --- a/src/internal/connector/graph_connector_test.go +++ b/src/internal/connector/graph_connector_test.go @@ -238,7 +238,10 @@ func (suite *GraphConnectorIntegrationSuite) TestRestoreFailsBadService() { acct, sel, dest, - control.Options{}, + control.Options{ + RestorePermissions: true, + ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}, + }, nil, ) assert.Error(t, err) @@ -312,7 +315,10 @@ func (suite *GraphConnectorIntegrationSuite) TestEmptyCollections() { suite.acct, test.sel, dest, - control.Options{RestorePermissions: true}, + control.Options{ + RestorePermissions: true, + ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}, + }, test.col, ) require.NoError(t, err) @@ -448,7 +454,15 @@ func runRestoreBackupTest( t.Logf("Selective backup of %s\n", backupSel) start = time.Now() - dcs, excludes, err := backupGC.DataCollections(ctx, backupSel, nil, control.Options{RestorePermissions: true}) + dcs, excludes, err := backupGC.DataCollections( + ctx, + backupSel, + nil, + control.Options{ + RestorePermissions: true, + ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}, + }, + ) require.NoError(t, err) // No excludes yet because this isn't an incremental backup. assert.Empty(t, excludes) @@ -568,7 +582,15 @@ func runRestoreBackupTestVersion0( backupSel := backupSelectorForExpected(t, test.service, expectedDests) start = time.Now() - dcs, excludes, err := backupGC.DataCollections(ctx, backupSel, nil, control.Options{RestorePermissions: true}) + dcs, excludes, err := backupGC.DataCollections( + ctx, + backupSel, + nil, + control.Options{ + RestorePermissions: true, + ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}, + }, + ) require.NoError(t, err) // No excludes yet because this isn't an incremental backup. assert.Empty(t, excludes) @@ -1026,7 +1048,10 @@ func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackup() { test, suite.connector.tenant, []string{suite.user}, - control.Options{RestorePermissions: true}, + control.Options{ + RestorePermissions: true, + ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}, + }, ) }) } @@ -1275,7 +1300,10 @@ func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackupVersion0() { test, suite.connector.tenant, []string{suite.user}, - control.Options{RestorePermissions: true}, + control.Options{ + RestorePermissions: true, + ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}, + }, ) }) } @@ -1391,7 +1419,10 @@ func (suite *GraphConnectorIntegrationSuite) TestMultiFolderBackupDifferentNames suite.acct, restoreSel, dest, - control.Options{RestorePermissions: true}, + control.Options{ + RestorePermissions: true, + ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}, + }, collections, ) require.NoError(t, err) @@ -1414,7 +1445,15 @@ func (suite *GraphConnectorIntegrationSuite) TestMultiFolderBackupDifferentNames backupSel := backupSelectorForExpected(t, test.service, expectedDests) t.Log("Selective backup of", backupSel) - dcs, excludes, err := backupGC.DataCollections(ctx, backupSel, nil, control.Options{RestorePermissions: true}) + dcs, excludes, err := backupGC.DataCollections( + ctx, + backupSel, + nil, + control.Options{ + RestorePermissions: true, + ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}, + }, + ) require.NoError(t, err) // No excludes yet because this isn't an incremental backup. assert.Empty(t, excludes) @@ -1446,7 +1485,7 @@ func (suite *GraphConnectorIntegrationSuite) TestPermissionsRestoreAndBackup() { table := []restoreBackupInfo{ { - name: "FilePermissionsResote", + name: "FilePermissionsRestore", service: path.OneDriveService, resource: Users, collections: []colInfo{ @@ -1677,7 +1716,10 @@ func (suite *GraphConnectorIntegrationSuite) TestPermissionsRestoreAndBackup() { test, suite.connector.tenant, []string{suite.user}, - control.Options{RestorePermissions: true}, + control.Options{ + RestorePermissions: true, + ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}, + }, ) }) } @@ -1697,7 +1739,7 @@ func (suite *GraphConnectorIntegrationSuite) TestPermissionsBackupAndNoRestore() table := []restoreBackupInfo{ { - name: "FilePermissionsResote", + name: "FilePermissionsRestore", service: path.OneDriveService, resource: Users, collections: []colInfo{ @@ -1733,7 +1775,10 @@ func (suite *GraphConnectorIntegrationSuite) TestPermissionsBackupAndNoRestore() test, suite.connector.tenant, []string{suite.user}, - control.Options{RestorePermissions: false}, + control.Options{ + RestorePermissions: true, + ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}, + }, ) }) } @@ -1769,6 +1814,9 @@ func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackup_largeMailAttac test, suite.connector.tenant, []string{suite.user}, - control.Options{RestorePermissions: true}, + control.Options{ + RestorePermissions: true, + ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}, + }, ) } diff --git a/src/internal/connector/onedrive/collection.go b/src/internal/connector/onedrive/collection.go index 2de05c073..343a8911e 100644 --- a/src/internal/connector/onedrive/collection.go +++ b/src/internal/connector/onedrive/collection.go @@ -279,10 +279,11 @@ func (oc *Collection) populateItems(ctx context.Context) { if oc.source == OneDriveSource { // Fetch metadata for the file for i := 1; i <= maxRetries; i++ { - if oc.ctrl.ToggleFeatures.DisablePermissionsBackup { + if !oc.ctrl.ToggleFeatures.EnablePermissionsBackup { // We are still writing the metadata file but with - // empty permissions as we are not sure how the - // restore will be called. + // empty permissions as we don't have a way to + // signify that the permissions was explicitly + // not added. itemMeta = io.NopCloser(strings.NewReader("{}")) itemMetaSize = 2 diff --git a/src/internal/connector/onedrive/collection_test.go b/src/internal/connector/onedrive/collection_test.go index b8e5fe446..734009d72 100644 --- a/src/internal/connector/onedrive/collection_test.go +++ b/src/internal/connector/onedrive/collection_test.go @@ -168,7 +168,7 @@ func (suite *CollectionUnitTestSuite) TestCollection() { suite, suite.testStatusUpdater(&wg, &collStatus), test.source, - control.Options{}) + control.Options{ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}}) require.NotNil(t, coll) assert.Equal(t, folderPath, coll.FullPath()) @@ -301,7 +301,7 @@ func (suite *CollectionUnitTestSuite) TestCollectionReadError() { suite, suite.testStatusUpdater(&wg, &collStatus), test.source, - control.Options{}) + control.Options{ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}}) mockItem := models.NewDriveItem() mockItem.SetId(&testItemID) @@ -372,7 +372,7 @@ func (suite *CollectionUnitTestSuite) TestCollectionDisablePermissionsBackup() { suite, suite.testStatusUpdater(&wg, &collStatus), test.source, - control.Options{ToggleFeatures: control.Toggles{DisablePermissionsBackup: true}}) + control.Options{ToggleFeatures: control.Toggles{}}) now := time.Now() mockItem := models.NewDriveItem() diff --git a/src/internal/connector/onedrive/collections_test.go b/src/internal/connector/onedrive/collections_test.go index 3316a10c5..f784bad62 100644 --- a/src/internal/connector/onedrive/collections_test.go +++ b/src/internal/connector/onedrive/collections_test.go @@ -635,7 +635,7 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { testFolderMatcher{tt.scope}, &MockGraphService{}, nil, - control.Options{}) + control.Options{ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}}) err := c.UpdateCollections( ctx, @@ -1380,7 +1380,7 @@ func (suite *OneDriveCollectionsSuite) TestGet() { testFolderMatcher{anyFolder}, &MockGraphService{}, func(*support.ConnectorOperationStatus) {}, - control.Options{}, + control.Options{ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}}, ) c.drivePagerFunc = drivePagerFunc c.itemPagerFunc = itemPagerFunc diff --git a/src/internal/connector/onedrive/drive_test.go b/src/internal/connector/onedrive/drive_test.go index 0ba3ec1c2..a67c89ab1 100644 --- a/src/internal/connector/onedrive/drive_test.go +++ b/src/internal/connector/onedrive/drive_test.go @@ -462,7 +462,7 @@ func (suite *OneDriveSuite) TestOneDriveNewCollections() { testFolderMatcher{scope}, service, service.updateStatus, - control.Options{}, + control.Options{ToggleFeatures: control.Toggles{EnablePermissionsBackup: true}}, ).Get(ctx, nil) assert.NoError(t, err) // Don't expect excludes as this isn't an incremental backup. diff --git a/src/internal/operations/backup_integration_test.go b/src/internal/operations/backup_integration_test.go index 5e9af1c46..b3ea617d9 100644 --- a/src/internal/operations/backup_integration_test.go +++ b/src/internal/operations/backup_integration_test.go @@ -1081,7 +1081,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_oneDrive() { sel.Include(sel.AllData()) - bo, _, _, _, closer := prepNewTestBackupOp(t, ctx, mb, sel.Selector, control.Toggles{}) + bo, _, _, _, closer := prepNewTestBackupOp(t, ctx, mb, sel.Selector, control.Toggles{EnablePermissionsBackup: true}) defer closer() runAndCheckBackup(t, ctx, &bo, mb) diff --git a/src/pkg/control/options.go b/src/pkg/control/options.go index 6f53839ca..7348e8fa6 100644 --- a/src/pkg/control/options.go +++ b/src/pkg/control/options.go @@ -76,8 +76,8 @@ type Toggles struct { // forcing a new, complete backup of all data regardless of prior state. DisableIncrementals bool `json:"exchangeIncrementals,omitempty"` - // DisablePermissionsBackup is used to disable backups of item + // EnablePermissionsBackup is used to enable backups of item // permissions. Permission metadata increases graph api call count, // so disabling their retrieval when not needed is advised. - DisablePermissionsBackup bool `json:"disablePermissionsBackup,omitempty"` + EnablePermissionsBackup bool `json:"enablePermissionsBackup,omitempty"` } From 272a8b30fe05afbb62c20b651a0d264f32ed52b4 Mon Sep 17 00:00:00 2001 From: Abin Simon Date: Fri, 3 Feb 2023 13:37:03 +0530 Subject: [PATCH 31/40] Run gci on beta package (#2368) ## Description Followup to https://github.com/alcionai/corso/pull/2340 . Runs `gci write --skip-generated -s 'standard,default,prefix(github.com/alcionai/corso)'` to properly order headers. This is generated files as mentioned in the previous PR, but I thought we might as well order the headers as well if we are formatting it. Let me know if leaving this out was intentional though. ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [x] :broom: Tech Debt/Cleanup --- src/internal/connector/discovery/api/beta_service.go | 3 ++- src/internal/connector/exchange/api/contacts.go | 2 +- src/internal/connector/exchange/api/events.go | 2 +- src/internal/connector/exchange/api/mail.go | 2 +- src/internal/connector/graph/betasdk/beta_client.go | 3 ++- .../graph/betasdk/models/site_page_collection_response.go | 1 - ...web_parts_item_get_position_of_web_part_request_builder.go | 3 ++- .../sites/item_pages_item_web_parts_request_builder.go | 3 ++- ...item_pages_item_web_parts_web_part_item_request_builder.go | 4 ++-- .../graph/betasdk/sites/item_pages_request_builder.go | 3 ++- src/pkg/logger/logger.go | 3 ++- 11 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/internal/connector/discovery/api/beta_service.go b/src/internal/connector/discovery/api/beta_service.go index df2b1533b..5ff65ac77 100644 --- a/src/internal/connector/discovery/api/beta_service.go +++ b/src/internal/connector/discovery/api/beta_service.go @@ -1,10 +1,11 @@ package api import ( - "github.com/alcionai/corso/src/internal/connector/graph/betasdk" absser "github.com/microsoft/kiota-abstractions-go/serialization" msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" "github.com/pkg/errors" + + "github.com/alcionai/corso/src/internal/connector/graph/betasdk" ) // Service wraps BetaClient's functionality. diff --git a/src/internal/connector/exchange/api/contacts.go b/src/internal/connector/exchange/api/contacts.go index 185bf6e22..458f364d1 100644 --- a/src/internal/connector/exchange/api/contacts.go +++ b/src/internal/connector/exchange/api/contacts.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/alcionai/clues" "github.com/hashicorp/go-multierror" "github.com/microsoft/kiota-abstractions-go/serialization" kioser "github.com/microsoft/kiota-serialization-json-go" @@ -12,7 +13,6 @@ import ( "github.com/microsoftgraph/msgraph-sdk-go/users" "github.com/pkg/errors" - "github.com/alcionai/clues" "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph/api" "github.com/alcionai/corso/src/internal/connector/support" diff --git a/src/internal/connector/exchange/api/events.go b/src/internal/connector/exchange/api/events.go index 810c1a2ff..70a1a45e9 100644 --- a/src/internal/connector/exchange/api/events.go +++ b/src/internal/connector/exchange/api/events.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/alcionai/clues" "github.com/hashicorp/go-multierror" "github.com/microsoft/kiota-abstractions-go/serialization" kioser "github.com/microsoft/kiota-serialization-json-go" @@ -12,7 +13,6 @@ import ( "github.com/microsoftgraph/msgraph-sdk-go/users" "github.com/pkg/errors" - "github.com/alcionai/clues" "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph/api" diff --git a/src/internal/connector/exchange/api/mail.go b/src/internal/connector/exchange/api/mail.go index 6a863322e..6acc05162 100644 --- a/src/internal/connector/exchange/api/mail.go +++ b/src/internal/connector/exchange/api/mail.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/alcionai/clues" "github.com/hashicorp/go-multierror" "github.com/microsoft/kiota-abstractions-go/serialization" kioser "github.com/microsoft/kiota-serialization-json-go" @@ -12,7 +13,6 @@ import ( "github.com/microsoftgraph/msgraph-sdk-go/users" "github.com/pkg/errors" - "github.com/alcionai/clues" "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph/api" "github.com/alcionai/corso/src/internal/connector/support" diff --git a/src/internal/connector/graph/betasdk/beta_client.go b/src/internal/connector/graph/betasdk/beta_client.go index ef77b8169..f79ab8974 100644 --- a/src/internal/connector/graph/betasdk/beta_client.go +++ b/src/internal/connector/graph/betasdk/beta_client.go @@ -1,13 +1,14 @@ package betasdk import ( - i1a3c1a5501c5e41b7fd169f2d4c768dce9b096ac28fb5431bf02afcc57295411 "github.com/alcionai/corso/src/internal/connector/graph/betasdk/sites" absser "github.com/microsoft/kiota-abstractions-go" kioser "github.com/microsoft/kiota-abstractions-go/serialization" kform "github.com/microsoft/kiota-serialization-form-go" kw "github.com/microsoft/kiota-serialization-json-go" ktext "github.com/microsoft/kiota-serialization-text-go" msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" + + i1a3c1a5501c5e41b7fd169f2d4c768dce9b096ac28fb5431bf02afcc57295411 "github.com/alcionai/corso/src/internal/connector/graph/betasdk/sites" ) // BetaClient the main entry point of the SDK, exposes the configuration and the fluent API. diff --git a/src/internal/connector/graph/betasdk/models/site_page_collection_response.go b/src/internal/connector/graph/betasdk/models/site_page_collection_response.go index f66cdafdf..bbd79c3a4 100644 --- a/src/internal/connector/graph/betasdk/models/site_page_collection_response.go +++ b/src/internal/connector/graph/betasdk/models/site_page_collection_response.go @@ -2,7 +2,6 @@ package models import ( i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91 "github.com/microsoft/kiota-abstractions-go/serialization" - msmodel "github.com/microsoftgraph/msgraph-sdk-go/models" ) diff --git a/src/internal/connector/graph/betasdk/sites/item_pages_item_web_parts_item_get_position_of_web_part_request_builder.go b/src/internal/connector/graph/betasdk/sites/item_pages_item_web_parts_item_get_position_of_web_part_request_builder.go index 4bb325673..9db79ace5 100644 --- a/src/internal/connector/graph/betasdk/sites/item_pages_item_web_parts_item_get_position_of_web_part_request_builder.go +++ b/src/internal/connector/graph/betasdk/sites/item_pages_item_web_parts_item_get_position_of_web_part_request_builder.go @@ -3,9 +3,10 @@ package sites import ( "context" - ifda19816f54f079134d70c11e75d6b26799300cf72079e282f1d3bb9a6750354 "github.com/alcionai/corso/src/internal/connector/graph/betasdk/models" i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f "github.com/microsoft/kiota-abstractions-go" i7ad325c11fbf3db4d761c429267362d8b24daa1eda0081f914ebc3cdc85181a0 "github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors" + + ifda19816f54f079134d70c11e75d6b26799300cf72079e282f1d3bb9a6750354 "github.com/alcionai/corso/src/internal/connector/graph/betasdk/models" ) // ItemPagesItemWebPartsItemGetPositionOfWebPartRequestBuilder provides operations to call the getPositionOfWebPart method. diff --git a/src/internal/connector/graph/betasdk/sites/item_pages_item_web_parts_request_builder.go b/src/internal/connector/graph/betasdk/sites/item_pages_item_web_parts_request_builder.go index 0e349df74..e2e32c640 100644 --- a/src/internal/connector/graph/betasdk/sites/item_pages_item_web_parts_request_builder.go +++ b/src/internal/connector/graph/betasdk/sites/item_pages_item_web_parts_request_builder.go @@ -3,9 +3,10 @@ package sites import ( "context" - ifda19816f54f079134d70c11e75d6b26799300cf72079e282f1d3bb9a6750354 "github.com/alcionai/corso/src/internal/connector/graph/betasdk/models" i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f "github.com/microsoft/kiota-abstractions-go" i7ad325c11fbf3db4d761c429267362d8b24daa1eda0081f914ebc3cdc85181a0 "github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors" + + ifda19816f54f079134d70c11e75d6b26799300cf72079e282f1d3bb9a6750354 "github.com/alcionai/corso/src/internal/connector/graph/betasdk/models" ) // ItemPagesItemWebPartsRequestBuilder provides operations to manage the webParts property of the microsoft.graph.sitePage entity. diff --git a/src/internal/connector/graph/betasdk/sites/item_pages_item_web_parts_web_part_item_request_builder.go b/src/internal/connector/graph/betasdk/sites/item_pages_item_web_parts_web_part_item_request_builder.go index 25dba98cf..1c16fc8df 100644 --- a/src/internal/connector/graph/betasdk/sites/item_pages_item_web_parts_web_part_item_request_builder.go +++ b/src/internal/connector/graph/betasdk/sites/item_pages_item_web_parts_web_part_item_request_builder.go @@ -3,10 +3,10 @@ package sites import ( "context" - ifda19816f54f079134d70c11e75d6b26799300cf72079e282f1d3bb9a6750354 "github.com/alcionai/corso/src/internal/connector/graph/betasdk/models" i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f "github.com/microsoft/kiota-abstractions-go" - i7ad325c11fbf3db4d761c429267362d8b24daa1eda0081f914ebc3cdc85181a0 "github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors" + + ifda19816f54f079134d70c11e75d6b26799300cf72079e282f1d3bb9a6750354 "github.com/alcionai/corso/src/internal/connector/graph/betasdk/models" ) // ItemPagesItemWebPartsWebPartItemRequestBuilder provides operations to manage the webParts property of the microsoft.graph.sitePage entity. diff --git a/src/internal/connector/graph/betasdk/sites/item_pages_request_builder.go b/src/internal/connector/graph/betasdk/sites/item_pages_request_builder.go index 43f503439..6c82f58df 100644 --- a/src/internal/connector/graph/betasdk/sites/item_pages_request_builder.go +++ b/src/internal/connector/graph/betasdk/sites/item_pages_request_builder.go @@ -3,9 +3,10 @@ package sites import ( "context" - ifda19816f54f079134d70c11e75d6b26799300cf72079e282f1d3bb9a6750354 "github.com/alcionai/corso/src/internal/connector/graph/betasdk/models" i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f "github.com/microsoft/kiota-abstractions-go" i7ad325c11fbf3db4d761c429267362d8b24daa1eda0081f914ebc3cdc85181a0 "github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors" + + ifda19816f54f079134d70c11e75d6b26799300cf72079e282f1d3bb9a6750354 "github.com/alcionai/corso/src/internal/connector/graph/betasdk/models" ) // ItemPagesRequestBuilder provides operations to manage the pages property of the microsoft.graph.site entity. diff --git a/src/pkg/logger/logger.go b/src/pkg/logger/logger.go index abbce9119..a6b5aa4dd 100644 --- a/src/pkg/logger/logger.go +++ b/src/pkg/logger/logger.go @@ -7,11 +7,12 @@ import ( "time" "github.com/alcionai/clues" - "github.com/alcionai/corso/src/cli/print" "github.com/spf13/cobra" "github.com/spf13/pflag" "go.uber.org/zap" "go.uber.org/zap/zapcore" + + "github.com/alcionai/corso/src/cli/print" ) // Default location for writing logs, initialized in platform specific files From 6e12885787f18ea8c73bdd93f9830ffe37644500 Mon Sep 17 00:00:00 2001 From: Danny Date: Fri, 3 Feb 2023 13:37:45 -0500 Subject: [PATCH 32/40] GC: SharePoint: BackUp: Pages (#2178) ## Description - Adds logic to retrieve `SharePoint.Pages` from M365 - Anchor PR for `SharePoint.Pages` feature support. Restore Pipeline PR to remain in Draft to ensure PR Train is stable until the solution to #2174 is implemented. ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :sunflower: Feature ## Issue(s) * related to #2173 * related to #2071 ## Test Plan - [x] :zap: Unit test NOTE: Tests will fail in CI due to complications with #2086. --- .../connector/discovery/api/beta_service.go | 10 +- src/internal/connector/sharepoint/api/api.go | 6 ++ .../connector/sharepoint/api/helper_test.go | 21 +++++ .../connector/sharepoint/api/pages.go | 93 +++++++++++++++++++ .../connector/sharepoint/api/pages_test.go | 71 ++++++++++++++ .../connector/sharepoint/collection.go | 2 + .../connector/sharepoint/collection_test.go | 64 ++++++++----- .../connector/sharepoint/data_collections.go | 52 +++++++++++ .../connector/sharepoint/helper_test.go | 13 +-- .../connector/sharepoint/list_test.go | 4 +- 10 files changed, 301 insertions(+), 35 deletions(-) create mode 100644 src/internal/connector/sharepoint/api/api.go create mode 100644 src/internal/connector/sharepoint/api/helper_test.go create mode 100644 src/internal/connector/sharepoint/api/pages.go create mode 100644 src/internal/connector/sharepoint/api/pages_test.go diff --git a/src/internal/connector/discovery/api/beta_service.go b/src/internal/connector/discovery/api/beta_service.go index 5ff65ac77..0208ace69 100644 --- a/src/internal/connector/discovery/api/beta_service.go +++ b/src/internal/connector/discovery/api/beta_service.go @@ -11,23 +11,23 @@ import ( // Service wraps BetaClient's functionality. // Abstraction created to comply loosely with graph.Servicer // methods for ease of switching between v1.0 and beta connnectors -type Service struct { +type BetaService struct { client *betasdk.BetaClient } -func (s Service) Client() *betasdk.BetaClient { +func (s BetaService) Client() *betasdk.BetaClient { return s.client } -func NewBetaService(adpt *msgraphsdk.GraphRequestAdapter) *Service { - return &Service{ +func NewBetaService(adpt *msgraphsdk.GraphRequestAdapter) *BetaService { + return &BetaService{ client: betasdk.NewBetaClient(adpt), } } // Seraialize writes an M365 parsable object into a byte array using the built-in // application/json writer within the adapter. -func (s Service) Serialize(object absser.Parsable) ([]byte, error) { +func (s BetaService) Serialize(object absser.Parsable) ([]byte, error) { writer, err := s.client.Adapter(). GetSerializationWriterFactory(). GetSerializationWriter("application/json") diff --git a/src/internal/connector/sharepoint/api/api.go b/src/internal/connector/sharepoint/api/api.go new file mode 100644 index 000000000..c05eaad6b --- /dev/null +++ b/src/internal/connector/sharepoint/api/api.go @@ -0,0 +1,6 @@ +package api + +type Tuple struct { + Name string + ID string +} diff --git a/src/internal/connector/sharepoint/api/helper_test.go b/src/internal/connector/sharepoint/api/helper_test.go new file mode 100644 index 000000000..631dd7b3b --- /dev/null +++ b/src/internal/connector/sharepoint/api/helper_test.go @@ -0,0 +1,21 @@ +package api + +import ( + "testing" + + "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 { + adapter, err := graph.CreateAdapter( + credentials.AzureTenantID, + credentials.AzureClientID, + credentials.AzureClientSecret, + ) + require.NoError(t, err) + + return api.NewBetaService(adapter) +} diff --git a/src/internal/connector/sharepoint/api/pages.go b/src/internal/connector/sharepoint/api/pages.go new file mode 100644 index 000000000..a2232140c --- /dev/null +++ b/src/internal/connector/sharepoint/api/pages.go @@ -0,0 +1,93 @@ +package api + +import ( + "context" + + "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" +) + +// 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, + siteID string, + pages []string, +) ([]models.SitePageable, error) { + col := make([]models.SitePageable, 0) + opts := retrieveSitePageOptions() + + for _, entry := range pages { + page, err := serv.Client().SitesById(siteID).PagesById(entry).Get(ctx, opts) + if err != nil { + return nil, support.ConnectorStackErrorTraceWrap(err, "fetching page: "+entry) + } + + col = append(col, page) + } + + return col, nil +} + +// fetchPages utility function to return the tuple of item +func FetchPages(ctx context.Context, bs *api.BetaService, siteID string) ([]Tuple, error) { + var ( + builder = bs.Client().SitesById(siteID).Pages() + opts = fetchPageOptions() + pageTuples = make([]Tuple, 0) + ) + + for { + resp, err := builder.Get(ctx, opts) + if err != nil { + return nil, support.ConnectorStackErrorTraceWrap(err, "failed fetching site page") + } + + for _, entry := range resp.GetValue() { + pid := *entry.GetId() + temp := Tuple{pid, pid} + + if entry.GetName() != nil { + temp.Name = *entry.GetName() + } + + pageTuples = append(pageTuples, temp) + } + + if resp.GetOdataNextLink() == nil { + break + } + + builder = sites.NewItemPagesRequestBuilder(*resp.GetOdataNextLink(), bs.Client().Adapter()) + } + + return pageTuples, nil +} + +// fetchPageOptions is used to return minimal information reltating to Site Pages +// Pages API: https://learn.microsoft.com/en-us/graph/api/resources/sitepage?view=graph-rest-beta +func fetchPageOptions() *sites.ItemPagesRequestBuilderGetRequestConfiguration { + fields := []string{"id", "name"} + options := &sites.ItemPagesRequestBuilderGetRequestConfiguration{ + QueryParameters: &sites.ItemPagesRequestBuilderGetQueryParameters{ + Select: fields, + }, + } + + return options +} + +// retrievePageOptions returns options to expand +func retrieveSitePageOptions() *sites.ItemPagesSitePageItemRequestBuilderGetRequestConfiguration { + fields := []string{"canvasLayout"} + options := &sites.ItemPagesSitePageItemRequestBuilderGetRequestConfiguration{ + QueryParameters: &sites.ItemPagesSitePageItemRequestBuilderGetQueryParameters{ + Expand: fields, + }, + } + + return options +} diff --git a/src/internal/connector/sharepoint/api/pages_test.go b/src/internal/connector/sharepoint/api/pages_test.go new file mode 100644 index 000000000..ecc2cf18d --- /dev/null +++ b/src/internal/connector/sharepoint/api/pages_test.go @@ -0,0 +1,71 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/internal/tester" + "github.com/alcionai/corso/src/pkg/account" +) + +type SharePointPageSuite struct { + suite.Suite + siteID string + creds account.M365Config +} + +func (suite *SharePointPageSuite) SetupSuite() { + t := suite.T() + tester.MustGetEnvSets(t, tester.M365AcctCredEnvs) + + suite.siteID = tester.M365SiteID(t) + a := tester.NewM365Account(t) + m365, err := a.M365Config() + require.NoError(t, err) + + suite.creds = m365 +} + +func TestSharePointPageSuite(t *testing.T) { + tester.RunOnAny( + t, + tester.CorsoCITests, + tester.CorsoGraphConnectorSharePointTests) + suite.Run(t, new(SharePointPageSuite)) +} + +func (suite *SharePointPageSuite) TestFetchPages() { + ctx, flush := tester.NewContext() + defer flush() + + t := suite.T() + service := createTestBetaService(t, suite.creds) + + pgs, err := FetchPages(ctx, service, suite.siteID) + assert.NoError(t, err) + require.NotNil(t, pgs) + assert.NotZero(t, len(pgs)) + + for _, entry := range pgs { + t.Logf("id: %s\t name: %s\n", entry.ID, entry.Name) + } +} + +func (suite *SharePointPageSuite) TestGetSitePage() { + ctx, flush := tester.NewContext() + defer flush() + + t := suite.T() + service := createTestBetaService(t, suite.creds) + tuples, err := FetchPages(ctx, service, suite.siteID) + require.NoError(t, err) + require.NotNil(t, tuples) + + jobs := []string{tuples[0].ID} + pages, err := GetSitePage(ctx, service, suite.siteID, jobs) + assert.NoError(t, err) + assert.NotEmpty(t, pages) +} diff --git a/src/internal/connector/sharepoint/collection.go b/src/internal/connector/sharepoint/collection.go index c34d2a2d1..c540af4e6 100644 --- a/src/internal/connector/sharepoint/collection.go +++ b/src/internal/connector/sharepoint/collection.go @@ -9,6 +9,7 @@ import ( kw "github.com/microsoft/kiota-serialization-json-go" "github.com/microsoftgraph/msgraph-sdk-go/models" + "github.com/alcionai/corso/src/internal/connector/discovery/api" "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/data" @@ -46,6 +47,7 @@ type Collection struct { jobs []string // M365 IDs of the items of this collection service graph.Servicer + betaService *api.BetaService statusUpdater support.StatusUpdater } diff --git a/src/internal/connector/sharepoint/collection_test.go b/src/internal/connector/sharepoint/collection_test.go index f049ab26f..c2b1ac830 100644 --- a/src/internal/connector/sharepoint/collection_test.go +++ b/src/internal/connector/sharepoint/collection_test.go @@ -17,11 +17,27 @@ import ( "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/tester" + "github.com/alcionai/corso/src/pkg/account" + "github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/path" ) type SharePointCollectionSuite struct { suite.Suite + siteID string + creds account.M365Config +} + +func (suite *SharePointCollectionSuite) SetupSuite() { + t := suite.T() + tester.MustGetEnvSets(t, tester.M365AcctCredEnvs) + + suite.siteID = tester.M365SiteID(t) + a := tester.NewM365Account(t) + m365, err := a.M365Config() + require.NoError(t, err) + + suite.creds = m365 } func TestSharePointCollectionSuite(t *testing.T) { @@ -95,20 +111,33 @@ func (suite *SharePointCollectionSuite) TestSharePointListCollection() { assert.Equal(t, testName, shareInfo.Info().SharePoint.ItemName) } +func (suite *SharePointCollectionSuite) TestCollectPages() { + ctx, flush := tester.NewContext() + defer flush() + + t := suite.T() + col, err := collectPages( + ctx, + suite.creds, + nil, + account.AzureTenantID, + suite.siteID, + nil, + &MockGraphService{}, + control.Defaults(), + ) + assert.NoError(t, err) + assert.NotEmpty(t, col) +} + // TestRestoreListCollection verifies Graph Restore API for the List Collection func (suite *SharePointCollectionSuite) TestRestoreListCollection() { ctx, flush := tester.NewContext() defer flush() t := suite.T() - siteID := tester.M365SiteID(t) - a := tester.NewM365Account(t) - account, err := a.M365Config() - require.NoError(t, err) - - service, err := createTestService(account) - require.NoError(t, err) + service := createTestService(t, suite.creds) listing := mockconnector.GetMockListDefault("Mock List") testName := "MockListing" listing.SetDisplayName(&testName) @@ -123,13 +152,13 @@ func (suite *SharePointCollectionSuite) TestRestoreListCollection() { destName := "Corso_Restore_" + common.FormatNow(common.SimpleTimeTesting) - deets, err := restoreListItem(ctx, service, listData, siteID, destName) + deets, err := restoreListItem(ctx, service, listData, suite.siteID, destName) assert.NoError(t, err) t.Logf("List created: %s\n", deets.SharePoint.ItemName) // Clean-Up var ( - builder = service.Client().SitesById(siteID).Lists() + builder = service.Client().SitesById(suite.siteID).Lists() isFound bool deleteID string ) @@ -156,7 +185,7 @@ func (suite *SharePointCollectionSuite) TestRestoreListCollection() { } if isFound { - err := DeleteList(ctx, service, siteID, deleteID) + err := DeleteList(ctx, service, suite.siteID, deleteID) assert.NoError(t, err) } } @@ -168,25 +197,18 @@ func (suite *SharePointCollectionSuite) TestRestoreLocation() { defer flush() t := suite.T() - a := tester.NewM365Account(t) - account, err := a.M365Config() - require.NoError(t, err) - - service, err := createTestService(account) - require.NoError(t, err) + service := createTestService(t, suite.creds) rootFolder := "General_" + common.FormatNow(common.SimpleTimeTesting) - siteID := tester.M365SiteID(t) - - folderID, err := createRestoreFolders(ctx, service, siteID, []string{rootFolder}) + folderID, err := createRestoreFolders(ctx, service, suite.siteID, []string{rootFolder}) assert.NoError(t, err) t.Log("FolderID: " + folderID) - _, err = createRestoreFolders(ctx, service, siteID, []string{rootFolder, "Tsao"}) + _, err = createRestoreFolders(ctx, service, suite.siteID, []string{rootFolder, "Tsao"}) assert.NoError(t, err) // CleanUp - siteDrive, err := service.Client().SitesById(siteID).Drive().Get(ctx, nil) + siteDrive, err := service.Client().SitesById(suite.siteID).Drive().Get(ctx, nil) require.NoError(t, err) driveID := *siteDrive.GetId() diff --git a/src/internal/connector/sharepoint/data_collections.go b/src/internal/connector/sharepoint/data_collections.go index 1fd2f786d..88e16882c 100644 --- a/src/internal/connector/sharepoint/data_collections.go +++ b/src/internal/connector/sharepoint/data_collections.go @@ -6,11 +6,14 @@ import ( "github.com/pkg/errors" + "github.com/alcionai/corso/src/internal/connector/discovery/api" "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/onedrive" + sapi "github.com/alcionai/corso/src/internal/connector/sharepoint/api" "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/observe" + "github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/path" @@ -162,6 +165,55 @@ func collectLibraries( return append(collections, odcs...), excludes, errs } +// collectPages constructs a sharepoint Collections struct and Get()s the associated +// M365 IDs for the associated Pages +func collectPages( + ctx context.Context, + creds account.M365Config, + serv graph.Servicer, + tenantID, siteID string, + scope selectors.SharePointScope, + updater statusUpdater, + ctrlOpts control.Options, +) ([]data.Collection, error) { + logger.Ctx(ctx).With("site", siteID).Debug("Creating SharePoint Pages collections") + + spcs := make([]data.Collection, 0) + + // make the betaClient + adpt, err := graph.CreateAdapter(creds.AzureTenantID, creds.AzureClientID, creds.AzureClientSecret) + if err != nil { + return nil, errors.Wrap(err, "adapter for betaservice not created") + } + + betaService := api.NewBetaService(adpt) + + tuples, err := sapi.FetchPages(ctx, betaService, siteID) + if err != nil { + return nil, err + } + + for _, tuple := range tuples { + dir, err := path.Builder{}.Append(tuple.Name). + ToDataLayerSharePointPath( + tenantID, + siteID, + path.PagesCategory, + false) + if err != nil { + return nil, errors.Wrapf(err, "failed to create collection path for site: %s", siteID) + } + + collection := NewCollection(dir, serv, updater.UpdateStatus) + collection.betaService = betaService + collection.AddJob(tuple.ID) + + spcs = append(spcs, collection) + } + + return spcs, nil +} + type folderMatcher struct { scope selectors.SharePointScope } diff --git a/src/internal/connector/sharepoint/helper_test.go b/src/internal/connector/sharepoint/helper_test.go index e716a5bae..30d589389 100644 --- a/src/internal/connector/sharepoint/helper_test.go +++ b/src/internal/connector/sharepoint/helper_test.go @@ -4,11 +4,11 @@ import ( "testing" msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" - "github.com/pkg/errors" "github.com/stretchr/testify/require" "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/onedrive" + "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/pkg/account" ) @@ -29,21 +29,22 @@ func (ms *MockGraphService) Adapter() *msgraphsdk.GraphRequestAdapter { return nil } +func (ms *MockGraphService) UpdateStatus(*support.ConnectorOperationStatus) { +} + // --------------------------------------------------------------------------- // Helper Functions // --------------------------------------------------------------------------- -func createTestService(credentials account.M365Config) (*graph.Service, error) { +func createTestService(t *testing.T, credentials account.M365Config) *graph.Service { adapter, err := graph.CreateAdapter( credentials.AzureTenantID, credentials.AzureClientID, credentials.AzureClientSecret, ) - if err != nil { - return nil, errors.Wrap(err, "creating microsoft graph service for exchange") - } + require.NoError(t, err, "creating microsoft graph service for exchange") - return graph.NewService(adapter), nil + return graph.NewService(adapter) } func expectedPathAsSlice(t *testing.T, tenant, user string, rest ...string) []string { diff --git a/src/internal/connector/sharepoint/list_test.go b/src/internal/connector/sharepoint/list_test.go index c798be368..2571c9183 100644 --- a/src/internal/connector/sharepoint/list_test.go +++ b/src/internal/connector/sharepoint/list_test.go @@ -49,9 +49,7 @@ func (suite *SharePointSuite) TestLoadList() { defer flush() t := suite.T() - service, err := createTestService(suite.creds) - require.NoError(t, err) - + service := createTestService(t, suite.creds) tuples, err := preFetchLists(ctx, service, "root") require.NoError(t, err) From 35d89427ce577f9b4ebb229a6c8ec2e0f1c47425 Mon Sep 17 00:00:00 2001 From: Danny Date: Fri, 3 Feb 2023 17:02:50 -0500 Subject: [PATCH 33/40] GC: Restore: `event.item` attachment support (#2355) ## Description `Item.Attachments` of OdataType `Event` require special transformations prior to being uploaded. ## Does this PR need a docs update or release note? - [x] :white_check_mark: Yes, it's included ## Type of change - [x] :sunflower: Feature ## Issue(s) *closes #2353 ## Test Plan - [x] :zap: Unit test --- src/internal/connector/exchange/attachment.go | 37 ++-- .../connector/exchange/restore_test.go | 16 +- .../connector/exchange/service_restore.go | 38 ++-- .../mockconnector/mock_data_message.go | 186 ++++++++++++++++++ .../connector/support/m365Transform.go | 90 +++++++++ 5 files changed, 337 insertions(+), 30 deletions(-) diff --git a/src/internal/connector/exchange/attachment.go b/src/internal/connector/exchange/attachment.go index 6ed05b5df..94e6dbc6a 100644 --- a/src/internal/connector/exchange/attachment.go +++ b/src/internal/connector/exchange/attachment.go @@ -8,6 +8,7 @@ import ( "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/pkg/errors" + "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/uploadsession" "github.com/alcionai/corso/src/pkg/logger" ) @@ -44,8 +45,11 @@ func uploadAttachment( attachment models.Attachmentable, ) error { logger.Ctx(ctx).Debugf("uploading attachment with size %d", *attachment.GetSize()) - attachmentType := attachmentType(attachment) + var ( + attachmentType = attachmentType(attachment) + err error + ) // Reference attachments that are inline() do not need to be recreated. The contents are part of the body. if attachmentType == models.REFERENCE_ATTACHMENTTYPE && attachment.GetIsInline() != nil && *attachment.GetIsInline() { @@ -55,19 +59,26 @@ func uploadAttachment( // item Attachments to be skipped until the completion of Issue #2353 if attachmentType == models.ITEM_ATTACHMENTTYPE { - name := "" - if attachment.GetName() != nil { - name = *attachment.GetName() + prev := attachment + + attachment, err = support.ToItemAttachment(attachment) + if err != nil { + name := "" + if prev.GetName() != nil { + name = *prev.GetName() + } + + // TODO: Update to support PII protection + logger.Ctx(ctx).Infow("item attachment uploads are not supported ", + "err", err, + "attachment_name", name, + "attachment_type", attachmentType, + "internal_item_type", getItemAttachmentItemType(prev), + "attachment_id", *prev.GetId(), + ) + + return nil } - - logger.Ctx(ctx).Infow("item attachment uploads are not supported ", - "attachment_name", name, // TODO: Update to support PII protection - "attachment_type", attachmentType, - "internal_item_type", getItemAttachmentItemType(attachment), - "attachment_id", *attachment.GetId(), - ) - - return nil } // For Item/Reference attachments *or* file attachments < 3MB, use the attachments endpoint diff --git a/src/internal/connector/exchange/restore_test.go b/src/internal/connector/exchange/restore_test.go index 187d0c127..360d15266 100644 --- a/src/internal/connector/exchange/restore_test.go +++ b/src/internal/connector/exchange/restore_test.go @@ -176,11 +176,23 @@ func (suite *ExchangeRestoreSuite) TestRestoreExchangeObject() { }, }, { - name: "Test Mail: Item Attachment", + name: "Test Mail: Item Attachment_Event", bytes: mockconnector.GetMockMessageWithItemAttachmentEvent("Event Item Attachment"), category: path.EmailCategory, destination: func(t *testing.T, ctx context.Context) string { - folderName := "TestRestoreMailItemAttachment: " + common.FormatSimpleDateTime(now) + folderName := "TestRestoreEventItemAttachment: " + common.FormatSimpleDateTime(now) + folder, err := suite.ac.Mail().CreateMailFolder(ctx, userID, folderName) + require.NoError(t, err) + + return *folder.GetId() + }, + }, + { // Restore will upload the Message without uploading the attachment + name: "Test Mail: Item Attachment_NestedEvent", + bytes: mockconnector.GetMockMessageWithNestedItemAttachmentEvent("Nested Item Attachment"), + category: path.EmailCategory, + destination: func(t *testing.T, ctx context.Context) string { + folderName := "TestRestoreNestedEventItemAttachment: " + common.FormatSimpleDateTime(now) folder, err := suite.ac.Mail().CreateMailFolder(ctx, userID, folderName) require.NoError(t, err) diff --git a/src/internal/connector/exchange/service_restore.go b/src/internal/connector/exchange/service_restore.go index e1144249a..45e2ff1c4 100644 --- a/src/internal/connector/exchange/service_restore.go +++ b/src/internal/connector/exchange/service_restore.go @@ -189,23 +189,32 @@ func RestoreMailMessage( // 1st: No transmission // 2nd: Send Date // 3rd: Recv Date + svlep := make([]models.SingleValueLegacyExtendedPropertyable, 0) sv1 := models.NewSingleValueLegacyExtendedProperty() sv1.SetId(&valueID) sv1.SetValue(&enableValue) + svlep = append(svlep, sv1) - sv2 := models.NewSingleValueLegacyExtendedProperty() - sendPropertyValue := common.FormatLegacyTime(*clone.GetSentDateTime()) - sendPropertyTag := MailSendDateTimeOverrideProperty - sv2.SetId(&sendPropertyTag) - sv2.SetValue(&sendPropertyValue) + if clone.GetSentDateTime() != nil { + sv2 := models.NewSingleValueLegacyExtendedProperty() + sendPropertyValue := common.FormatLegacyTime(*clone.GetSentDateTime()) + sendPropertyTag := MailSendDateTimeOverrideProperty + sv2.SetId(&sendPropertyTag) + sv2.SetValue(&sendPropertyValue) - sv3 := models.NewSingleValueLegacyExtendedProperty() - recvPropertyValue := common.FormatLegacyTime(*clone.GetReceivedDateTime()) - recvPropertyTag := MailReceiveDateTimeOverriveProperty - sv3.SetId(&recvPropertyTag) - sv3.SetValue(&recvPropertyValue) + svlep = append(svlep, sv2) + } + + if clone.GetReceivedDateTime() != nil { + sv3 := models.NewSingleValueLegacyExtendedProperty() + recvPropertyValue := common.FormatLegacyTime(*clone.GetReceivedDateTime()) + recvPropertyTag := MailReceiveDateTimeOverriveProperty + sv3.SetId(&recvPropertyTag) + sv3.SetValue(&recvPropertyValue) + + svlep = append(svlep, sv3) + } - svlep := []models.SingleValueLegacyExtendedPropertyable{sv1, sv2, sv3} clone.SetSingleValueExtendedProperties(svlep) // Switch workflow based on collision policy @@ -248,10 +257,9 @@ func SendMailToBackStore( errs error ) - if *message.GetHasAttachments() { - attached = message.GetAttachments() - message.SetAttachments([]models.Attachmentable{}) - } + // Item.Attachments --> HasAttachments doesn't always have a value populated when deserialized + attached = message.GetAttachments() + message.SetAttachments([]models.Attachmentable{}) sentMessage, err := service.Client().UsersById(user).MailFoldersById(destination).Messages().Post(ctx, message, nil) if err != nil { diff --git a/src/internal/connector/mockconnector/mock_data_message.go b/src/internal/connector/mockconnector/mock_data_message.go index da06cbaab..4c2e84235 100644 --- a/src/internal/connector/mockconnector/mock_data_message.go +++ b/src/internal/connector/mockconnector/mock_data_message.go @@ -359,3 +359,189 @@ func GetMockMessageWithItemAttachmentEvent(subject string) []byte { return []byte(message) } + +func GetMockMessageWithNestedItemAttachmentEvent(subject string) []byte { + //nolint:lll + // Order of fields: + // 1. subject + // 2. alias + // 3. sender address + // 4. from address + // 5. toRecipients email address + template := `{ + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('f435c656-f8b2-4d71-93c3-6e092f52a167')/messages(attachments())/$entity", + "@odata.etag": "W/\"CQAAABYAAAB8wYc0thTTTYl3RpEYIUq+AADFK782\"", + "id": "AAMkAGQ1NzViZTdhLTEwMTMtNGJjNi05YWI2LTg4NWRlZDA2Y2UxOABGAAAAAAAPvVwUramXT7jlSGpVU8_7BwB8wYc0thTTTYl3RpEYIUq_AAAAAAEMAAB8wYc0thTTTYl3RpEYIUq_AADFfThSAAA=", + "createdDateTime": "2023-02-02T21:38:27Z", + "lastModifiedDateTime": "2023-02-02T22:42:49Z", + "changeKey": "CQAAABYAAAB8wYc0thTTTYl3RpEYIUq+AADFK782", + "categories": [], + "receivedDateTime": "2023-02-02T21:38:27Z", + "sentDateTime": "2023-02-02T21:38:24Z", + "hasAttachments": true, + "internetMessageId": "", + "subject": "%[1]v", + "bodyPreview": "Dustin,\r\n\r\nI'm here to see if we are still able to discover our object.", + "importance": "normal", + "parentFolderId": "AQMkAGQ1NzViZTdhLTEwMTMtNGJjNi05YWI2LTg4ADVkZWQwNmNlMTgALgAAAw_9XBStqZdPuOVIalVTz7sBAHzBhzS2FNNNiXdGkRghSr4AAAIBDAAAAA==", + "conversationId": "AAQkAGQ1NzViZTdhLTEwMTMtNGJjNi05YWI2LTg4NWRlZDA2Y2UxOAAQAB13OyMdkNJJqEaIrGi3Yjc=", + "conversationIndex": "AQHZN06dHXc7Ix2Q0kmoRoisaLdiNw==", + "isDeliveryReceiptRequested": false, + "isReadReceiptRequested": false, + "isRead": false, + "isDraft": false, + "webLink": "https://outlook.office365.com/owa/?ItemID=AAMkAGQ1NzTruncated", + "inferenceClassification": "focused", + "body": { + "contentType": "html", + "content": "\r\n


" + }, + "sender": { + "emailAddress": { + "name": "%[2]s", + "address": "%[3]s" + } + }, + "from": { + "emailAddress": { + "name": "%[2]s", + "address": "%[4]s" + } + }, + "toRecipients": [ + { + "emailAddress": { + "name": "%[2]s", + "address": "%[5]s" + } + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "flag": { + "flagStatus": "notFlagged" + }, + "attachments": [ + { + "@odata.type": "#microsoft.graph.itemAttachment", + "id": "AAMkAGQ1NzViZTdhLTEwMTMtNGJjNi05YWI2LTg4NWRlZDA2Y2UxOABGAAAAAAAPvVwUramXT7jlSGpVU8_7BwB8wYc0thTTTYl3RpEYIUq_AAAAAAEMAAB8wYc0thTTTYl3RpEYIUq_AADFfThSAAABEgAQAIyAgT1ZccRCjKKyF7VZ3dA=", + "lastModifiedDateTime": "2023-02-02T21:38:27Z", + "name": "Mail Item Attachment", + "contentType": null, + "size": 5362, + "isInline": false, + "item@odata.associationLink": "https://graph.microsoft.com/v1.0/users('f435c656-f8b2-4d71-93c3-6e092f52a167')/messages('')/$ref", + "item@odata.navigationLink": "https://graph.microsoft.com/v1.0/users('f435c656-f8b2-4d71-93c3-6e092f52a167')/messages('')", + "item": { + "@odata.type": "#microsoft.graph.message", + "id": "", + "createdDateTime": "2023-02-02T21:38:27Z", + "lastModifiedDateTime": "2023-02-02T21:38:27Z", + "receivedDateTime": "2023-02-01T13:48:47Z", + "sentDateTime": "2023-02-01T13:48:46Z", + "hasAttachments": true, + "internetMessageId": "", + "subject": "Mail Item Attachment", + "bodyPreview": "Lookingtodothis", + "importance": "normal", + "conversationId": "AAQkAGQ1NzViZTdhLTEwMTMtNGJjNi05YWI2LTg4NWRlZDA2Y2UxOAAQAMNK0NU7Kx5GhAaHdzhfSRU=", + "conversationIndex": "AQHZN02pw0rQ1TsrHkaEBod3OF9JFQ==", + "isDeliveryReceiptRequested": false, + "isReadReceiptRequested": false, + "isRead": true, + "isDraft": false, + "webLink": "https://outlook.office365.com/owa/?AttachmentItemID=AAMkAGQ1NzViZTdhLTEwMTM", + "body": { + "contentType": "html", + "content": "\r\nLookingtodothis 
" + }, + "sender": { + "emailAddress": { + "name": "A Stranger", + "address": "foobar@8qzvrj.onmicrosoft.com" + } + }, + "from": { + "emailAddress": { + "name": "A Stranger", + "address": "foobar@8qzvrj.onmicrosoft.com" + } + }, + "toRecipients": [ + { + "emailAddress": { + "name": "Direct Report", + "address": "notAvailable@8qzvrj.onmicrosoft.com" + } + } + ], + "flag": { + "flagStatus": "notFlagged" + }, + "attachments": [ + { + "@odata.type": "#microsoft.graph.itemAttachment", + "id": "AAMkAGQ1NzViZTdhLTEwMTMtNGJjNi05YWI2LTg4NWRlZDA2Y2UxOABGAAAAAAAPvVwUramXT7jlSGpVU8_7BwB8wYc0thTTTYl3RpEYIUq_AAAAAAEMAAB8wYc0thTTTYl3RpEYIUq_AADFfThSAAACEgAQAIyAgT1ZccRCjKKyF7VZ3dASABAAuYCb3N2YZ02RpJrZPzCBFQ==", + "lastModifiedDateTime": "2023-02-02T21:38:27Z", + "name": "Holidayevent", + "contentType": null, + "size": 2331, + "isInline": false, + "item@odata.associationLink": "https://graph.microsoft.com/v1.0/users('f435c656-f8b2-4d71-93c3-6e092f52a167')/events('')/$ref", + "item@odata.navigationLink": "https://graph.microsoft.com/v1.0/users('f435c656-f8b2-4d71-93c3-6e092f52a167')/events('')", + "item": { + "@odata.type": "#microsoft.graph.event", + "id": "", + "createdDateTime": "2023-02-02T21:38:27Z", + "lastModifiedDateTime": "2023-02-02T21:38:27Z", + "originalStartTimeZone": "tzone://Microsoft/Utc", + "originalEndTimeZone": "tzone://Microsoft/Utc", + "reminderMinutesBeforeStart": 0, + "isReminderOn": false, + "hasAttachments": false, + "subject": "Discuss Gifts for Children", + "isAllDay": false, + "isCancelled": false, + "isOrganizer": true, + "responseRequested": true, + "type": "singleInstance", + "isOnlineMeeting": false, + "isDraft": true, + "body": { + "contentType": "html", + "content": "\r\nLet'slookforfunding! " + }, + "start": { + "dateTime": "2016-12-02T18:00:00.0000000Z", + "timeZone": "UTC" + }, + "end": { + "dateTime": "2016-12-02T19:00:00.0000000Z", + "timeZone": "UTC" + }, + "organizer": { + "emailAddress": { + "name": "Event Manager", + "address": "philonis@8qzvrj.onmicrosoft.com" + } + } + } + } + ] + } + } + ] + }` + + message := fmt.Sprintf( + template, + subject, + defaultAlias, + defaultMessageSender, + defaultMessageFrom, + defaultMessageTo, + ) + + return []byte(message) +} diff --git a/src/internal/connector/support/m365Transform.go b/src/internal/connector/support/m365Transform.go index 651689430..7fa207c9e 100644 --- a/src/internal/connector/support/m365Transform.go +++ b/src/internal/connector/support/m365Transform.go @@ -1,11 +1,14 @@ package support import ( + "fmt" "strings" "github.com/microsoftgraph/msgraph-sdk-go/models" ) +const itemAttachment = "#microsoft.graph.itemAttachment" + // CloneMessageableFields places data from original data into new message object. // SingleLegacyValueProperty is not populated during this operation func CloneMessageableFields(orig, message models.Messageable) models.Messageable { @@ -278,3 +281,90 @@ func cloneColumnDefinitionable(orig models.ColumnDefinitionable) models.ColumnDe return newColumn } + +// ToItemAttachment transforms internal item, OutlookItemables, into +// objects that are able to be uploaded into M365. +// Supported Internal Items: +// - Events +func ToItemAttachment(orig models.Attachmentable) (models.Attachmentable, error) { + transform, ok := orig.(models.ItemAttachmentable) + supported := "#microsoft.graph.event" + + if !ok { // Shouldn't ever happen + return nil, fmt.Errorf("transforming attachment to item attachment") + } + + item := transform.GetItem() + itemType := item.GetOdataType() + + switch *itemType { + case supported: + event := item.(models.Eventable) + + newEvent, err := sanitizeEvent(event) + if err != nil { + return nil, err + } + + transform.SetItem(newEvent) + + return transform, nil + default: + return nil, fmt.Errorf("exiting ToItemAttachment: %s not supported", *itemType) + } +} + +// sanitizeEvent transfers data into event object and +// removes unique IDs from the M365 object +func sanitizeEvent(orig models.Eventable) (models.Eventable, error) { + newEvent := models.NewEvent() + newEvent.SetAttendees(orig.GetAttendees()) + newEvent.SetBody(orig.GetBody()) + newEvent.SetBodyPreview(orig.GetBodyPreview()) + newEvent.SetCalendar(orig.GetCalendar()) + newEvent.SetCreatedDateTime(orig.GetCreatedDateTime()) + newEvent.SetEnd(orig.GetEnd()) + newEvent.SetHasAttachments(orig.GetHasAttachments()) + newEvent.SetHideAttendees(orig.GetHideAttendees()) + newEvent.SetImportance(orig.GetImportance()) + newEvent.SetIsAllDay(orig.GetIsAllDay()) + newEvent.SetIsOnlineMeeting(orig.GetIsOnlineMeeting()) + newEvent.SetLocation(orig.GetLocation()) + newEvent.SetLocations(orig.GetLocations()) + newEvent.SetSensitivity(orig.GetSensitivity()) + newEvent.SetReminderMinutesBeforeStart(orig.GetReminderMinutesBeforeStart()) + newEvent.SetStart(orig.GetStart()) + newEvent.SetSubject(orig.GetSubject()) + newEvent.SetType(orig.GetType()) + + // Sanitation + // isDraft and isOrganizer *bool ptr's have to be removed completely + // from JSON in order for POST method to succeed. + // Current as of 2/2/2023 + + newEvent.SetIsOrganizer(nil) + newEvent.SetIsDraft(nil) + newEvent.SetAdditionalData(orig.GetAdditionalData()) + + attached := orig.GetAttachments() + attachments := make([]models.Attachmentable, len(attached)) + + for _, ax := range attached { + if *ax.GetOdataType() == itemAttachment { + newAttachment, err := ToItemAttachment(ax) + if err != nil { + return nil, err + } + + attachments = append(attachments, newAttachment) + + continue + } + + attachments = append(attachments, ax) + } + + newEvent.SetAttachments(attachments) + + return newEvent, nil +} From 38f56cccbab6718a8eec82202fca8bf0adb4aa3c Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Fri, 3 Feb 2023 15:29:55 -0800 Subject: [PATCH 34/40] Alternative way to handle 2-level calendars hierarchy (#2397) ## Description All calendars except the default are nested under a "Other Calendars" folder. Having a non-default calendar named the same as the default calendar does not cause problems when fetching the default calendar by name. Only the default calendar will be returned in that situation. This fixes the bug where we had multiple collections for the same path but representing different folders. Also updates the restore execution path to handle the new nested folder structure. Backup, incremental backup, and restore flows tested manually ## Does this PR need a docs update or release note? - [x] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [ ] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * #2388 ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- CHANGELOG.md | 3 +++ .../connector/exchange/container_resolver_test.go | 15 +++++++++------ .../connector/exchange/data_collections_test.go | 4 ++-- .../connector/exchange/event_calendar_cache.go | 12 ++++++++++-- src/internal/connector/exchange/exchange_vars.go | 1 + .../connector/exchange/service_restore.go | 6 +++++- 6 files changed, 30 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53ec01a36..bf2cd7f0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `--restore-permissions` flag to toggle restoration of OneDrive permissions - Add versions to backups so that we can understand/handle older backup formats +### Fixed +- Backing up a calendar that has the same name as the default calendar + ### Known Issues - When the same user has permissions to a file and the containing diff --git a/src/internal/connector/exchange/container_resolver_test.go b/src/internal/connector/exchange/container_resolver_test.go index d7c6651f1..be0704f46 100644 --- a/src/internal/connector/exchange/container_resolver_test.go +++ b/src/internal/connector/exchange/container_resolver_test.go @@ -501,10 +501,11 @@ func (suite *FolderCacheIntegrationSuite) TestCreateContainerDestination() { directoryCaches = make(map[path.CategoryType]graph.ContainerResolver) folderName = tester.DefaultTestRestoreDestination().ContainerName tests = []struct { - name string - pathFunc1 func(t *testing.T) path.Path - pathFunc2 func(t *testing.T) path.Path - category path.CategoryType + name string + pathFunc1 func(t *testing.T) path.Path + pathFunc2 func(t *testing.T) path.Path + category path.CategoryType + folderPrefix string }{ { name: "Mail Cache Test", @@ -587,6 +588,7 @@ func (suite *FolderCacheIntegrationSuite) TestCreateContainerDestination() { require.NoError(t, err) return aPath }, + folderPrefix: calendarOthersFolder, }, } ) @@ -617,8 +619,9 @@ func (suite *FolderCacheIntegrationSuite) TestCreateContainerDestination() { _, err = resolver.IDToPath(ctx, secondID) require.NoError(t, err) - _, ok := resolver.PathInCache(folderName) - require.True(t, ok) + p := stdpath.Join(test.folderPrefix, folderName) + _, ok := resolver.PathInCache(p) + require.True(t, ok, "looking for path in cache: %s", p) }) } } diff --git a/src/internal/connector/exchange/data_collections_test.go b/src/internal/connector/exchange/data_collections_test.go index 70f413239..07eef5e7a 100644 --- a/src/internal/connector/exchange/data_collections_test.go +++ b/src/internal/connector/exchange/data_collections_test.go @@ -537,9 +537,9 @@ func (suite *DataCollectionsIntegrationSuite) TestEventsSerializationRegression( }, { name: "Birthday Calendar", - expected: "Birthdays", + expected: calendarOthersFolder + "/Birthdays", scope: selectors.NewExchangeBackup(users).EventCalendars( - []string{"Birthdays"}, + []string{calendarOthersFolder + "/Birthdays"}, selectors.PrefixMatch(), )[0], }, diff --git a/src/internal/connector/exchange/event_calendar_cache.go b/src/internal/connector/exchange/event_calendar_cache.go index e497a272a..0377433ee 100644 --- a/src/internal/connector/exchange/event_calendar_cache.go +++ b/src/internal/connector/exchange/event_calendar_cache.go @@ -64,7 +64,15 @@ func (ecc *eventCalendarCache) Populate( return errors.Wrap(err, "initializing") } - err := ecc.enumer.EnumerateContainers(ctx, ecc.userID, "", ecc.addFolder) + err := ecc.enumer.EnumerateContainers( + ctx, + ecc.userID, + "", + func(cf graph.CacheFolder) error { + cf.SetPath(path.Builder{}.Append(calendarOthersFolder, *cf.GetDisplayName())) + return ecc.addFolder(cf) + }, + ) if err != nil { return errors.Wrap(err, "enumerating containers") } @@ -83,7 +91,7 @@ func (ecc *eventCalendarCache) AddToCache(ctx context.Context, f graph.Container return errors.Wrap(err, "validating container") } - temp := graph.NewCacheFolder(f, path.Builder{}.Append(*f.GetDisplayName())) + temp := graph.NewCacheFolder(f, path.Builder{}.Append(calendarOthersFolder, *f.GetDisplayName())) if err := ecc.addFolder(temp); err != nil { return errors.Wrap(err, "adding container") diff --git a/src/internal/connector/exchange/exchange_vars.go b/src/internal/connector/exchange/exchange_vars.go index e45de0bf0..988d20330 100644 --- a/src/internal/connector/exchange/exchange_vars.go +++ b/src/internal/connector/exchange/exchange_vars.go @@ -38,4 +38,5 @@ const ( rootFolderAlias = "msgfolderroot" DefaultContactFolder = "Contacts" DefaultCalendar = "Calendar" + calendarOthersFolder = "Other Calendars" ) diff --git a/src/internal/connector/exchange/service_restore.go b/src/internal/connector/exchange/service_restore.go index 45e2ff1c4..e6fa592f7 100644 --- a/src/internal/connector/exchange/service_restore.go +++ b/src/internal/connector/exchange/service_restore.go @@ -645,7 +645,11 @@ func establishEventsRestoreLocation( user string, isNewCache bool, ) (string, error) { - cached, ok := ecc.PathInCache(folders[0]) + // Need to prefix with the "Other Calendars" folder so lookup happens properly. + cached, ok := ecc.PathInCache(path.Builder{}.Append( + calendarOthersFolder, + folders[0], + ).String()) if ok { return cached, nil } From 8d04957e5f184d3e45624917750b2f7c0c40722e Mon Sep 17 00:00:00 2001 From: Keepers Date: Fri, 3 Feb 2023 17:11:59 -0700 Subject: [PATCH 35/40] clean up operation run-do funcs (#2391) ## Description The operation run-do pattern is currently spaghetti, The do() call includes a deferred persistence that occurs before the Run call concludes, causing us to need panic handlers in multiple levels. This change normalizes the interacton: do() now only contains the behavior necessary to process the backup or restore. Run() contains all setup and teardown processes surrounding that. General pattern looks like this: Run() 0. defer panic recovery 1. create state builders/recorder vars/clients 2. call do() 3. persist results of do(), even in case of error. do() process step-by-step backup or restore operation update builders/recorders along the way exit immediately on any error ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :broom: Tech Debt/Cleanup ## Test Plan - [x] :zap: Unit test - [x] :green_heart: E2E --- src/internal/operations/backup.go | 214 +++++++++++++++-------------- src/internal/operations/restore.go | 135 +++++++++++------- 2 files changed, 192 insertions(+), 157 deletions(-) diff --git a/src/internal/operations/backup.go b/src/internal/operations/backup.go index e6b8c767a..ec47bae1c 100644 --- a/src/internal/operations/backup.go +++ b/src/internal/operations/backup.go @@ -2,6 +2,7 @@ package operations import ( "context" + "fmt" "runtime/debug" "time" @@ -110,7 +111,21 @@ type detailsWriter interface { func (op *BackupOperation) Run(ctx context.Context) (err error) { defer func() { if r := recover(); r != nil { - err = clues.Wrap(r.(error), "panic recovery").WithClues(ctx).With("stacktrace", debug.Stack()) + var rerr error + if re, ok := r.(error); ok { + rerr = re + } else if re, ok := r.(string); ok { + rerr = clues.New(re) + } else { + rerr = clues.New(fmt.Sprintf("%v", r)) + } + + err = clues.Wrap(rerr, "panic recovery"). + WithClues(ctx). + With("stacktrace", string(debug.Stack())) + logger.Ctx(ctx). + With("err", err). + Errorw("backup panic", clues.InErr(err).Slice()...) } }() @@ -121,6 +136,18 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) { observe.Complete() }() + // ----- + // Setup + // ----- + + var ( + opStats backupStats + startTime = time.Now() + detailsStore = streamstore.New(op.kopia, op.account.ID(), op.Selectors.PathService()) + ) + + op.Results.BackupID = model.StableID(uuid.NewString()) + ctx = clues.AddAll( ctx, "tenant_id", op.account.ID(), // TODO: pii @@ -129,32 +156,6 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) { "service", op.Selectors.Service, "incremental", op.incremental) - if err := op.do(ctx); err != nil { - logger.Ctx(ctx). - With("err", err). - Errorw("backup operation", clues.InErr(err).Slice()...) - - return err - } - - logger.Ctx(ctx).Infow("completed backup", "results", op.Results) - - return nil -} - -func (op *BackupOperation) do(ctx context.Context) (err error) { - var ( - opStats backupStats - backupDetails *details.Builder - toMerge map[string]path.Path - tenantID = op.account.ID() - startTime = time.Now() - detailsStore = streamstore.New(op.kopia, tenantID, op.Selectors.PathService()) - reasons = selectorToReasons(op.Selectors) - ) - - op.Results.BackupID = model.StableID(uuid.NewString()) - op.bus.Event( ctx, events.BackupStart, @@ -162,122 +163,128 @@ func (op *BackupOperation) do(ctx context.Context) (err error) { events.StartTime: startTime, events.Service: op.Selectors.Service.String(), events.BackupID: op.Results.BackupID, - }, - ) + }) - // persist operation results to the model store on exit - defer func() { - // panic recovery here prevents additional errors in op.persistResults() - if r := recover(); r != nil { - err = clues.Wrap(r.(error), "panic recovery").WithClues(ctx).With("stacktrace", debug.Stack()) - return - } + // ----- + // Execution + // ----- - err = op.persistResults(startTime, &opStats) - if err != nil { - op.Errors.Fail(errors.Wrap(err, "persisting backup results")) - return - } + deets, err := op.do( + ctx, + &opStats, + detailsStore, + op.Results.BackupID) + if err != nil { + // No return here! We continue down to persistResults, even in case of failure. + logger.Ctx(ctx). + With("err", err). + Errorw("doing backup", clues.InErr(err).Slice()...) + op.Errors.Fail(errors.Wrap(err, "doing backup")) + opStats.readErr = op.Errors.Err() + } - err = op.createBackupModels( - ctx, - detailsStore, - opStats.k.SnapshotID, - backupDetails.Details()) - if err != nil { - op.Errors.Fail(errors.Wrap(err, "persisting backup")) - opStats.writeErr = op.Errors.Err() - } - }() + // ----- + // Persistence + // ----- + + err = op.persistResults(startTime, &opStats) + if err != nil { + op.Errors.Fail(errors.Wrap(err, "persisting backup results")) + opStats.writeErr = op.Errors.Err() + + return op.Errors.Err() + } + + err = op.createBackupModels( + ctx, + detailsStore, + opStats.k.SnapshotID, + op.Results.BackupID, + deets.Details()) + if err != nil { + op.Errors.Fail(errors.Wrap(err, "persisting backup")) + opStats.writeErr = op.Errors.Err() + + return op.Errors.Err() + } + + logger.Ctx(ctx).Infow("completed backup", "results", op.Results) + + return nil +} + +// do is purely the action of running a backup. All pre/post behavior +// is found in Run(). +func (op *BackupOperation) do( + ctx context.Context, + opStats *backupStats, + detailsStore detailsReader, + backupID model.StableID, +) (*details.Builder, error) { + reasons := selectorToReasons(op.Selectors) + + // should always be 1, since backups are 1:1 with resourceOwners. + opStats.resourceCount = 1 mans, mdColls, canUseMetaData, err := produceManifestsAndMetadata( ctx, op.kopia, op.store, reasons, - tenantID, + op.account.ID(), op.incremental, op.Errors) if err != nil { - op.Errors.Fail(errors.Wrap(err, "collecting manifest heuristics")) - opStats.readErr = op.Errors.Err() - - logger.Ctx(ctx).With("err", err).Errorw("producing manifests and metadata", clues.InErr(err).Slice()...) - - return opStats.readErr + return nil, errors.Wrap(err, "producing manifests and metadata") } gc, err := connectToM365(ctx, op.Selectors, op.account) if err != nil { - op.Errors.Fail(errors.Wrap(err, "connecting to m365")) - opStats.readErr = op.Errors.Err() - - logger.Ctx(ctx).With("err", err).Errorw("connectng to m365", clues.InErr(err).Slice()...) - - return opStats.readErr + return nil, errors.Wrap(err, "connectng to m365") } cs, err := produceBackupDataCollections(ctx, gc, op.Selectors, mdColls, op.Options) if err != nil { - op.Errors.Fail(errors.Wrap(err, "retrieving data to backup")) - opStats.readErr = op.Errors.Err() - - logger.Ctx(ctx).With("err", err).Errorw("producing backup data collections", clues.InErr(err).Slice()...) - - return opStats.readErr + return nil, errors.Wrap(err, "producing backup data collections") } ctx = clues.Add(ctx, "coll_count", len(cs)) - opStats.k, backupDetails, toMerge, err = consumeBackupDataCollections( + writeStats, deets, toMerge, err := consumeBackupDataCollections( ctx, op.kopia, - tenantID, + op.account.ID(), reasons, mans, cs, - op.Results.BackupID, + backupID, op.incremental && canUseMetaData) if err != nil { - op.Errors.Fail(errors.Wrap(err, "backing up service data")) - opStats.writeErr = op.Errors.Err() - - logger.Ctx(ctx).With("err", err).Errorw("persisting collection backups", clues.InErr(err).Slice()...) - - return opStats.writeErr + return nil, errors.Wrap(err, "persisting collection backups") } - if err = mergeDetails( + opStats.k = writeStats + + err = mergeDetails( ctx, op.store, detailsStore, mans, toMerge, - backupDetails, - ); err != nil { - op.Errors.Fail(errors.Wrap(err, "merging backup details")) - opStats.writeErr = op.Errors.Err() - - logger.Ctx(ctx).With("err", err).Errorw("merging details", clues.InErr(err).Slice()...) - - return opStats.writeErr + deets) + if err != nil { + return nil, errors.Wrap(err, "merging details") } opStats.gc = gc.AwaitStatus() - // TODO(keepers): remove when fault.Errors handles all iterable error aggregation. if opStats.gc.ErrorCount > 0 { - merr := multierror.Append(opStats.readErr, errors.Wrap(opStats.gc.Err, "retrieving data")) - opStats.readErr = merr.ErrorOrNil() - - // Need to exit before we set started to true else we'll report no errors. - return opStats.readErr + return nil, opStats.gc.Err } - // should always be 1, since backups are 1:1 with resourceOwners. - opStats.resourceCount = 1 + logger.Ctx(ctx).Debug(gc.PrintableStatus()) - return err + return deets, nil } // checker to see if conditions are correct for incremental backup behavior such as @@ -520,8 +527,7 @@ func mergeDetails( ctx, model.StableID(bID), ms, - detailsStore, - ) + detailsStore) if err != nil { return clues.New("fetching base details for backup").WithClues(mctx) } @@ -566,8 +572,7 @@ func mergeDetails( newPath.ShortRef(), newPath.ToBuilder().Dir().ShortRef(), itemUpdated, - item, - ) + item) folders := details.FolderEntriesForPath(newPath.ToBuilder().Dir()) deets.AddFoldersForItem(folders, item, itemUpdated) @@ -617,10 +622,10 @@ func (op *BackupOperation) persistResults( if opStats.gc == nil { op.Status = Failed - return errors.New("data population never completed") + return errors.New("backup population never completed") } - if opStats.readErr == nil && opStats.writeErr == nil && opStats.gc.Successful == 0 { + if opStats.gc.Successful == 0 { op.Status = NoData } @@ -634,6 +639,7 @@ func (op *BackupOperation) createBackupModels( ctx context.Context, detailsStore detailsWriter, snapID string, + backupID model.StableID, backupDetails *details.Details, ) error { ctx = clues.Add(ctx, "snapshot_id", snapID) @@ -650,7 +656,7 @@ func (op *BackupOperation) createBackupModels( ctx = clues.Add(ctx, "details_id", detailsID) b := backup.New( snapID, detailsID, op.Status.String(), - op.Results.BackupID, + backupID, op.Selectors, op.Results.ReadWrites, op.Results.StartAndEndTime, diff --git a/src/internal/operations/restore.go b/src/internal/operations/restore.go index f90e3c3c8..dafb8670e 100644 --- a/src/internal/operations/restore.go +++ b/src/internal/operations/restore.go @@ -110,10 +110,37 @@ type restorer interface { func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.Details, err error) { defer func() { if r := recover(); r != nil { - err = clues.Wrap(r.(error), "panic recovery").WithClues(ctx).With("stacktrace", debug.Stack()) + var rerr error + if re, ok := r.(error); ok { + rerr = re + } else if re, ok := r.(string); ok { + rerr = clues.New(re) + } else { + rerr = clues.New(fmt.Sprintf("%v", r)) + } + + err = clues.Wrap(rerr, "panic recovery"). + WithClues(ctx). + With("stacktrace", string(debug.Stack())) + logger.Ctx(ctx). + With("err", err). + Errorw("backup panic", clues.InErr(err).Slice()...) } }() + var ( + opStats = restoreStats{ + bytesRead: &stats.ByteCounter{}, + restoreID: uuid.NewString(), + } + start = time.Now() + detailsStore = streamstore.New(op.kopia, op.account.ID(), op.Selectors.PathService()) + ) + + // ----- + // Setup + // ----- + ctx, end := D.Span(ctx, "operations:restore:run") defer func() { end() @@ -127,13 +154,30 @@ func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.De "backup_id", op.BackupID, "service", op.Selectors.Service) - deets, err := op.do(ctx) + // ----- + // Execution + // ----- + + deets, err := op.do(ctx, &opStats, detailsStore, start) if err != nil { + // No return here! We continue down to persistResults, even in case of failure. logger.Ctx(ctx). With("err", err). - Errorw("restore operation", clues.InErr(err).Slice()...) + Errorw("doing restore", clues.InErr(err).Slice()...) + op.Errors.Fail(errors.Wrap(err, "doing restore")) + opStats.readErr = op.Errors.Err() + } - return nil, err + // ----- + // Persistence + // ----- + + err = op.persistResults(ctx, start, &opStats) + if err != nil { + op.Errors.Fail(errors.Wrap(err, "persisting restore results")) + opStats.writeErr = op.Errors.Err() + + return nil, op.Errors.Err() } logger.Ctx(ctx).Infow("completed restore", "results", op.Results) @@ -141,30 +185,12 @@ func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.De return deets, nil } -func (op *RestoreOperation) do(ctx context.Context) (restoreDetails *details.Details, err error) { - var ( - opStats = restoreStats{ - bytesRead: &stats.ByteCounter{}, - restoreID: uuid.NewString(), - } - startTime = time.Now() - ) - - defer func() { - // panic recovery here prevents additional errors in op.persistResults() - if r := recover(); r != nil { - err = clues.Wrap(r.(error), "panic recovery").WithClues(ctx).With("stacktrace", debug.Stack()) - return - } - - err = op.persistResults(ctx, startTime, &opStats) - if err != nil { - return - } - }() - - detailsStore := streamstore.New(op.kopia, op.account.ID(), op.Selectors.PathService()) - +func (op *RestoreOperation) do( + ctx context.Context, + opStats *restoreStats, + detailsStore detailsReader, + start time.Time, +) (*details.Details, error) { bup, deets, err := getBackupAndDetailsFromID( ctx, op.BackupID, @@ -172,30 +198,29 @@ func (op *RestoreOperation) do(ctx context.Context) (restoreDetails *details.Det detailsStore, ) if err != nil { - opStats.readErr = errors.Wrap(err, "restore") - return nil, opStats.readErr + return nil, errors.Wrap(err, "getting backup and details") } - ctx = clues.Add(ctx, "resource_owner", bup.Selector.DiscreteOwner) + paths, err := formatDetailsForRestoration(ctx, op.Selectors, deets) + if err != nil { + return nil, errors.Wrap(err, "formatting paths from details") + } + + ctx = clues.AddAll( + ctx, + "resource_owner", bup.Selector.DiscreteOwner, + "details_paths", len(paths)) op.bus.Event( ctx, events.RestoreStart, map[string]any{ - events.StartTime: startTime, + events.StartTime: start, events.BackupID: op.BackupID, events.BackupCreateTime: bup.CreationTime, events.RestoreID: opStats.restoreID, - }, - ) + }) - paths, err := formatDetailsForRestoration(ctx, op.Selectors, deets) - if err != nil { - opStats.readErr = err - return nil, err - } - - ctx = clues.Add(ctx, "details_paths", len(paths)) observe.Message(ctx, observe.Safe(fmt.Sprintf("Discovered %d items in backup %s to restore", len(paths), op.BackupID))) kopiaComplete, closer := observe.MessageWithCompletion(ctx, observe.Safe("Enumerating items in repository")) @@ -204,41 +229,45 @@ func (op *RestoreOperation) do(ctx context.Context) (restoreDetails *details.Det dcs, err := op.kopia.RestoreMultipleItems(ctx, bup.SnapshotID, paths, opStats.bytesRead) if err != nil { - opStats.readErr = errors.Wrap(err, "retrieving service data") - return nil, opStats.readErr + return nil, errors.Wrap(err, "retrieving collections from repository") } + kopiaComplete <- struct{}{} ctx = clues.Add(ctx, "coll_count", len(dcs)) + + // should always be 1, since backups are 1:1 with resourceOwners. + opStats.resourceCount = 1 opStats.cs = dcs - opStats.resourceCount = len(data.ResourceOwnerSet(dcs)) gc, err := connectToM365(ctx, op.Selectors, op.account) if err != nil { - opStats.readErr = errors.Wrap(err, "connecting to M365") - return nil, opStats.readErr + return nil, errors.Wrap(err, "connecting to M365") } restoreComplete, closer := observe.MessageWithCompletion(ctx, observe.Safe("Restoring data")) defer closer() defer close(restoreComplete) - restoreDetails, err = gc.RestoreDataCollections( + restoreDetails, err := gc.RestoreDataCollections( ctx, bup.Version, op.account, op.Selectors, op.Destination, op.Options, - dcs, - ) + dcs) if err != nil { - opStats.writeErr = errors.Wrap(err, "restoring service data") - return nil, opStats.writeErr + return nil, errors.Wrap(err, "restoring collections") } + restoreComplete <- struct{}{} opStats.gc = gc.AwaitStatus() + // TODO(keepers): remove when fault.Errors handles all iterable error aggregation. + if opStats.gc.ErrorCount > 0 { + return nil, opStats.gc.Err + } logger.Ctx(ctx).Debug(gc.PrintableStatus()) @@ -273,10 +302,10 @@ func (op *RestoreOperation) persistResults( if opStats.gc == nil { op.Status = Failed - return errors.New("data restoration never completed") + return errors.New("restoration never completed") } - if opStats.readErr == nil && opStats.writeErr == nil && opStats.gc.Successful == 0 { + if opStats.gc.Successful == 0 { op.Status = NoData } From 5f3eaa01780aa2e09c4ab0877df2d6dc1ac90694 Mon Sep 17 00:00:00 2001 From: Abin Simon Date: Sat, 4 Feb 2023 06:13:23 +0530 Subject: [PATCH 36/40] Adds more retries to OneDrive API calls (#2387) ## Description Adds more reties to handle timeout issues. ## Does this PR need a docs update or release note? - [x] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [ ] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * # ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- CHANGELOG.md | 1 + src/internal/connector/discovery/api/users.go | 31 +++++++++++--- src/internal/connector/exchange/api/api.go | 24 ----------- .../connector/exchange/api/contacts.go | 10 ++--- src/internal/connector/exchange/api/events.go | 10 ++--- src/internal/connector/exchange/api/mail.go | 8 ++-- src/internal/connector/graph/service.go | 25 +++++++++++ src/internal/connector/onedrive/api/drive.go | 36 ++++++++++++++-- src/internal/connector/onedrive/drive.go | 13 +++++- src/internal/connector/onedrive/drive_test.go | 42 ++++++++++--------- 10 files changed, 132 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf2cd7f0f..0303c9206 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Backing up a calendar that has the same name as the default calendar +- Added additional backoff-retry to all OneDrive queries. ### Known Issues diff --git a/src/internal/connector/discovery/api/users.go b/src/internal/connector/discovery/api/users.go index ff41ee06f..e4a8e0f00 100644 --- a/src/internal/connector/discovery/api/users.go +++ b/src/internal/connector/discovery/api/users.go @@ -77,7 +77,13 @@ func (c Users) GetAll(ctx context.Context) ([]models.Userable, error) { return nil, err } - resp, err := service.Client().Users().Get(ctx, userOptions(&userFilterNoGuests)) + var resp models.UserCollectionResponseable + + err = graph.RunWithRetry(func() error { + resp, err = service.Client().Users().Get(ctx, userOptions(&userFilterNoGuests)) + return err + }) + if err != nil { return nil, support.ConnectorStackErrorTraceWrap(err, "getting all users") } @@ -114,22 +120,37 @@ func (c Users) GetAll(ctx context.Context) ([]models.Userable, error) { } func (c Users) GetByID(ctx context.Context, userID string) (models.Userable, error) { - user, err := c.stable.Client().UsersById(userID).Get(ctx, nil) + var ( + resp models.Userable + err error + ) + + err = graph.RunWithRetry(func() error { + resp, err = c.stable.Client().UsersById(userID).Get(ctx, nil) + return err + }) + if err != nil { return nil, support.ConnectorStackErrorTraceWrap(err, "getting user by id") } - return user, nil + return resp, err } func (c Users) GetInfo(ctx context.Context, userID string) (*UserInfo, error) { // Assume all services are enabled // then filter down to only services the user has enabled - userInfo := newUserInfo() + var ( + err error + userInfo = newUserInfo() + ) // TODO: OneDrive + err = graph.RunWithRetry(func() error { + _, err = c.stable.Client().UsersById(userID).MailFolders().Get(ctx, nil) + return err + }) - _, err := c.stable.Client().UsersById(userID).MailFolders().Get(ctx, nil) if err != nil { if !graph.IsErrExchangeMailFolderNotFound(err) { return nil, support.ConnectorStackErrorTraceWrap(err, "getting user's exchange mailfolders") diff --git a/src/internal/connector/exchange/api/api.go b/src/internal/connector/exchange/api/api.go index c47cc9b5b..c4858c5c1 100644 --- a/src/internal/connector/exchange/api/api.go +++ b/src/internal/connector/exchange/api/api.go @@ -10,7 +10,6 @@ import ( "github.com/pkg/errors" "github.com/alcionai/corso/src/internal/connector/graph" - "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/pkg/account" ) @@ -154,26 +153,3 @@ func HasAttachments(body models.ItemBodyable) bool { return strings.Contains(content, "src=\"cid:") } - -// Run a function with retries -func runWithRetry(run func() error) error { - var err error - - for i := 0; i < numberOfRetries; i++ { - err = run() - if err == nil { - return nil - } - - // only retry on timeouts and 500-internal-errors. - if !(graph.IsErrTimeout(err) || graph.IsInternalServerError(err)) { - break - } - - if i < numberOfRetries { - time.Sleep(time.Duration(3*(i+2)) * time.Second) - } - } - - return support.ConnectorStackErrorTraceWrap(err, "maximum retries or unretryable") -} diff --git a/src/internal/connector/exchange/api/contacts.go b/src/internal/connector/exchange/api/contacts.go index 458f364d1..ac4afeb34 100644 --- a/src/internal/connector/exchange/api/contacts.go +++ b/src/internal/connector/exchange/api/contacts.go @@ -68,7 +68,7 @@ func (c Contacts) GetItem( err error ) - err = runWithRetry(func() error { + err = graph.RunWithRetry(func() error { cont, err = c.stable.Client().UsersById(user).ContactsById(itemID).Get(ctx, nil) return err }) @@ -94,7 +94,7 @@ func (c Contacts) GetAllContactFolderNamesForUser( var resp models.ContactFolderCollectionResponseable - err = runWithRetry(func() error { + err = graph.RunWithRetry(func() error { resp, err = c.stable.Client().UsersById(user).ContactFolders().Get(ctx, options) return err }) @@ -113,7 +113,7 @@ func (c Contacts) GetContainerByID( var resp models.ContactFolderable - err = runWithRetry(func() error { + err = graph.RunWithRetry(func() error { resp, err = c.stable.Client().UsersById(userID).ContactFoldersById(dirID).Get(ctx, ofcf) return err }) @@ -154,7 +154,7 @@ func (c Contacts) EnumerateContainers( ChildFolders() for { - err = runWithRetry(func() error { + err = graph.RunWithRetry(func() error { resp, err = builder.Get(ctx, ofcf) return err }) @@ -206,7 +206,7 @@ func (p *contactPager) getPage(ctx context.Context) (api.DeltaPageLinker, error) err error ) - err = runWithRetry(func() error { + err = graph.RunWithRetry(func() error { resp, err = p.builder.Get(ctx, p.options) return err }) diff --git a/src/internal/connector/exchange/api/events.go b/src/internal/connector/exchange/api/events.go index 70a1a45e9..b9c16f319 100644 --- a/src/internal/connector/exchange/api/events.go +++ b/src/internal/connector/exchange/api/events.go @@ -77,7 +77,7 @@ func (c Events) GetContainerByID( var cal models.Calendarable - err = runWithRetry(func() error { + err = graph.RunWithRetry(func() error { cal, err = service.Client().UsersById(userID).CalendarsById(containerID).Get(ctx, ofc) return err }) @@ -99,7 +99,7 @@ func (c Events) GetItem( err error ) - err = runWithRetry(func() error { + err = graph.RunWithRetry(func() error { event, err = c.stable.Client().UsersById(user).EventsById(itemID).Get(ctx, nil) return err }) @@ -154,7 +154,7 @@ func (c Client) GetAllCalendarNamesForUser( var resp models.CalendarCollectionResponseable - err = runWithRetry(func() error { + err = graph.RunWithRetry(func() error { resp, err = c.stable.Client().UsersById(user).Calendars().Get(ctx, options) return err }) @@ -193,7 +193,7 @@ func (c Events) EnumerateContainers( for { var err error - err = runWithRetry(func() error { + err = graph.RunWithRetry(func() error { resp, err = builder.Get(ctx, ofc) return err }) @@ -250,7 +250,7 @@ func (p *eventPager) getPage(ctx context.Context) (api.DeltaPageLinker, error) { err error ) - err = runWithRetry(func() error { + err = graph.RunWithRetry(func() error { resp, err = p.builder.Get(ctx, p.options) return err }) diff --git a/src/internal/connector/exchange/api/mail.go b/src/internal/connector/exchange/api/mail.go index 6acc05162..01a485fbb 100644 --- a/src/internal/connector/exchange/api/mail.go +++ b/src/internal/connector/exchange/api/mail.go @@ -99,7 +99,7 @@ func (c Mail) GetContainerByID( var resp graph.Container - err = runWithRetry(func() error { + err = graph.RunWithRetry(func() error { resp, err = service.Client().UsersById(userID).MailFoldersById(dirID).Get(ctx, ofmf) return err }) @@ -118,7 +118,7 @@ func (c Mail) GetItem( err error ) - err = runWithRetry(func() error { + err = graph.RunWithRetry(func() error { mail, err = c.stable.Client().UsersById(user).MessagesById(itemID).Get(ctx, nil) return err }) @@ -188,7 +188,7 @@ func (c Mail) EnumerateContainers( for { var err error - err = runWithRetry(func() error { + err = graph.RunWithRetry(func() error { resp, err = builder.Get(ctx, nil) return err }) @@ -235,7 +235,7 @@ func (p *mailPager) getPage(ctx context.Context) (api.DeltaPageLinker, error) { err error ) - err = runWithRetry(func() error { + err = graph.RunWithRetry(func() error { page, err = p.builder.Get(ctx, p.options) return err }) diff --git a/src/internal/connector/graph/service.go b/src/internal/connector/graph/service.go index 093995a42..fd6142028 100644 --- a/src/internal/connector/graph/service.go +++ b/src/internal/connector/graph/service.go @@ -8,6 +8,7 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/alcionai/corso/src/internal/connector/support" "github.com/microsoft/kiota-abstractions-go/serialization" ka "github.com/microsoft/kiota-authentication-azure-go" khttp "github.com/microsoft/kiota-http-go" @@ -22,6 +23,7 @@ import ( const ( logGraphRequestsEnvKey = "LOG_GRAPH_REQUESTS" + numberOfRetries = 3 ) // AllMetadataFileNames produces the standard set of filenames used to store graph @@ -294,3 +296,26 @@ func (handler *LoggingMiddleware) Intercept( return resp, err } + +// Run a function with retries +func RunWithRetry(run func() error) error { + var err error + + for i := 0; i < numberOfRetries; i++ { + err = run() + if err == nil { + return nil + } + + // only retry on timeouts and 500-internal-errors. + if !(IsErrTimeout(err) || IsInternalServerError(err)) { + break + } + + if i < numberOfRetries { + time.Sleep(time.Duration(3*(i+2)) * time.Second) + } + } + + return support.ConnectorStackErrorTraceWrap(err, "maximum retries or unretryable") +} diff --git a/src/internal/connector/onedrive/api/drive.go b/src/internal/connector/onedrive/api/drive.go index fea6e53a7..ce246da85 100644 --- a/src/internal/connector/onedrive/api/drive.go +++ b/src/internal/connector/onedrive/api/drive.go @@ -61,7 +61,17 @@ func NewItemPager( } func (p *driveItemPager) GetPage(ctx context.Context) (api.DeltaPageLinker, error) { - return p.builder.Get(ctx, p.options) + var ( + resp api.DeltaPageLinker + err error + ) + + err = graph.RunWithRetry(func() error { + resp, err = p.builder.Get(ctx, p.options) + return err + }) + + return resp, err } func (p *driveItemPager) SetNext(link string) { @@ -99,7 +109,17 @@ func NewUserDrivePager( } func (p *userDrivePager) GetPage(ctx context.Context) (api.PageLinker, error) { - return p.builder.Get(ctx, p.options) + var ( + resp api.PageLinker + err error + ) + + err = graph.RunWithRetry(func() error { + resp, err = p.builder.Get(ctx, p.options) + return err + }) + + return resp, err } func (p *userDrivePager) SetNext(link string) { @@ -137,7 +157,17 @@ func NewSiteDrivePager( } func (p *siteDrivePager) GetPage(ctx context.Context) (api.PageLinker, error) { - return p.builder.Get(ctx, p.options) + var ( + resp api.PageLinker + err error + ) + + err = graph.RunWithRetry(func() error { + resp, err = p.builder.Get(ctx, p.options) + return err + }) + + return resp, err } func (p *siteDrivePager) SetNext(link string) { diff --git a/src/internal/connector/onedrive/drive.go b/src/internal/connector/onedrive/drive.go index 8e1578caf..ebcbe8b6f 100644 --- a/src/internal/connector/onedrive/drive.go +++ b/src/internal/connector/onedrive/drive.go @@ -80,7 +80,7 @@ func drives( page, err = pager.GetPage(ctx) if err != nil { // Various error handling. May return an error or perform a retry. - detailedError := support.ConnectorStackErrorTrace(err) + detailedError := err.Error() if strings.Contains(detailedError, userMysiteURLNotFound) || strings.Contains(detailedError, userMysiteNotFound) { logger.Ctx(ctx).Infof("resource owner does not have a drive") @@ -236,7 +236,16 @@ func getFolder( rawURL := fmt.Sprintf(itemByPathRawURLFmt, driveID, parentFolderID, folderName) builder := msdrive.NewItemsDriveItemItemRequestBuilder(rawURL, service.Adapter()) - foundItem, err := builder.Get(ctx, nil) + var ( + foundItem models.DriveItemable + err error + ) + + err = graph.RunWithRetry(func() error { + foundItem, err = builder.Get(ctx, nil) + return err + }) + if err != nil { var oDataError *odataerrors.ODataError if errors.As(err, &oDataError) && diff --git a/src/internal/connector/onedrive/drive_test.go b/src/internal/connector/onedrive/drive_test.go index a67c89ab1..5eeda6aac 100644 --- a/src/internal/connector/onedrive/drive_test.go +++ b/src/internal/connector/onedrive/drive_test.go @@ -15,6 +15,7 @@ import ( "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph/api" + "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/logger" @@ -76,6 +77,15 @@ func TestOneDriveUnitSuite(t *testing.T) { suite.Run(t, new(OneDriveUnitSuite)) } +func odErr(code string) *odataerrors.ODataError { + odErr := &odataerrors.ODataError{} + merr := odataerrors.MainError{} + merr.SetCode(&code) + odErr.SetError(&merr) + + return odErr +} + func (suite *OneDriveUnitSuite) TestDrives() { numDriveResults := 4 emptyLink := "" @@ -84,26 +94,18 @@ func (suite *OneDriveUnitSuite) TestDrives() { // These errors won't be the "correct" format when compared to what graph // returns, but they're close enough to have the same info when the inner // details are extracted via support package. - tmp := userMysiteURLNotFound - tmpMySiteURLNotFound := odataerrors.NewMainError() - tmpMySiteURLNotFound.SetMessage(&tmp) - - mySiteURLNotFound := odataerrors.NewODataError() - mySiteURLNotFound.SetError(tmpMySiteURLNotFound) - - tmp2 := userMysiteNotFound - tmpMySiteNotFound := odataerrors.NewMainError() - tmpMySiteNotFound.SetMessage(&tmp2) - - mySiteNotFound := odataerrors.NewODataError() - mySiteNotFound.SetError(tmpMySiteNotFound) - - tmp3 := contextDeadlineExceeded - tmpDeadlineExceeded := odataerrors.NewMainError() - tmpDeadlineExceeded.SetMessage(&tmp3) - - deadlineExceeded := odataerrors.NewODataError() - deadlineExceeded.SetError(tmpDeadlineExceeded) + mySiteURLNotFound := support.ConnectorStackErrorTraceWrap( + odErr(userMysiteURLNotFound), + "maximum retries or unretryable", + ) + mySiteNotFound := support.ConnectorStackErrorTraceWrap( + odErr(userMysiteNotFound), + "maximum retries or unretryable", + ) + deadlineExceeded := support.ConnectorStackErrorTraceWrap( + odErr(contextDeadlineExceeded), + "maximum retries or unretryable", + ) resultDrives := make([]models.Driveable, 0, numDriveResults) From de3c7e5e832bfbc81992752b61854fcf975215ce Mon Sep 17 00:00:00 2001 From: Niraj Tolia Date: Fri, 3 Feb 2023 17:21:43 -0800 Subject: [PATCH 37/40] Upgrade Docusaurus to v2.3.1 (#2393) ## Description This picks up a GTM fix and dependency security upgrades ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :bug: Bugfix - [x] :world_map: Documentation --- website/package-lock.json | 641 +++++++++++++++++++------------------- website/package.json | 8 +- 2 files changed, 324 insertions(+), 325 deletions(-) diff --git a/website/package-lock.json b/website/package-lock.json index 632819d60..4ef68bb92 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -8,9 +8,9 @@ "name": "docs", "version": "0.1.0", "dependencies": { - "@docusaurus/core": "2.3.0", - "@docusaurus/plugin-google-gtag": "^2.3.0", - "@docusaurus/preset-classic": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/plugin-google-gtag": "^2.3.1", + "@docusaurus/preset-classic": "2.3.1", "@loadable/component": "^5.15.3", "@mdx-js/react": "^1.6.22", "animate.css": "^4.1.1", @@ -29,7 +29,7 @@ "wow.js": "^1.2.2" }, "devDependencies": { - "@docusaurus/module-type-aliases": "2.3.0", + "@docusaurus/module-type-aliases": "2.3.1", "@iconify/react": "^4.1.0", "autoprefixer": "^10.4.13", "postcss": "^8.4.21", @@ -1988,9 +1988,9 @@ } }, "node_modules/@docusaurus/core": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-2.3.0.tgz", - "integrity": "sha512-2AU5HfKyExO+/mi41SBnx5uY0aGZFXr3D93wntBY4lN1gsDKUpi7EE4lPBAXm9CoH4Pw6N24yDHy9CPR3sh/uA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-2.3.1.tgz", + "integrity": "sha512-0Jd4jtizqnRAr7svWaBbbrCCN8mzBNd2xFLoT/IM7bGfFie5y58oz97KzXliwiLY3zWjqMXjQcuP1a5VgCv2JA==", "dependencies": { "@babel/core": "^7.18.6", "@babel/generator": "^7.18.7", @@ -2002,13 +2002,13 @@ "@babel/runtime": "^7.18.6", "@babel/runtime-corejs3": "^7.18.6", "@babel/traverse": "^7.18.8", - "@docusaurus/cssnano-preset": "2.3.0", - "@docusaurus/logger": "2.3.0", - "@docusaurus/mdx-loader": "2.3.0", + "@docusaurus/cssnano-preset": "2.3.1", + "@docusaurus/logger": "2.3.1", + "@docusaurus/mdx-loader": "2.3.1", "@docusaurus/react-loadable": "5.5.2", - "@docusaurus/utils": "2.3.0", - "@docusaurus/utils-common": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/utils": "2.3.1", + "@docusaurus/utils-common": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "@slorber/static-site-generator-webpack-plugin": "^4.0.7", "@svgr/webpack": "^6.2.1", "autoprefixer": "^10.4.7", @@ -2029,7 +2029,7 @@ "del": "^6.1.1", "detect-port": "^1.3.0", "escape-html": "^1.0.3", - "eta": "^1.12.3", + "eta": "^2.0.0", "file-loader": "^6.2.0", "fs-extra": "^10.1.0", "html-minifier-terser": "^6.1.0", @@ -2146,9 +2146,9 @@ } }, "node_modules/@docusaurus/cssnano-preset": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-2.3.0.tgz", - "integrity": "sha512-igmsXc1Q95lMeq07A1xua0/5wOPygDQ/ENSV7VVbiGhnvMv4gzkba8ZvbAtc7PmqK+kpYRfPzNCOk0GnQCvibg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-2.3.1.tgz", + "integrity": "sha512-7mIhAROES6CY1GmCjR4CZkUfjTL6B3u6rKHK0ChQl2d1IevYXq/k/vFgvOrJfcKxiObpMnE9+X6R2Wt1KqxC6w==", "dependencies": { "cssnano-preset-advanced": "^5.3.8", "postcss": "^8.4.14", @@ -2160,9 +2160,9 @@ } }, "node_modules/@docusaurus/logger": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-2.3.0.tgz", - "integrity": "sha512-GO8s+FJpNT0vwt6kr/BZ/B1iB8EgHH/CF590i55Epy3TP2baQHGEHcAnQWvz5067OXIEke7Sa8sUNi0V9FrcJw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-2.3.1.tgz", + "integrity": "sha512-2lAV/olKKVr9qJhfHFCaqBIl8FgYjbUFwgUnX76+cULwQYss+42ZQ3grHGFvI0ocN2X55WcYe64ellQXz7suqg==", "dependencies": { "chalk": "^4.1.2", "tslib": "^2.4.0" @@ -2236,14 +2236,14 @@ } }, "node_modules/@docusaurus/mdx-loader": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-2.3.0.tgz", - "integrity": "sha512-uxownG7dlg/l19rTIfUP0KDsbI8lTCgziWsdubMcWpGvOgXgm1p4mKSmWPzAwkRENn+un4L8DBhl3j1toeJy1A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-2.3.1.tgz", + "integrity": "sha512-Gzga7OsxQRpt3392K9lv/bW4jGppdLFJh3luKRknCKSAaZrmVkOQv2gvCn8LAOSZ3uRg5No7AgYs/vpL8K94lA==", "dependencies": { "@babel/parser": "^7.18.8", "@babel/traverse": "^7.18.8", - "@docusaurus/logger": "2.3.0", - "@docusaurus/utils": "2.3.0", + "@docusaurus/logger": "2.3.1", + "@docusaurus/utils": "2.3.1", "@mdx-js/mdx": "^1.6.22", "escape-html": "^1.0.3", "file-loader": "^6.2.0", @@ -2267,12 +2267,12 @@ } }, "node_modules/@docusaurus/module-type-aliases": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-2.3.0.tgz", - "integrity": "sha512-DvJtVejgrgIgxSNZ0pRaVu4EndRVBgbtp1LKvIO4xBgKlrsq8o4qkj1HKwH6yok5NoMqGApu8/E0KPOdZBtDpQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-2.3.1.tgz", + "integrity": "sha512-6KkxfAVOJqIUynTRb/tphYCl+co3cP0PlHiMDbi+SzmYxMdgIrwYqH9yAnGSDoN6Jk2ZE/JY/Azs/8LPgKP48A==", "dependencies": { "@docusaurus/react-loadable": "5.5.2", - "@docusaurus/types": "2.3.0", + "@docusaurus/types": "2.3.1", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -2286,17 +2286,17 @@ } }, "node_modules/@docusaurus/plugin-content-blog": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.3.0.tgz", - "integrity": "sha512-/v+nWEaqRxH1U4I6uJIMdj8Iilrh0XwIG5vsmsi4AXbpArgqqyfMjbf70lzPOmSdYfdWYgb7tWcA6OhJqyKj0w==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.3.1.tgz", + "integrity": "sha512-f5LjqX+9WkiLyGiQ41x/KGSJ/9bOjSD8lsVhPvYeUYHCtYpuiDKfhZE07O4EqpHkBx4NQdtQDbp+aptgHSTuiw==", "dependencies": { - "@docusaurus/core": "2.3.0", - "@docusaurus/logger": "2.3.0", - "@docusaurus/mdx-loader": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils": "2.3.0", - "@docusaurus/utils-common": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/logger": "2.3.1", + "@docusaurus/mdx-loader": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils": "2.3.1", + "@docusaurus/utils-common": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "cheerio": "^1.0.0-rc.12", "feed": "^4.2.2", "fs-extra": "^10.1.0", @@ -2316,17 +2316,17 @@ } }, "node_modules/@docusaurus/plugin-content-docs": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.3.0.tgz", - "integrity": "sha512-P53gYvtPY/VJTMdV5pFnKv8d7qMBOPyu/4NPREQU5PWsXJOYedCwNBqdAR7A5P69l55TrzyUEUYLjIcwuoSPGg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.3.1.tgz", + "integrity": "sha512-DxztTOBEruv7qFxqUtbsqXeNcHqcVEIEe+NQoI1oi2DBmKBhW/o0MIal8lt+9gvmpx3oYtlwmLOOGepxZgJGkw==", "dependencies": { - "@docusaurus/core": "2.3.0", - "@docusaurus/logger": "2.3.0", - "@docusaurus/mdx-loader": "2.3.0", - "@docusaurus/module-type-aliases": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/logger": "2.3.1", + "@docusaurus/mdx-loader": "2.3.1", + "@docusaurus/module-type-aliases": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "@types/react-router-config": "^5.0.6", "combine-promises": "^1.1.0", "fs-extra": "^10.1.0", @@ -2346,15 +2346,15 @@ } }, "node_modules/@docusaurus/plugin-content-pages": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.3.0.tgz", - "integrity": "sha512-H21Ux3Ln+pXlcp0RGdD1fyes7H3tsyhFpeflkxnCoXfTQf/pQB9IMuddFnxuXzj+34rp6jAQmLSaPssuixJXRQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.3.1.tgz", + "integrity": "sha512-E80UL6hvKm5VVw8Ka8YaVDtO6kWWDVUK4fffGvkpQ/AJQDOg99LwOXKujPoICC22nUFTsZ2Hp70XvpezCsFQaA==", "dependencies": { - "@docusaurus/core": "2.3.0", - "@docusaurus/mdx-loader": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/mdx-loader": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "fs-extra": "^10.1.0", "tslib": "^2.4.0", "webpack": "^5.73.0" @@ -2368,13 +2368,13 @@ } }, "node_modules/@docusaurus/plugin-debug": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-2.3.0.tgz", - "integrity": "sha512-TyeH3DMA9/8sIXyX8+zpdLtSixBnLJjW9KSvncKj/iXs1t20tpUZ1WFL7D+G1gxGGbLCBUGDluh738VvsRHC6Q==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-2.3.1.tgz", + "integrity": "sha512-Ujpml1Ppg4geB/2hyu2diWnO49az9U2bxM9Shen7b6qVcyFisNJTkVG2ocvLC7wM1efTJcUhBO6zAku2vKJGMw==", "dependencies": { - "@docusaurus/core": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils": "2.3.1", "fs-extra": "^10.1.0", "react-json-view": "^1.21.3", "tslib": "^2.4.0" @@ -2388,13 +2388,13 @@ } }, "node_modules/@docusaurus/plugin-google-analytics": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.3.0.tgz", - "integrity": "sha512-Z9FqTQzeOC1R6i/x07VgkrTKpQ4OtMe3WBOKZKzgldWXJr6CDUWPSR8pfDEjA+RRAj8ajUh0E+BliKBmFILQvQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.3.1.tgz", + "integrity": "sha512-OHip0GQxKOFU8n7gkt3TM4HOYTXPCFDjqKbMClDD3KaDnyTuMp/Zvd9HSr770lLEscgPWIvzhJByRAClqsUWiQ==", "dependencies": { - "@docusaurus/core": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "tslib": "^2.4.0" }, "engines": { @@ -2406,13 +2406,13 @@ } }, "node_modules/@docusaurus/plugin-google-gtag": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.3.0.tgz", - "integrity": "sha512-oZavqtfwQAGjz+Dyhsb45mVssTevCW1PJgLcmr3WKiID15GTolbBrrp/fueTrEh60DzOd81HbiCLs56JWBwDhQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.3.1.tgz", + "integrity": "sha512-uXtDhfu4+Hm+oqWUySr3DNI5cWC/rmP6XJyAk83Heor3dFjZqDwCbkX8yWPywkRiWev3Dk/rVF8lEn0vIGVocA==", "dependencies": { - "@docusaurus/core": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "tslib": "^2.4.0" }, "engines": { @@ -2424,13 +2424,13 @@ } }, "node_modules/@docusaurus/plugin-google-tag-manager": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-2.3.0.tgz", - "integrity": "sha512-toAhuMX1h+P2CfavwoDlz9s2/Zm7caiEznW/inxq3izywG2l9ujWI/o6u2g70O3ACQ19eHMGHDsyEUcRDPrxBw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-2.3.1.tgz", + "integrity": "sha512-Ww2BPEYSqg8q8tJdLYPFFM3FMDBCVhEM4UUqKzJaiRMx3NEoly3qqDRAoRDGdIhlC//Rf0iJV9cWAoq2m6k3sw==", "dependencies": { - "@docusaurus/core": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "tslib": "^2.4.0" }, "engines": { @@ -2442,16 +2442,16 @@ } }, "node_modules/@docusaurus/plugin-sitemap": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.3.0.tgz", - "integrity": "sha512-kwIHLP6lyubWOnNO0ejwjqdxB9C6ySnATN61etd6iwxHri5+PBZCEOv1sVm5U1gfQiDR1sVsXnJq2zNwLwgEtQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.3.1.tgz", + "integrity": "sha512-8Yxile/v6QGYV9vgFiYL+8d2N4z4Er3pSHsrD08c5XI8bUXxTppMwjarDUTH/TRTfgAWotRbhJ6WZLyajLpozA==", "dependencies": { - "@docusaurus/core": "2.3.0", - "@docusaurus/logger": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils": "2.3.0", - "@docusaurus/utils-common": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/logger": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils": "2.3.1", + "@docusaurus/utils-common": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "fs-extra": "^10.1.0", "sitemap": "^7.1.1", "tslib": "^2.4.0" @@ -2465,23 +2465,23 @@ } }, "node_modules/@docusaurus/preset-classic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-2.3.0.tgz", - "integrity": "sha512-mI37ieJe7cs5dHuvWz415U7hO209Q19Fp4iSHeFFgtQoK1PiRg7HJHkVbEsLZII2MivdzGFB5Hxoq2wUPWdNEA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-2.3.1.tgz", + "integrity": "sha512-OQ5W0AHyfdUk0IldwJ3BlnZ1EqoJuu2L2BMhqLbqwNWdkmzmSUvlFLH1Pe7CZSQgB2YUUC/DnmjbPKk/qQD0lQ==", "dependencies": { - "@docusaurus/core": "2.3.0", - "@docusaurus/plugin-content-blog": "2.3.0", - "@docusaurus/plugin-content-docs": "2.3.0", - "@docusaurus/plugin-content-pages": "2.3.0", - "@docusaurus/plugin-debug": "2.3.0", - "@docusaurus/plugin-google-analytics": "2.3.0", - "@docusaurus/plugin-google-gtag": "2.3.0", - "@docusaurus/plugin-google-tag-manager": "2.3.0", - "@docusaurus/plugin-sitemap": "2.3.0", - "@docusaurus/theme-classic": "2.3.0", - "@docusaurus/theme-common": "2.3.0", - "@docusaurus/theme-search-algolia": "2.3.0", - "@docusaurus/types": "2.3.0" + "@docusaurus/core": "2.3.1", + "@docusaurus/plugin-content-blog": "2.3.1", + "@docusaurus/plugin-content-docs": "2.3.1", + "@docusaurus/plugin-content-pages": "2.3.1", + "@docusaurus/plugin-debug": "2.3.1", + "@docusaurus/plugin-google-analytics": "2.3.1", + "@docusaurus/plugin-google-gtag": "2.3.1", + "@docusaurus/plugin-google-tag-manager": "2.3.1", + "@docusaurus/plugin-sitemap": "2.3.1", + "@docusaurus/theme-classic": "2.3.1", + "@docusaurus/theme-common": "2.3.1", + "@docusaurus/theme-search-algolia": "2.3.1", + "@docusaurus/types": "2.3.1" }, "engines": { "node": ">=16.14" @@ -2505,22 +2505,22 @@ } }, "node_modules/@docusaurus/theme-classic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-2.3.0.tgz", - "integrity": "sha512-x2h9KZ4feo22b1aArsfqvK05aDCgTkLZGRgAPY/9TevFV5/Yy19cZtBOCbzaKa2dKq1ofBRK9Hm1DdLJdLB14A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-2.3.1.tgz", + "integrity": "sha512-SelSIDvyttb7ZYHj8vEUhqykhAqfOPKk+uP0z85jH72IMC58e7O8DIlcAeBv+CWsLbNIl9/Hcg71X0jazuxJug==", "dependencies": { - "@docusaurus/core": "2.3.0", - "@docusaurus/mdx-loader": "2.3.0", - "@docusaurus/module-type-aliases": "2.3.0", - "@docusaurus/plugin-content-blog": "2.3.0", - "@docusaurus/plugin-content-docs": "2.3.0", - "@docusaurus/plugin-content-pages": "2.3.0", - "@docusaurus/theme-common": "2.3.0", - "@docusaurus/theme-translations": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils": "2.3.0", - "@docusaurus/utils-common": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/mdx-loader": "2.3.1", + "@docusaurus/module-type-aliases": "2.3.1", + "@docusaurus/plugin-content-blog": "2.3.1", + "@docusaurus/plugin-content-docs": "2.3.1", + "@docusaurus/plugin-content-pages": "2.3.1", + "@docusaurus/theme-common": "2.3.1", + "@docusaurus/theme-translations": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils": "2.3.1", + "@docusaurus/utils-common": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "@mdx-js/react": "^1.6.22", "clsx": "^1.2.1", "copy-text-to-clipboard": "^3.0.1", @@ -2544,16 +2544,16 @@ } }, "node_modules/@docusaurus/theme-common": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-2.3.0.tgz", - "integrity": "sha512-1eAvaULgu6ywHbjkdWOOHl1PdMylne/88i0kg25qimmkMgRHoIQ23JgRD/q5sFr+2YX7U7SggR1UNNsqu2zZPw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-2.3.1.tgz", + "integrity": "sha512-RYmYl2OR2biO+yhmW1aS5FyEvnrItPINa+0U2dMxcHpah8reSCjQ9eJGRmAgkZFchV1+aIQzXOI1K7LCW38O0g==", "dependencies": { - "@docusaurus/mdx-loader": "2.3.0", - "@docusaurus/module-type-aliases": "2.3.0", - "@docusaurus/plugin-content-blog": "2.3.0", - "@docusaurus/plugin-content-docs": "2.3.0", - "@docusaurus/plugin-content-pages": "2.3.0", - "@docusaurus/utils": "2.3.0", + "@docusaurus/mdx-loader": "2.3.1", + "@docusaurus/module-type-aliases": "2.3.1", + "@docusaurus/plugin-content-blog": "2.3.1", + "@docusaurus/plugin-content-docs": "2.3.1", + "@docusaurus/plugin-content-pages": "2.3.1", + "@docusaurus/utils": "2.3.1", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -2573,22 +2573,22 @@ } }, "node_modules/@docusaurus/theme-search-algolia": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.3.0.tgz", - "integrity": "sha512-/i5k1NAlbYvgnw69vJQA174+ipwdtTCCUvxRp7bVZ+8KmviEybAC/kuKe7WmiUbIGVYbAbwYaEsPuVnsd65DrA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.3.1.tgz", + "integrity": "sha512-JdHaRqRuH1X++g5fEMLnq7OtULSGQdrs9AbhcWRQ428ZB8/HOiaN6mj3hzHvcD3DFgu7koIVtWPQnvnN7iwzHA==", "dependencies": { "@docsearch/react": "^3.1.1", - "@docusaurus/core": "2.3.0", - "@docusaurus/logger": "2.3.0", - "@docusaurus/plugin-content-docs": "2.3.0", - "@docusaurus/theme-common": "2.3.0", - "@docusaurus/theme-translations": "2.3.0", - "@docusaurus/utils": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/logger": "2.3.1", + "@docusaurus/plugin-content-docs": "2.3.1", + "@docusaurus/theme-common": "2.3.1", + "@docusaurus/theme-translations": "2.3.1", + "@docusaurus/utils": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "algoliasearch": "^4.13.1", "algoliasearch-helper": "^3.10.0", "clsx": "^1.2.1", - "eta": "^1.12.3", + "eta": "^2.0.0", "fs-extra": "^10.1.0", "lodash": "^4.17.21", "tslib": "^2.4.0", @@ -2603,9 +2603,9 @@ } }, "node_modules/@docusaurus/theme-translations": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-2.3.0.tgz", - "integrity": "sha512-YLVD6LrszBld1EvThTOa9PcblKAZs1jOmRjwtffdg1CGjQWFXEeWUL24n2M4ARByzuLry5D8ZRVmKyRt3LOwsw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-2.3.1.tgz", + "integrity": "sha512-BsBZzAewJabVhoGG1Ij2u4pMS3MPW6gZ6sS4pc+Y7czevRpzxoFNJXRtQDVGe7mOpv/MmRmqg4owDK+lcOTCVQ==", "dependencies": { "fs-extra": "^10.1.0", "tslib": "^2.4.0" @@ -2615,9 +2615,9 @@ } }, "node_modules/@docusaurus/types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-2.3.0.tgz", - "integrity": "sha512-c5C0nROxVFsgMAm4vWDB1LDv3v4K18Y8eVxazL3dEr7w+7kNLc5koWrW7fWmCnrbItnuTna4nLS2PcSZrkYidg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-2.3.1.tgz", + "integrity": "sha512-PREbIRhTaNNY042qmfSE372Jb7djZt+oVTZkoqHJ8eff8vOIc2zqqDqBVc5BhOfpZGPTrE078yy/torUEZy08A==", "dependencies": { "@types/history": "^4.7.11", "@types/react": "*", @@ -2634,11 +2634,11 @@ } }, "node_modules/@docusaurus/utils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-2.3.0.tgz", - "integrity": "sha512-6+GCurDsePHHbLM3ktcjv8N4zrjgrl1O7gOQNG4UMktcwHssFFVm+geVcB4M8siOmwUjV2VaNrp0hpGy8DOQHw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-2.3.1.tgz", + "integrity": "sha512-9WcQROCV0MmrpOQDXDGhtGMd52DHpSFbKLfkyaYumzbTstrbA5pPOtiGtxK1nqUHkiIv8UwexS54p0Vod2I1lg==", "dependencies": { - "@docusaurus/logger": "2.3.0", + "@docusaurus/logger": "2.3.1", "@svgr/webpack": "^6.2.1", "escape-string-regexp": "^4.0.0", "file-loader": "^6.2.0", @@ -2668,9 +2668,9 @@ } }, "node_modules/@docusaurus/utils-common": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-2.3.0.tgz", - "integrity": "sha512-nu5An+26FS7SQTwvyFR4g9lw3NU1u2RLcxJPZF+NCOG8Ne96ciuQosa7+N1kllm/heEJqfTaAUD0sFxpTZrDtw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-2.3.1.tgz", + "integrity": "sha512-pVlRpXkdNcxmKNxAaB1ya2hfCEvVsLDp2joeM6K6uv55Oc5nVIqgyYSgSNKZyMdw66NnvMfsu0RBylcwZQKo9A==", "dependencies": { "tslib": "^2.4.0" }, @@ -2687,12 +2687,12 @@ } }, "node_modules/@docusaurus/utils-validation": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-2.3.0.tgz", - "integrity": "sha512-TBJCLqwAoiQQJ6dbgBpuLvzsn/XiTgbZkd6eJFUIQYLb1d473Zv58QrHXVmVQDLWiCgmJpHW2LpMfumTpCDgJw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-2.3.1.tgz", + "integrity": "sha512-7n0208IG3k1HVTByMHlZoIDjjOFC8sbViHVXJx0r3Q+3Ezrx+VQ1RZ/zjNn6lT+QBCRCXlnlaoJ8ug4HIVgQ3w==", "dependencies": { - "@docusaurus/logger": "2.3.0", - "@docusaurus/utils": "2.3.0", + "@docusaurus/logger": "2.3.1", + "@docusaurus/utils": "2.3.1", "joi": "^17.6.0", "js-yaml": "^4.1.0", "tslib": "^2.4.0" @@ -6719,10 +6719,9 @@ } }, "node_modules/eta": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/eta/-/eta-1.12.3.tgz", - "integrity": "sha512-qHixwbDLtekO/d51Yr4glcaUJCIjGVJyTzuqV4GPlgZo1YpgOKG+avQynErZIYrfM6JIJdtiG2Kox8tbb+DoGg==", - "license": "MIT", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-2.0.0.tgz", + "integrity": "sha512-NqE7S2VmVwgMS8yBxsH4VgNQjNjLq1gfGU0u9I6Cjh468nPRMoDfGdK9n1p/3Dvsw3ebklDkZsFAnKJ9sefjBA==", "engines": { "node": ">=6.0.0" }, @@ -15786,9 +15785,9 @@ } }, "@docusaurus/core": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-2.3.0.tgz", - "integrity": "sha512-2AU5HfKyExO+/mi41SBnx5uY0aGZFXr3D93wntBY4lN1gsDKUpi7EE4lPBAXm9CoH4Pw6N24yDHy9CPR3sh/uA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-2.3.1.tgz", + "integrity": "sha512-0Jd4jtizqnRAr7svWaBbbrCCN8mzBNd2xFLoT/IM7bGfFie5y58oz97KzXliwiLY3zWjqMXjQcuP1a5VgCv2JA==", "requires": { "@babel/core": "^7.18.6", "@babel/generator": "^7.18.7", @@ -15800,13 +15799,13 @@ "@babel/runtime": "^7.18.6", "@babel/runtime-corejs3": "^7.18.6", "@babel/traverse": "^7.18.8", - "@docusaurus/cssnano-preset": "2.3.0", - "@docusaurus/logger": "2.3.0", - "@docusaurus/mdx-loader": "2.3.0", + "@docusaurus/cssnano-preset": "2.3.1", + "@docusaurus/logger": "2.3.1", + "@docusaurus/mdx-loader": "2.3.1", "@docusaurus/react-loadable": "5.5.2", - "@docusaurus/utils": "2.3.0", - "@docusaurus/utils-common": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/utils": "2.3.1", + "@docusaurus/utils-common": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "@slorber/static-site-generator-webpack-plugin": "^4.0.7", "@svgr/webpack": "^6.2.1", "autoprefixer": "^10.4.7", @@ -15827,7 +15826,7 @@ "del": "^6.1.1", "detect-port": "^1.3.0", "escape-html": "^1.0.3", - "eta": "^1.12.3", + "eta": "^2.0.0", "file-loader": "^6.2.0", "fs-extra": "^10.1.0", "html-minifier-terser": "^6.1.0", @@ -15909,9 +15908,9 @@ } }, "@docusaurus/cssnano-preset": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-2.3.0.tgz", - "integrity": "sha512-igmsXc1Q95lMeq07A1xua0/5wOPygDQ/ENSV7VVbiGhnvMv4gzkba8ZvbAtc7PmqK+kpYRfPzNCOk0GnQCvibg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-2.3.1.tgz", + "integrity": "sha512-7mIhAROES6CY1GmCjR4CZkUfjTL6B3u6rKHK0ChQl2d1IevYXq/k/vFgvOrJfcKxiObpMnE9+X6R2Wt1KqxC6w==", "requires": { "cssnano-preset-advanced": "^5.3.8", "postcss": "^8.4.14", @@ -15920,9 +15919,9 @@ } }, "@docusaurus/logger": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-2.3.0.tgz", - "integrity": "sha512-GO8s+FJpNT0vwt6kr/BZ/B1iB8EgHH/CF590i55Epy3TP2baQHGEHcAnQWvz5067OXIEke7Sa8sUNi0V9FrcJw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-2.3.1.tgz", + "integrity": "sha512-2lAV/olKKVr9qJhfHFCaqBIl8FgYjbUFwgUnX76+cULwQYss+42ZQ3grHGFvI0ocN2X55WcYe64ellQXz7suqg==", "requires": { "chalk": "^4.1.2", "tslib": "^2.4.0" @@ -15974,14 +15973,14 @@ } }, "@docusaurus/mdx-loader": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-2.3.0.tgz", - "integrity": "sha512-uxownG7dlg/l19rTIfUP0KDsbI8lTCgziWsdubMcWpGvOgXgm1p4mKSmWPzAwkRENn+un4L8DBhl3j1toeJy1A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-2.3.1.tgz", + "integrity": "sha512-Gzga7OsxQRpt3392K9lv/bW4jGppdLFJh3luKRknCKSAaZrmVkOQv2gvCn8LAOSZ3uRg5No7AgYs/vpL8K94lA==", "requires": { "@babel/parser": "^7.18.8", "@babel/traverse": "^7.18.8", - "@docusaurus/logger": "2.3.0", - "@docusaurus/utils": "2.3.0", + "@docusaurus/logger": "2.3.1", + "@docusaurus/utils": "2.3.1", "@mdx-js/mdx": "^1.6.22", "escape-html": "^1.0.3", "file-loader": "^6.2.0", @@ -15998,12 +15997,12 @@ } }, "@docusaurus/module-type-aliases": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-2.3.0.tgz", - "integrity": "sha512-DvJtVejgrgIgxSNZ0pRaVu4EndRVBgbtp1LKvIO4xBgKlrsq8o4qkj1HKwH6yok5NoMqGApu8/E0KPOdZBtDpQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-2.3.1.tgz", + "integrity": "sha512-6KkxfAVOJqIUynTRb/tphYCl+co3cP0PlHiMDbi+SzmYxMdgIrwYqH9yAnGSDoN6Jk2ZE/JY/Azs/8LPgKP48A==", "requires": { "@docusaurus/react-loadable": "5.5.2", - "@docusaurus/types": "2.3.0", + "@docusaurus/types": "2.3.1", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -16013,17 +16012,17 @@ } }, "@docusaurus/plugin-content-blog": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.3.0.tgz", - "integrity": "sha512-/v+nWEaqRxH1U4I6uJIMdj8Iilrh0XwIG5vsmsi4AXbpArgqqyfMjbf70lzPOmSdYfdWYgb7tWcA6OhJqyKj0w==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.3.1.tgz", + "integrity": "sha512-f5LjqX+9WkiLyGiQ41x/KGSJ/9bOjSD8lsVhPvYeUYHCtYpuiDKfhZE07O4EqpHkBx4NQdtQDbp+aptgHSTuiw==", "requires": { - "@docusaurus/core": "2.3.0", - "@docusaurus/logger": "2.3.0", - "@docusaurus/mdx-loader": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils": "2.3.0", - "@docusaurus/utils-common": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/logger": "2.3.1", + "@docusaurus/mdx-loader": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils": "2.3.1", + "@docusaurus/utils-common": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "cheerio": "^1.0.0-rc.12", "feed": "^4.2.2", "fs-extra": "^10.1.0", @@ -16036,17 +16035,17 @@ } }, "@docusaurus/plugin-content-docs": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.3.0.tgz", - "integrity": "sha512-P53gYvtPY/VJTMdV5pFnKv8d7qMBOPyu/4NPREQU5PWsXJOYedCwNBqdAR7A5P69l55TrzyUEUYLjIcwuoSPGg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.3.1.tgz", + "integrity": "sha512-DxztTOBEruv7qFxqUtbsqXeNcHqcVEIEe+NQoI1oi2DBmKBhW/o0MIal8lt+9gvmpx3oYtlwmLOOGepxZgJGkw==", "requires": { - "@docusaurus/core": "2.3.0", - "@docusaurus/logger": "2.3.0", - "@docusaurus/mdx-loader": "2.3.0", - "@docusaurus/module-type-aliases": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/logger": "2.3.1", + "@docusaurus/mdx-loader": "2.3.1", + "@docusaurus/module-type-aliases": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "@types/react-router-config": "^5.0.6", "combine-promises": "^1.1.0", "fs-extra": "^10.1.0", @@ -16059,100 +16058,100 @@ } }, "@docusaurus/plugin-content-pages": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.3.0.tgz", - "integrity": "sha512-H21Ux3Ln+pXlcp0RGdD1fyes7H3tsyhFpeflkxnCoXfTQf/pQB9IMuddFnxuXzj+34rp6jAQmLSaPssuixJXRQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.3.1.tgz", + "integrity": "sha512-E80UL6hvKm5VVw8Ka8YaVDtO6kWWDVUK4fffGvkpQ/AJQDOg99LwOXKujPoICC22nUFTsZ2Hp70XvpezCsFQaA==", "requires": { - "@docusaurus/core": "2.3.0", - "@docusaurus/mdx-loader": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/mdx-loader": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "fs-extra": "^10.1.0", "tslib": "^2.4.0", "webpack": "^5.73.0" } }, "@docusaurus/plugin-debug": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-2.3.0.tgz", - "integrity": "sha512-TyeH3DMA9/8sIXyX8+zpdLtSixBnLJjW9KSvncKj/iXs1t20tpUZ1WFL7D+G1gxGGbLCBUGDluh738VvsRHC6Q==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-2.3.1.tgz", + "integrity": "sha512-Ujpml1Ppg4geB/2hyu2diWnO49az9U2bxM9Shen7b6qVcyFisNJTkVG2ocvLC7wM1efTJcUhBO6zAku2vKJGMw==", "requires": { - "@docusaurus/core": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils": "2.3.1", "fs-extra": "^10.1.0", "react-json-view": "^1.21.3", "tslib": "^2.4.0" } }, "@docusaurus/plugin-google-analytics": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.3.0.tgz", - "integrity": "sha512-Z9FqTQzeOC1R6i/x07VgkrTKpQ4OtMe3WBOKZKzgldWXJr6CDUWPSR8pfDEjA+RRAj8ajUh0E+BliKBmFILQvQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.3.1.tgz", + "integrity": "sha512-OHip0GQxKOFU8n7gkt3TM4HOYTXPCFDjqKbMClDD3KaDnyTuMp/Zvd9HSr770lLEscgPWIvzhJByRAClqsUWiQ==", "requires": { - "@docusaurus/core": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "tslib": "^2.4.0" } }, "@docusaurus/plugin-google-gtag": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.3.0.tgz", - "integrity": "sha512-oZavqtfwQAGjz+Dyhsb45mVssTevCW1PJgLcmr3WKiID15GTolbBrrp/fueTrEh60DzOd81HbiCLs56JWBwDhQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.3.1.tgz", + "integrity": "sha512-uXtDhfu4+Hm+oqWUySr3DNI5cWC/rmP6XJyAk83Heor3dFjZqDwCbkX8yWPywkRiWev3Dk/rVF8lEn0vIGVocA==", "requires": { - "@docusaurus/core": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "tslib": "^2.4.0" } }, "@docusaurus/plugin-google-tag-manager": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-2.3.0.tgz", - "integrity": "sha512-toAhuMX1h+P2CfavwoDlz9s2/Zm7caiEznW/inxq3izywG2l9ujWI/o6u2g70O3ACQ19eHMGHDsyEUcRDPrxBw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-2.3.1.tgz", + "integrity": "sha512-Ww2BPEYSqg8q8tJdLYPFFM3FMDBCVhEM4UUqKzJaiRMx3NEoly3qqDRAoRDGdIhlC//Rf0iJV9cWAoq2m6k3sw==", "requires": { - "@docusaurus/core": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "tslib": "^2.4.0" } }, "@docusaurus/plugin-sitemap": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.3.0.tgz", - "integrity": "sha512-kwIHLP6lyubWOnNO0ejwjqdxB9C6ySnATN61etd6iwxHri5+PBZCEOv1sVm5U1gfQiDR1sVsXnJq2zNwLwgEtQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.3.1.tgz", + "integrity": "sha512-8Yxile/v6QGYV9vgFiYL+8d2N4z4Er3pSHsrD08c5XI8bUXxTppMwjarDUTH/TRTfgAWotRbhJ6WZLyajLpozA==", "requires": { - "@docusaurus/core": "2.3.0", - "@docusaurus/logger": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils": "2.3.0", - "@docusaurus/utils-common": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/logger": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils": "2.3.1", + "@docusaurus/utils-common": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "fs-extra": "^10.1.0", "sitemap": "^7.1.1", "tslib": "^2.4.0" } }, "@docusaurus/preset-classic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-2.3.0.tgz", - "integrity": "sha512-mI37ieJe7cs5dHuvWz415U7hO209Q19Fp4iSHeFFgtQoK1PiRg7HJHkVbEsLZII2MivdzGFB5Hxoq2wUPWdNEA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-2.3.1.tgz", + "integrity": "sha512-OQ5W0AHyfdUk0IldwJ3BlnZ1EqoJuu2L2BMhqLbqwNWdkmzmSUvlFLH1Pe7CZSQgB2YUUC/DnmjbPKk/qQD0lQ==", "requires": { - "@docusaurus/core": "2.3.0", - "@docusaurus/plugin-content-blog": "2.3.0", - "@docusaurus/plugin-content-docs": "2.3.0", - "@docusaurus/plugin-content-pages": "2.3.0", - "@docusaurus/plugin-debug": "2.3.0", - "@docusaurus/plugin-google-analytics": "2.3.0", - "@docusaurus/plugin-google-gtag": "2.3.0", - "@docusaurus/plugin-google-tag-manager": "2.3.0", - "@docusaurus/plugin-sitemap": "2.3.0", - "@docusaurus/theme-classic": "2.3.0", - "@docusaurus/theme-common": "2.3.0", - "@docusaurus/theme-search-algolia": "2.3.0", - "@docusaurus/types": "2.3.0" + "@docusaurus/core": "2.3.1", + "@docusaurus/plugin-content-blog": "2.3.1", + "@docusaurus/plugin-content-docs": "2.3.1", + "@docusaurus/plugin-content-pages": "2.3.1", + "@docusaurus/plugin-debug": "2.3.1", + "@docusaurus/plugin-google-analytics": "2.3.1", + "@docusaurus/plugin-google-gtag": "2.3.1", + "@docusaurus/plugin-google-tag-manager": "2.3.1", + "@docusaurus/plugin-sitemap": "2.3.1", + "@docusaurus/theme-classic": "2.3.1", + "@docusaurus/theme-common": "2.3.1", + "@docusaurus/theme-search-algolia": "2.3.1", + "@docusaurus/types": "2.3.1" } }, "@docusaurus/react-loadable": { @@ -16165,22 +16164,22 @@ } }, "@docusaurus/theme-classic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-2.3.0.tgz", - "integrity": "sha512-x2h9KZ4feo22b1aArsfqvK05aDCgTkLZGRgAPY/9TevFV5/Yy19cZtBOCbzaKa2dKq1ofBRK9Hm1DdLJdLB14A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-2.3.1.tgz", + "integrity": "sha512-SelSIDvyttb7ZYHj8vEUhqykhAqfOPKk+uP0z85jH72IMC58e7O8DIlcAeBv+CWsLbNIl9/Hcg71X0jazuxJug==", "requires": { - "@docusaurus/core": "2.3.0", - "@docusaurus/mdx-loader": "2.3.0", - "@docusaurus/module-type-aliases": "2.3.0", - "@docusaurus/plugin-content-blog": "2.3.0", - "@docusaurus/plugin-content-docs": "2.3.0", - "@docusaurus/plugin-content-pages": "2.3.0", - "@docusaurus/theme-common": "2.3.0", - "@docusaurus/theme-translations": "2.3.0", - "@docusaurus/types": "2.3.0", - "@docusaurus/utils": "2.3.0", - "@docusaurus/utils-common": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/mdx-loader": "2.3.1", + "@docusaurus/module-type-aliases": "2.3.1", + "@docusaurus/plugin-content-blog": "2.3.1", + "@docusaurus/plugin-content-docs": "2.3.1", + "@docusaurus/plugin-content-pages": "2.3.1", + "@docusaurus/theme-common": "2.3.1", + "@docusaurus/theme-translations": "2.3.1", + "@docusaurus/types": "2.3.1", + "@docusaurus/utils": "2.3.1", + "@docusaurus/utils-common": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "@mdx-js/react": "^1.6.22", "clsx": "^1.2.1", "copy-text-to-clipboard": "^3.0.1", @@ -16197,16 +16196,16 @@ } }, "@docusaurus/theme-common": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-2.3.0.tgz", - "integrity": "sha512-1eAvaULgu6ywHbjkdWOOHl1PdMylne/88i0kg25qimmkMgRHoIQ23JgRD/q5sFr+2YX7U7SggR1UNNsqu2zZPw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-2.3.1.tgz", + "integrity": "sha512-RYmYl2OR2biO+yhmW1aS5FyEvnrItPINa+0U2dMxcHpah8reSCjQ9eJGRmAgkZFchV1+aIQzXOI1K7LCW38O0g==", "requires": { - "@docusaurus/mdx-loader": "2.3.0", - "@docusaurus/module-type-aliases": "2.3.0", - "@docusaurus/plugin-content-blog": "2.3.0", - "@docusaurus/plugin-content-docs": "2.3.0", - "@docusaurus/plugin-content-pages": "2.3.0", - "@docusaurus/utils": "2.3.0", + "@docusaurus/mdx-loader": "2.3.1", + "@docusaurus/module-type-aliases": "2.3.1", + "@docusaurus/plugin-content-blog": "2.3.1", + "@docusaurus/plugin-content-docs": "2.3.1", + "@docusaurus/plugin-content-pages": "2.3.1", + "@docusaurus/utils": "2.3.1", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -16219,22 +16218,22 @@ } }, "@docusaurus/theme-search-algolia": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.3.0.tgz", - "integrity": "sha512-/i5k1NAlbYvgnw69vJQA174+ipwdtTCCUvxRp7bVZ+8KmviEybAC/kuKe7WmiUbIGVYbAbwYaEsPuVnsd65DrA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.3.1.tgz", + "integrity": "sha512-JdHaRqRuH1X++g5fEMLnq7OtULSGQdrs9AbhcWRQ428ZB8/HOiaN6mj3hzHvcD3DFgu7koIVtWPQnvnN7iwzHA==", "requires": { "@docsearch/react": "^3.1.1", - "@docusaurus/core": "2.3.0", - "@docusaurus/logger": "2.3.0", - "@docusaurus/plugin-content-docs": "2.3.0", - "@docusaurus/theme-common": "2.3.0", - "@docusaurus/theme-translations": "2.3.0", - "@docusaurus/utils": "2.3.0", - "@docusaurus/utils-validation": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/logger": "2.3.1", + "@docusaurus/plugin-content-docs": "2.3.1", + "@docusaurus/theme-common": "2.3.1", + "@docusaurus/theme-translations": "2.3.1", + "@docusaurus/utils": "2.3.1", + "@docusaurus/utils-validation": "2.3.1", "algoliasearch": "^4.13.1", "algoliasearch-helper": "^3.10.0", "clsx": "^1.2.1", - "eta": "^1.12.3", + "eta": "^2.0.0", "fs-extra": "^10.1.0", "lodash": "^4.17.21", "tslib": "^2.4.0", @@ -16242,18 +16241,18 @@ } }, "@docusaurus/theme-translations": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-2.3.0.tgz", - "integrity": "sha512-YLVD6LrszBld1EvThTOa9PcblKAZs1jOmRjwtffdg1CGjQWFXEeWUL24n2M4ARByzuLry5D8ZRVmKyRt3LOwsw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-2.3.1.tgz", + "integrity": "sha512-BsBZzAewJabVhoGG1Ij2u4pMS3MPW6gZ6sS4pc+Y7czevRpzxoFNJXRtQDVGe7mOpv/MmRmqg4owDK+lcOTCVQ==", "requires": { "fs-extra": "^10.1.0", "tslib": "^2.4.0" } }, "@docusaurus/types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-2.3.0.tgz", - "integrity": "sha512-c5C0nROxVFsgMAm4vWDB1LDv3v4K18Y8eVxazL3dEr7w+7kNLc5koWrW7fWmCnrbItnuTna4nLS2PcSZrkYidg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-2.3.1.tgz", + "integrity": "sha512-PREbIRhTaNNY042qmfSE372Jb7djZt+oVTZkoqHJ8eff8vOIc2zqqDqBVc5BhOfpZGPTrE078yy/torUEZy08A==", "requires": { "@types/history": "^4.7.11", "@types/react": "*", @@ -16266,11 +16265,11 @@ } }, "@docusaurus/utils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-2.3.0.tgz", - "integrity": "sha512-6+GCurDsePHHbLM3ktcjv8N4zrjgrl1O7gOQNG4UMktcwHssFFVm+geVcB4M8siOmwUjV2VaNrp0hpGy8DOQHw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-2.3.1.tgz", + "integrity": "sha512-9WcQROCV0MmrpOQDXDGhtGMd52DHpSFbKLfkyaYumzbTstrbA5pPOtiGtxK1nqUHkiIv8UwexS54p0Vod2I1lg==", "requires": { - "@docusaurus/logger": "2.3.0", + "@docusaurus/logger": "2.3.1", "@svgr/webpack": "^6.2.1", "escape-string-regexp": "^4.0.0", "file-loader": "^6.2.0", @@ -16296,20 +16295,20 @@ } }, "@docusaurus/utils-common": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-2.3.0.tgz", - "integrity": "sha512-nu5An+26FS7SQTwvyFR4g9lw3NU1u2RLcxJPZF+NCOG8Ne96ciuQosa7+N1kllm/heEJqfTaAUD0sFxpTZrDtw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-2.3.1.tgz", + "integrity": "sha512-pVlRpXkdNcxmKNxAaB1ya2hfCEvVsLDp2joeM6K6uv55Oc5nVIqgyYSgSNKZyMdw66NnvMfsu0RBylcwZQKo9A==", "requires": { "tslib": "^2.4.0" } }, "@docusaurus/utils-validation": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-2.3.0.tgz", - "integrity": "sha512-TBJCLqwAoiQQJ6dbgBpuLvzsn/XiTgbZkd6eJFUIQYLb1d473Zv58QrHXVmVQDLWiCgmJpHW2LpMfumTpCDgJw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-2.3.1.tgz", + "integrity": "sha512-7n0208IG3k1HVTByMHlZoIDjjOFC8sbViHVXJx0r3Q+3Ezrx+VQ1RZ/zjNn6lT+QBCRCXlnlaoJ8ug4HIVgQ3w==", "requires": { - "@docusaurus/logger": "2.3.0", - "@docusaurus/utils": "2.3.0", + "@docusaurus/logger": "2.3.1", + "@docusaurus/utils": "2.3.1", "joi": "^17.6.0", "js-yaml": "^4.1.0", "tslib": "^2.4.0" @@ -19065,9 +19064,9 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, "eta": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/eta/-/eta-1.12.3.tgz", - "integrity": "sha512-qHixwbDLtekO/d51Yr4glcaUJCIjGVJyTzuqV4GPlgZo1YpgOKG+avQynErZIYrfM6JIJdtiG2Kox8tbb+DoGg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-2.0.0.tgz", + "integrity": "sha512-NqE7S2VmVwgMS8yBxsH4VgNQjNjLq1gfGU0u9I6Cjh468nPRMoDfGdK9n1p/3Dvsw3ebklDkZsFAnKJ9sefjBA==" }, "etag": { "version": "1.8.1", diff --git a/website/package.json b/website/package.json index 08e24f415..63f86b146 100644 --- a/website/package.json +++ b/website/package.json @@ -14,9 +14,9 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "dependencies": { - "@docusaurus/core": "2.3.0", - "@docusaurus/plugin-google-gtag": "^2.3.0", - "@docusaurus/preset-classic": "2.3.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/plugin-google-gtag": "^2.3.1", + "@docusaurus/preset-classic": "2.3.1", "@loadable/component": "^5.15.3", "@mdx-js/react": "^1.6.22", "animate.css": "^4.1.1", @@ -35,7 +35,7 @@ "wow.js": "^1.2.2" }, "devDependencies": { - "@docusaurus/module-type-aliases": "2.3.0", + "@docusaurus/module-type-aliases": "2.3.1", "@iconify/react": "^4.1.0", "autoprefixer": "^10.4.13", "postcss": "^8.4.21", From 0ae904e9bc8dd1d02b0457be833ac53f8198e788 Mon Sep 17 00:00:00 2001 From: Keepers Date: Fri, 3 Feb 2023 21:10:22 -0700 Subject: [PATCH 38/40] fix user query filtering on null userType (#2399) ## Description UserType can be null for users created before 2014. In order to not filter them out with the other guest users, we have to amend our user query to only exclude guests, and retain all other persons. ## Does this PR need a docs update or release note? - [x] :white_check_mark: Yes, it's included ## Type of change - [x] :bug: Bugfix ## Test Plan - [x] :muscle: Manual - [x] :zap: Unit test - [x] :green_heart: E2E --- CHANGELOG.md | 1 + src/internal/connector/discovery/api/users.go | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0303c9206..7c029b8f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Backing up a calendar that has the same name as the default calendar - Added additional backoff-retry to all OneDrive queries. +- Users with `null` userType values are no longer excluded from user queries. ### Known Issues diff --git a/src/internal/connector/discovery/api/users.go b/src/internal/connector/discovery/api/users.go index e4a8e0f00..c08297a9e 100644 --- a/src/internal/connector/discovery/api/users.go +++ b/src/internal/connector/discovery/api/users.go @@ -3,6 +3,7 @@ package api import ( "context" + absser "github.com/microsoft/kiota-abstractions-go" msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core" "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/users" @@ -58,14 +59,27 @@ const ( // require more fine-tuned controls in the future. // https://stackoverflow.com/questions/64044266/error-message-unsupported-or-invalid-query-filter-clause-specified-for-property // +// ne 'Guest' ensures we don't filter out users where userType = null, which can happen +// for user accounts created prior to 2014. In order to use the `ne` comparator, we +// MUST include $count=true and the ConsistencyLevel: eventual header. +// https://stackoverflow.com/questions/49340485/how-to-filter-users-by-usertype-null +// //nolint:lll -var userFilterNoGuests = "onPremisesSyncEnabled eq true OR userType eq 'Member'" +var userFilterNoGuests = "onPremisesSyncEnabled eq true OR userType ne 'Guest'" + +// I can't believe I have to do this. +var t = true func userOptions(fs *string) *users.UsersRequestBuilderGetRequestConfiguration { + headers := absser.NewRequestHeaders() + headers.Add("ConsistencyLevel", "eventual") + return &users.UsersRequestBuilderGetRequestConfiguration{ + Headers: headers, QueryParameters: &users.UsersRequestBuilderGetQueryParameters{ Select: []string{userSelectID, userSelectPrincipalName, userSelectDisplayName}, Filter: fs, + Count: &t, }, } } From c7e74edc4955e301ff5d5e249d0142841a49a29f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C4=8Dnica=20Mellifera?= Date: Fri, 3 Feb 2023 21:44:32 -0800 Subject: [PATCH 39/40] Blog corso storage (#2396) ## Description ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [ ] :bug: Bugfix - [x] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * # ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- website/blog/2023-2-4-where-to-store-corso.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/website/blog/2023-2-4-where-to-store-corso.md b/website/blog/2023-2-4-where-to-store-corso.md index cdde919b4..2d9da3ea7 100644 --- a/website/blog/2023-2-4-where-to-store-corso.md +++ b/website/blog/2023-2-4-where-to-store-corso.md @@ -47,7 +47,7 @@ here’s the single script that can run a non-production instance of MinIO withi and the AWS CLI as prerequisites) and get you started with Corso quickly: ```bash -mkdir -p ~\s/minio/data +mkdir -p $HOME/minio/data docker run \ -p 9000:9000 \ @@ -68,8 +68,12 @@ export AWS_SECRET_ACCESS_KEY=CHANGEME123 aws s3api create-bucket --bucket corso-backup --endpoint=http://127.0.0.1:9000 ``` -To connect Corso to a local MinIO server with `[corso repo init](https://corsobackup.io/docs/cli/corso-repo-init-s3/)` -you’ll want to pass the `--disable-tls` flag so that it will accept an `http` connection +To connect Corso to a local MinIO server with [`corso repo init`](https://corsobackup.io/docs/cli/corso-repo-init-s3/) +you’ll want to pass the `--disable-tls` flag so that it will accept an `http` connection. The full command would look like: + +```bash +./corso repo init s3 --bucket corso-backup --disable-tls --endpoint 127.0.0.1:9000 +``` ## Reducing Cost With S3 Storage Classes From 438bcd78eda288f6580da40bcfad792ea8911585 Mon Sep 17 00:00:00 2001 From: Danny Date: Sat, 4 Feb 2023 16:20:35 -0500 Subject: [PATCH 40/40] GC: Restore: SharePoint: Serialization Support (#2224) ## Description Adds serialization method for `SharePoint.Page` objects. Test suite included ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :sunflower: Feature ## Issue(s) * related to #2169 * related to #2173 ## Test Plan - [x] :zap: Unit test --- .github/workflows/ci.yml | 2 +- .../mock_data_collection_test.go | 9 ++ .../connector/mockconnector/mock_data_page.go | 25 ++++ src/internal/connector/support/m365Support.go | 36 ++++- .../connector/support/m365Support_test.go | 128 +++++++++++++++++- 5 files changed, 188 insertions(+), 12 deletions(-) create mode 100644 src/internal/connector/mockconnector/mock_data_page.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7083e885b..170a63357 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: working-directory: src steps: - uses: actions/checkout@v3 - + # single setup and sum cache handling here. # the results will cascade onto both testing and linting. - name: Setup Golang with cache diff --git a/src/internal/connector/mockconnector/mock_data_collection_test.go b/src/internal/connector/mockconnector/mock_data_collection_test.go index 2af9236c4..f2ba4d08e 100644 --- a/src/internal/connector/mockconnector/mock_data_collection_test.go +++ b/src/internal/connector/mockconnector/mock_data_collection_test.go @@ -202,6 +202,15 @@ func (suite *MockExchangeDataSuite) TestMockByteHydration() { return err }, }, + { + name: "SharePoint: Page", + transformation: func(t *testing.T) error { + bytes := mockconnector.GetMockPage(subject) + _, err := support.CreatePageFromBytes(bytes) + + return err + }, + }, } for _, test := range tests { diff --git a/src/internal/connector/mockconnector/mock_data_page.go b/src/internal/connector/mockconnector/mock_data_page.go new file mode 100644 index 000000000..0b8425418 --- /dev/null +++ b/src/internal/connector/mockconnector/mock_data_page.go @@ -0,0 +1,25 @@ +package mockconnector + +// GetMockPage returns bytes for models.SitePageable object +// Title string changes of fields: name and title +func GetMockPage(title string) []byte { + fileName := title + ".aspx" + + // Create Test Page + //nolint:lll + byteArray := []byte("{\"name\":\"" + fileName + "\",\"title\":\"" + title + "\",\"pageLayout\":\"article\",\"showComments\":true," + + "\"showRecommendedPages\":false,\"titleArea\":{\"enableGradientEffect\":true,\"imageWebUrl\":\"/_LAYOUTS/IMAGES/VISUALTEMPLATETITLEIMAGE.JPG\"," + + "\"layout\":\"colorBlock\",\"showAuthor\":true,\"showPublishedDate\":false,\"showTextBlockAboveTitle\":false,\"textAboveTitle\":\"TEXTABOVETITLE\"," + + "\"textAlignment\":\"left\",\"imageSourceType\":2,\"title\":\"sample1\"}," + + "\"canvasLayout\":{\"horizontalSections\":[{\"layout\":\"oneThirdRightColumn\",\"id\":\"1\",\"emphasis\":\"none\",\"columns\":[{\"id\":\"1\",\"width\":8," + + "\"webparts\":[{\"id\":\"6f9230af-2a98-4952-b205-9ede4f9ef548\",\"innerHtml\":\"

Hello!

\"}]},{\"id\":\"2\",\"width\":4," + + "\"webparts\":[{\"id\":\"73d07dde-3474-4545-badb-f28ba239e0e1\",\"webPartType\":\"d1d91016-032f-456d-98a4-721247c305e8\",\"data\":{\"dataVersion\":\"1.9\"," + + "\"description\":\"Showanimageonyourpage\",\"title\":\"Image\",\"properties\":{\"imageSourceType\":2,\"altText\":\"\",\"overlayText\":\"\"," + + "\"siteid\":\"0264cabe-6b92-450a-b162-b0c3d54fe5e8\",\"webid\":\"f3989670-cd37-4514-8ccb-0f7c2cbe5314\",\"listid\":\"bdb41041-eb06-474e-ac29-87093386bb14\"," + + "\"uniqueid\":\"d9f94b40-78ba-48d0-a39f-3cb23c2fe7eb\",\"imgWidth\":4288,\"imgHeight\":2848,\"fixAspectRatio\":false,\"captionText\":\"\",\"alignment\":\"Center\"}," + + "\"serverProcessedContent\":{\"imageSources\":[{\"key\":\"imageSource\",\"value\":\"/_LAYOUTS/IMAGES/VISUALTEMPLATEIMAGE1.JPG\"}]," + + "\"customMetadata\":[{\"key\":\"imageSource\",\"value\":{\"siteid\":\"0264cabe-6b92-450a-b162-b0c3d54fe5e8\",\"webid\":\"f3989670-cd37-4514-8ccb-0f7c2cbe5314\"," + + "\"listid\":\"bdb41041-eb06-474e-ac29-87093386bb14\",\"uniqueid\":\"d9f94b40-78ba-48d0-a39f-3cb23c2fe7eb\",\"width\":\"4288\",\"height\":\"2848\"}}]}}}]}]}]}}") + + return byteArray +} diff --git a/src/internal/connector/support/m365Support.go b/src/internal/connector/support/m365Support.go index d7e51e513..0780a2b0e 100644 --- a/src/internal/connector/support/m365Support.go +++ b/src/internal/connector/support/m365Support.go @@ -1,6 +1,9 @@ package support import ( + "strings" + + bmodels "github.com/alcionai/corso/src/internal/connector/graph/betasdk/models" absser "github.com/microsoft/kiota-abstractions-go/serialization" js "github.com/microsoft/kiota-serialization-json-go" "github.com/microsoftgraph/msgraph-sdk-go/models" @@ -12,7 +15,7 @@ import ( func CreateFromBytes(bytes []byte, createFunc absser.ParsableFactory) (absser.Parsable, error) { parseNode, err := js.NewJsonParseNodeFactory().GetRootParseNode("application/json", bytes) if err != nil { - return nil, errors.Wrap(err, "parsing byte array into m365 object") + return nil, errors.Wrap(err, "deserializing bytes into base m365 object") } anObject, err := parseNode.GetObjectValue(createFunc) @@ -27,7 +30,7 @@ func CreateFromBytes(bytes []byte, createFunc absser.ParsableFactory) (absser.Pa func CreateMessageFromBytes(bytes []byte) (models.Messageable, error) { aMessage, err := CreateFromBytes(bytes, models.CreateMessageFromDiscriminatorValue) if err != nil { - return nil, errors.Wrap(err, "creating m365 exchange.Mail object from provided bytes") + return nil, errors.Wrap(err, "deserializing bytes to exchange message") } message := aMessage.(models.Messageable) @@ -40,7 +43,7 @@ func CreateMessageFromBytes(bytes []byte) (models.Messageable, error) { func CreateContactFromBytes(bytes []byte) (models.Contactable, error) { parsable, err := CreateFromBytes(bytes, models.CreateContactFromDiscriminatorValue) if err != nil { - return nil, errors.Wrap(err, "creating m365 exchange.Contact object from provided bytes") + return nil, errors.Wrap(err, "deserializing bytes to exchange contact") } contact := parsable.(models.Contactable) @@ -52,7 +55,7 @@ func CreateContactFromBytes(bytes []byte) (models.Contactable, error) { func CreateEventFromBytes(bytes []byte) (models.Eventable, error) { parsable, err := CreateFromBytes(bytes, models.CreateEventFromDiscriminatorValue) if err != nil { - return nil, errors.Wrap(err, "creating m365 exchange.Event object from provided bytes") + return nil, errors.Wrap(err, "deserializing bytes to exchange event") } event := parsable.(models.Eventable) @@ -64,10 +67,33 @@ func CreateEventFromBytes(bytes []byte) (models.Eventable, error) { func CreateListFromBytes(bytes []byte) (models.Listable, error) { parsable, err := CreateFromBytes(bytes, models.CreateListFromDiscriminatorValue) if err != nil { - return nil, errors.Wrap(err, "creating m365 sharepoint.List object from provided bytes") + return nil, errors.Wrap(err, "deserializing bytes to sharepoint list") } list := parsable.(models.Listable) return list, nil } + +// CreatePageFromBytes transforms given bytes in models.SitePageable object +func CreatePageFromBytes(bytes []byte) (bmodels.SitePageable, error) { + parsable, err := CreateFromBytes(bytes, bmodels.CreateSitePageFromDiscriminatorValue) + if err != nil { + return nil, errors.Wrap(err, "deserializing bytes to sharepoint page") + } + + page := parsable.(bmodels.SitePageable) + + return page, nil +} + +func HasAttachments(body models.ItemBodyable) bool { + if body.GetContent() == nil || body.GetContentType() == nil || + *body.GetContentType() == models.TEXT_BODYTYPE || len(*body.GetContent()) == 0 { + return false + } + + content := *body.GetContent() + + return strings.Contains(content, "src=\"cid:") +} diff --git a/src/internal/connector/support/m365Support_test.go b/src/internal/connector/support/m365Support_test.go index c04c74604..946996431 100644 --- a/src/internal/connector/support/m365Support_test.go +++ b/src/internal/connector/support/m365Support_test.go @@ -3,10 +3,13 @@ package support import ( "testing" + kioser "github.com/microsoft/kiota-serialization-json-go" + "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + bmodels "github.com/alcionai/corso/src/internal/connector/graph/betasdk/models" "github.com/alcionai/corso/src/internal/connector/mockconnector" ) @@ -18,6 +21,11 @@ func TestDataSupportSuite(t *testing.T) { suite.Run(t, new(DataSupportSuite)) } +var ( + empty = "Empty Bytes" + invalid = "Invalid Bytes" +) + // TestCreateMessageFromBytes verifies approved mockdata bytes can // be successfully transformed into M365 Message data. func (suite *DataSupportSuite) TestCreateMessageFromBytes() { @@ -59,13 +67,13 @@ func (suite *DataSupportSuite) TestCreateContactFromBytes() { isNil assert.ValueAssertionFunc }{ { - name: "Empty Bytes", + name: empty, byteArray: make([]byte, 0), checkError: assert.Error, isNil: assert.Nil, }, { - name: "Invalid Bytes", + name: invalid, byteArray: []byte("A random sentence doesn't make an object"), checkError: assert.Error, isNil: assert.Nil, @@ -94,13 +102,13 @@ func (suite *DataSupportSuite) TestCreateEventFromBytes() { isNil assert.ValueAssertionFunc }{ { - name: "Empty Byes", + name: empty, byteArray: make([]byte, 0), checkError: assert.Error, isNil: assert.Nil, }, { - name: "Invalid Bytes", + name: invalid, byteArray: []byte("Invalid byte stream \"subject:\" Not going to work"), checkError: assert.Error, isNil: assert.Nil, @@ -132,13 +140,13 @@ func (suite *DataSupportSuite) TestCreateListFromBytes() { isNil assert.ValueAssertionFunc }{ { - name: "Empty Byes", + name: empty, byteArray: make([]byte, 0), checkError: assert.Error, isNil: assert.Nil, }, { - name: "Invalid Bytes", + name: invalid, byteArray: []byte("Invalid byte stream \"subject:\" Not going to work"), checkError: assert.Error, isNil: assert.Nil, @@ -159,3 +167,111 @@ func (suite *DataSupportSuite) TestCreateListFromBytes() { }) } } + +func (suite *DataSupportSuite) TestCreatePageFromBytes() { + tests := []struct { + name string + checkError assert.ErrorAssertionFunc + isNil assert.ValueAssertionFunc + getBytes func(t *testing.T) []byte + }{ + { + empty, + assert.Error, + assert.Nil, + func(t *testing.T) []byte { + return make([]byte, 0) + }, + }, + { + invalid, + assert.Error, + assert.Nil, + func(t *testing.T) []byte { + return []byte("snarf") + }, + }, + { + "Valid Page", + assert.NoError, + assert.NotNil, + func(t *testing.T) []byte { + pg := bmodels.NewSitePage() + title := "Tested" + pg.SetTitle(&title) + pg.SetName(&title) + pg.SetWebUrl(&title) + + writer := kioser.NewJsonSerializationWriter() + err := pg.Serialize(writer) + require.NoError(t, err) + + byteArray, err := writer.GetSerializedContent() + require.NoError(t, err) + + return byteArray + }, + }, + } + + for _, test := range tests { + suite.T().Run(test.name, func(t *testing.T) { + result, err := CreatePageFromBytes(test.getBytes(t)) + test.checkError(t, err) + test.isNil(t, result) + }) + } +} + +func (suite *DataSupportSuite) TestHasAttachments() { + tests := []struct { + name string + hasAttachment assert.BoolAssertionFunc + getBodyable func(t *testing.T) models.ItemBodyable + }{ + { + name: "Mock w/out attachment", + hasAttachment: assert.False, + getBodyable: func(t *testing.T) models.ItemBodyable { + byteArray := mockconnector.GetMockMessageWithBodyBytes( + "Test", + "This is testing", + "This is testing", + ) + message, err := CreateMessageFromBytes(byteArray) + require.NoError(t, err) + return message.GetBody() + }, + }, + { + name: "Mock w/ inline attachment", + hasAttachment: assert.True, + getBodyable: func(t *testing.T) models.ItemBodyable { + byteArray := mockconnector.GetMessageWithOneDriveAttachment("Test legacy") + message, err := CreateMessageFromBytes(byteArray) + require.NoError(t, err) + return message.GetBody() + }, + }, + { + name: "Edge Case", + hasAttachment: assert.True, + getBodyable: func(t *testing.T) models.ItemBodyable { + //nolint:lll + content := "\r\n
Happy New Year,

In accordance with TPS report guidelines, there have been questions about how to address our activities SharePoint Cover page. Do you believe this is the best picture? 



Let me know if this meets our culture requirements.

Warm Regards,

Dustin
" + body := models.NewItemBody() + body.SetContent(&content) + cat := models.HTML_BODYTYPE + body.SetContentType(&cat) + return body + }, + }, + } + + for _, test := range tests { + suite.T().Run(test.name, func(t *testing.T) { + found := HasAttachments(test.getBodyable(t)) + test.hasAttachment(t, found) + }) + } +}

id~6Km9tl_Uf@_XGg7y+z_XA6 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 aQOXTUfP2T_)`(-MV`oJk44B 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