diff --git a/CHANGELOG.md b/CHANGELOG.md index f542df62a..6751683c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] (beta) +### Added +- Restore commands now accept an optional resource override with the `--to-resource` flag. This allows restores to recreate backup data witthin different mailboxes, sites, and users. + ### Fixed - SharePoint document libraries deleted after the last backup can now be restored. +- Restore requires the protected resource to have access to the service being restored. ## [v0.11.0] (beta) - 2023-07-18 diff --git a/src/cli/flags/restore_config.go b/src/cli/flags/restore_config.go index a2b8c3a86..2db9e429b 100644 --- a/src/cli/flags/restore_config.go +++ b/src/cli/flags/restore_config.go @@ -9,11 +9,13 @@ import ( const ( CollisionsFN = "collisions" DestinationFN = "destination" + ToResourceFN = "to-resource" ) var ( CollisionsFV string DestinationFV string + ToResourceFV string ) // AddRestoreConfigFlags adds the restore config flag set. @@ -25,5 +27,8 @@ func AddRestoreConfigFlags(cmd *cobra.Command) { "Sets the behavior for existing item collisions: "+string(control.Skip)+", "+string(control.Copy)+", or "+string(control.Replace)) fs.StringVar( &DestinationFV, DestinationFN, "", - "Overrides the destination where items get restored; '/' places items into their original location") + "Overrides the folder where items get restored; '/' places items into their original location") + fs.StringVar( + &ToResourceFV, ToResourceFN, "", + "Overrides the protected resource (mailbox, site, etc) where data gets restored") } diff --git a/src/cli/restore/exchange_test.go b/src/cli/restore/exchange_test.go index 0dd022a81..a257fff7a 100644 --- a/src/cli/restore/exchange_test.go +++ b/src/cli/restore/exchange_test.go @@ -84,6 +84,7 @@ func (suite *ExchangeUnitSuite) TestAddExchangeCommands() { "--" + flags.CollisionsFN, testdata.Collisions, "--" + flags.DestinationFN, testdata.Destination, + "--" + flags.ToResourceFN, testdata.ToResource, "--" + flags.AWSAccessKeyFN, testdata.AWSAccessKeyID, "--" + flags.AWSSecretAccessKeyFN, testdata.AWSSecretAccessKey, @@ -125,6 +126,7 @@ func (suite *ExchangeUnitSuite) TestAddExchangeCommands() { assert.Equal(t, testdata.Collisions, opts.RestoreCfg.Collisions) assert.Equal(t, testdata.Destination, opts.RestoreCfg.Destination) + assert.Equal(t, testdata.ToResource, opts.RestoreCfg.ProtectedResource) assert.Equal(t, testdata.AWSAccessKeyID, flags.AWSAccessKeyFV) assert.Equal(t, testdata.AWSSecretAccessKey, flags.AWSSecretAccessKeyFV) diff --git a/src/cli/restore/onedrive_test.go b/src/cli/restore/onedrive_test.go index a21f1191d..8a9fc7a94 100644 --- a/src/cli/restore/onedrive_test.go +++ b/src/cli/restore/onedrive_test.go @@ -70,6 +70,7 @@ func (suite *OneDriveUnitSuite) TestAddOneDriveCommands() { "--" + flags.CollisionsFN, testdata.Collisions, "--" + flags.DestinationFN, testdata.Destination, + "--" + flags.ToResourceFN, testdata.ToResource, "--" + flags.AWSAccessKeyFN, testdata.AWSAccessKeyID, "--" + flags.AWSSecretAccessKeyFN, testdata.AWSSecretAccessKey, @@ -80,6 +81,9 @@ func (suite *OneDriveUnitSuite) TestAddOneDriveCommands() { "--" + flags.AzureClientSecretFN, testdata.AzureClientSecret, "--" + flags.CorsoPassphraseFN, testdata.CorsoPassphrase, + + // bool flags + "--" + flags.RestorePermissionsFN, }) cmd.SetOut(new(bytes.Buffer)) // drop output @@ -99,6 +103,7 @@ func (suite *OneDriveUnitSuite) TestAddOneDriveCommands() { assert.Equal(t, testdata.Collisions, opts.RestoreCfg.Collisions) assert.Equal(t, testdata.Destination, opts.RestoreCfg.Destination) + assert.Equal(t, testdata.ToResource, opts.RestoreCfg.ProtectedResource) assert.Equal(t, testdata.AWSAccessKeyID, flags.AWSAccessKeyFV) assert.Equal(t, testdata.AWSSecretAccessKey, flags.AWSSecretAccessKeyFV) @@ -109,6 +114,7 @@ func (suite *OneDriveUnitSuite) TestAddOneDriveCommands() { assert.Equal(t, testdata.AzureClientSecret, flags.AzureClientSecretFV) assert.Equal(t, testdata.CorsoPassphrase, flags.CorsoPassphraseFV) + assert.True(t, flags.RestorePermissionsFV) }) } } diff --git a/src/cli/restore/sharepoint_test.go b/src/cli/restore/sharepoint_test.go index b4547077f..6a8de8e57 100644 --- a/src/cli/restore/sharepoint_test.go +++ b/src/cli/restore/sharepoint_test.go @@ -75,6 +75,7 @@ func (suite *SharePointUnitSuite) TestAddSharePointCommands() { "--" + flags.CollisionsFN, testdata.Collisions, "--" + flags.DestinationFN, testdata.Destination, + "--" + flags.ToResourceFN, testdata.ToResource, "--" + flags.AWSAccessKeyFN, testdata.AWSAccessKeyID, "--" + flags.AWSSecretAccessKeyFN, testdata.AWSSecretAccessKey, @@ -85,6 +86,9 @@ func (suite *SharePointUnitSuite) TestAddSharePointCommands() { "--" + flags.AzureClientSecretFN, testdata.AzureClientSecret, "--" + flags.CorsoPassphraseFN, testdata.CorsoPassphrase, + + // bool flags + "--" + flags.RestorePermissionsFN, }) cmd.SetOut(new(bytes.Buffer)) // drop output @@ -111,6 +115,7 @@ func (suite *SharePointUnitSuite) TestAddSharePointCommands() { assert.Equal(t, testdata.Collisions, opts.RestoreCfg.Collisions) assert.Equal(t, testdata.Destination, opts.RestoreCfg.Destination) + assert.Equal(t, testdata.ToResource, opts.RestoreCfg.ProtectedResource) assert.Equal(t, testdata.AWSAccessKeyID, flags.AWSAccessKeyFV) assert.Equal(t, testdata.AWSSecretAccessKey, flags.AWSSecretAccessKeyFV) @@ -121,6 +126,9 @@ func (suite *SharePointUnitSuite) TestAddSharePointCommands() { assert.Equal(t, testdata.AzureClientSecret, flags.AzureClientSecretFV) assert.Equal(t, testdata.CorsoPassphrase, flags.CorsoPassphraseFV) + + // bool flags + assert.True(t, flags.RestorePermissionsFV) }) } } diff --git a/src/cli/utils/restore_config.go b/src/cli/utils/restore_config.go index a96449053..6be54f1ab 100644 --- a/src/cli/utils/restore_config.go +++ b/src/cli/utils/restore_config.go @@ -19,6 +19,7 @@ type RestoreCfgOpts struct { // to the default folder name. Defaults to // dttm.HumanReadable. DTTMFormat dttm.TimeFormat + ProtectedResource string RestorePermissions bool Populated flags.PopulatedFlags @@ -29,6 +30,7 @@ func makeRestoreCfgOpts(cmd *cobra.Command) RestoreCfgOpts { Collisions: flags.CollisionsFV, Destination: flags.DestinationFV, DTTMFormat: dttm.HumanReadable, + ProtectedResource: flags.ToResourceFV, RestorePermissions: flags.RestorePermissionsFV, // populated contains the list of flags that appear in the @@ -69,6 +71,7 @@ func MakeRestoreConfig( restoreCfg.Location = opts.Destination } + restoreCfg.ProtectedResource = opts.ProtectedResource restoreCfg.IncludePermissions = opts.RestorePermissions Infof(ctx, "Restoring to folder %s", restoreCfg.Location) diff --git a/src/cli/utils/testdata/flags.go b/src/cli/utils/testdata/flags.go index f97529b57..9f237dc5e 100644 --- a/src/cli/utils/testdata/flags.go +++ b/src/cli/utils/testdata/flags.go @@ -46,6 +46,7 @@ var ( Collisions = "collisions" Destination = "destination" + ToResource = "toResource" RestorePermissions = true AzureClientID = "testAzureClientId" diff --git a/src/internal/m365/onedrive/restore.go b/src/internal/m365/onedrive/restore.go index 5dde04a03..a237a305b 100644 --- a/src/internal/m365/onedrive/restore.go +++ b/src/internal/m365/onedrive/restore.go @@ -51,16 +51,15 @@ func ConsumeRestoreCollections( ctr *count.Bus, ) (*support.ControllerOperationStatus, error) { var ( - restoreMetrics support.CollectionMetrics - el = errs.Local() - caches = NewRestoreCaches(backupDriveIDNames) - protectedResourceID = rcc.ProtectedResource.ID() - fallbackDriveName = "" // onedrive cannot create drives + restoreMetrics support.CollectionMetrics + el = errs.Local() + caches = NewRestoreCaches(backupDriveIDNames) + fallbackDriveName = "" // onedrive cannot create drives ) ctx = clues.Add(ctx, "backup_version", rcc.BackupVersion) - err := caches.Populate(ctx, rh, protectedResourceID) + err := caches.Populate(ctx, rh, rcc.ProtectedResource.ID()) if err != nil { return nil, clues.Wrap(err, "initializing restore caches") } @@ -132,15 +131,14 @@ func RestoreCollection( ctr *count.Bus, ) (support.CollectionMetrics, error) { var ( - metrics = support.CollectionMetrics{} - directory = dc.FullPath() - protectedResourceID = directory.ResourceOwner() - el = errs.Local() - metricsObjects int64 - metricsBytes int64 - metricsSuccess int64 - wg sync.WaitGroup - complete bool + metrics = support.CollectionMetrics{} + directory = dc.FullPath() + el = errs.Local() + metricsObjects int64 + metricsBytes int64 + metricsSuccess int64 + wg sync.WaitGroup + complete bool ) ctx, end := diagnostics.Span(ctx, "gc:drive:restoreCollection", diagnostics.Label("path", directory)) @@ -156,7 +154,7 @@ func RestoreCollection( rh, caches, drivePath, - protectedResourceID, + rcc.ProtectedResource.ID(), fallbackDriveName) if err != nil { return metrics, clues.Wrap(err, "ensuring drive exists") diff --git a/src/internal/m365/sharepoint/restore.go b/src/internal/m365/sharepoint/restore.go index 157f63453..bb894f5ea 100644 --- a/src/internal/m365/sharepoint/restore.go +++ b/src/internal/m365/sharepoint/restore.go @@ -41,14 +41,13 @@ func ConsumeRestoreCollections( ctr *count.Bus, ) (*support.ControllerOperationStatus, error) { var ( - lrh = libraryRestoreHandler{ac} - protectedResourceID = dcs[0].FullPath().ResourceOwner() - restoreMetrics support.CollectionMetrics - caches = onedrive.NewRestoreCaches(backupDriveIDNames) - el = errs.Local() + lrh = libraryRestoreHandler{ac} + restoreMetrics support.CollectionMetrics + caches = onedrive.NewRestoreCaches(backupDriveIDNames) + el = errs.Local() ) - err := caches.Populate(ctx, lrh, protectedResourceID) + err := caches.Populate(ctx, lrh, rcc.ProtectedResource.ID()) if err != nil { return nil, clues.Wrap(err, "initializing restore caches") } @@ -69,7 +68,7 @@ func ConsumeRestoreCollections( metrics support.CollectionMetrics ictx = clues.Add(ctx, "category", category, - "restore_location", rcc.RestoreConfig.Location, + "restore_location", clues.Hide(rcc.RestoreConfig.Location), "resource_owner", clues.Hide(dc.FullPath().ResourceOwner()), "full_path", dc.FullPath()) ) diff --git a/website/docs/setup/restore-options.md b/website/docs/setup/restore-options.md index 9072b2d3f..add019ad6 100644 --- a/website/docs/setup/restore-options.md +++ b/website/docs/setup/restore-options.md @@ -108,3 +108,18 @@ the copy of`reports.txt` is named `reports 1.txt`. Collisions will entirely replace the current version of the item with the backup version. If multiple existing items collide with the backup item, only one of the existing items is replaced. + +## To resource + +The `--to-resource` flag lets you select which resource will receive the restored data. +A resource can be a mailbox, user, or sharepoint site. + +{ + `corso restore onedrive --backup abcd --to-resource adelev@alcion.ai` +} + +### Limitations + +* The resource must exist. Corso will not create new mailboxes, users, or sites. +* The resource must have access to the service being restored. No restore will be +performed for an unlicensed resource. \ No newline at end of file