diff --git a/src/cli/utils/exchange.go b/src/cli/utils/exchange.go index 031ce15b6..53188550f 100644 --- a/src/cli/utils/exchange.go +++ b/src/cli/utils/exchange.go @@ -59,7 +59,7 @@ func MakeExchangeOpts(cmd *cobra.Command) ExchangeOpts { EventStartsBefore: flags.EventStartsBeforeFV, EventSubject: flags.EventSubjectFV, - RestoreCfg: makeRestoreCfgOpts(cmd), + RestoreCfg: makeBaseRestoreCfgOpts(cmd), // populated contains the list of flags that appear in the // command, according to pflags. Use this to differentiate diff --git a/src/cli/utils/groups.go b/src/cli/utils/groups.go index 078c379cc..f6cd2db18 100644 --- a/src/cli/utils/groups.go +++ b/src/cli/utils/groups.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/alcionai/corso/src/cli/flags" + "github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/selectors" ) @@ -67,6 +68,10 @@ func AddGroupsCategories(sel *selectors.GroupsBackup, cats []string) *selectors. } func MakeGroupsOpts(cmd *cobra.Command) GroupsOpts { + restoreCfg := makeBaseRestoreCfgOpts(cmd) + restoreCfg.SubServiceType = path.SharePointService // this is the only possibility as of now + restoreCfg.SubService = append(flags.SiteIDFV, flags.WebURLFV...)[0] // we should always have just one + return GroupsOpts{ Groups: flags.GroupFV, Channels: flags.ChannelFV, @@ -92,7 +97,7 @@ func MakeGroupsOpts(cmd *cobra.Command) GroupsOpts { Page: flags.PageFV, PageFolder: flags.PageFolderFV, - RestoreCfg: makeRestoreCfgOpts(cmd), + RestoreCfg: restoreCfg, ExportCfg: makeExportCfgOpts(cmd), // populated contains the list of flags that appear in the diff --git a/src/cli/utils/onedrive.go b/src/cli/utils/onedrive.go index 20bcad4b7..6735717bb 100644 --- a/src/cli/utils/onedrive.go +++ b/src/cli/utils/onedrive.go @@ -35,7 +35,7 @@ func MakeOneDriveOpts(cmd *cobra.Command) OneDriveOpts { FileModifiedAfter: flags.FileModifiedAfterFV, FileModifiedBefore: flags.FileModifiedBeforeFV, - RestoreCfg: makeRestoreCfgOpts(cmd), + RestoreCfg: makeBaseRestoreCfgOpts(cmd), ExportCfg: makeExportCfgOpts(cmd), // populated contains the list of flags that appear in the diff --git a/src/cli/utils/restore_config.go b/src/cli/utils/restore_config.go index 1f4137446..eae971970 100644 --- a/src/cli/utils/restore_config.go +++ b/src/cli/utils/restore_config.go @@ -11,6 +11,7 @@ import ( . "github.com/alcionai/corso/src/cli/print" "github.com/alcionai/corso/src/internal/common/dttm" "github.com/alcionai/corso/src/pkg/control" + "github.com/alcionai/corso/src/pkg/path" ) type RestoreCfgOpts struct { @@ -23,10 +24,18 @@ type RestoreCfgOpts struct { ProtectedResource string SkipPermissions bool + // SubService is useful when we are trying to restore Groups where + // the user can only restore a single service at any point instead of + // the entire backup. + SubServiceType path.ServiceType + SubService string + Populated flags.PopulatedFlags } -func makeRestoreCfgOpts(cmd *cobra.Command) RestoreCfgOpts { +// makeBaseRestoreCfgOpts fills in all RestoreCfgOpts except for sub +// service details. That is filled by the caller. +func makeBaseRestoreCfgOpts(cmd *cobra.Command) RestoreCfgOpts { return RestoreCfgOpts{ Collisions: flags.CollisionsFV, Destination: flags.DestinationFV, @@ -74,6 +83,9 @@ func MakeRestoreConfig( restoreCfg.ProtectedResource = opts.ProtectedResource restoreCfg.IncludePermissions = !opts.SkipPermissions + restoreCfg.SubService.Type = opts.SubServiceType + restoreCfg.SubService.ID = opts.SubService + Infof(ctx, "Restoring to folder %s", restoreCfg.Location) return restoreCfg diff --git a/src/cli/utils/sharepoint.go b/src/cli/utils/sharepoint.go index d106187a9..623810810 100644 --- a/src/cli/utils/sharepoint.go +++ b/src/cli/utils/sharepoint.go @@ -56,7 +56,7 @@ func MakeSharePointOpts(cmd *cobra.Command) SharePointOpts { Page: flags.PageFV, PageFolder: flags.PageFolderFV, - RestoreCfg: makeRestoreCfgOpts(cmd), + RestoreCfg: makeBaseRestoreCfgOpts(cmd), ExportCfg: makeExportCfgOpts(cmd), // populated contains the list of flags that appear in the diff --git a/src/internal/m365/controller.go b/src/internal/m365/controller.go index 6bcd5e27b..929300d7f 100644 --- a/src/internal/m365/controller.go +++ b/src/internal/m365/controller.go @@ -10,7 +10,6 @@ import ( "github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/m365/graph" - "github.com/alcionai/corso/src/internal/m365/resource" "github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/pkg/account" @@ -81,37 +80,11 @@ func NewController( return nil, clues.Wrap(err, "creating api client").WithClues(ctx) } - var rCli *resourceClient - - // no failure for unknown service. - // In that case we create a controller that doesn't attempt to look up any resource - // data. This case helps avoid unnecessary service calls when the end user is running - // repo init and connect commands via the CLI. All other callers should be expected - // to pass in a known service, or else expect downstream failures. - if pst != path.UnknownService { - rc := resource.UnknownResource - - switch pst { - case path.ExchangeService, path.OneDriveService: - rc = resource.Users - case path.GroupsService: - rc = resource.Groups - case path.SharePointService: - rc = resource.Sites - } - - rCli, err = getResourceClient(rc, ac) - if err != nil { - return nil, clues.Wrap(err, "creating resource client").WithClues(ctx) - } - } - ctrl := Controller{ AC: ac, IDNameLookup: idname.NewCache(nil), credentials: creds, - ownerLookup: rCli, tenant: acct.ID(), wg: &sync.WaitGroup{}, backupDriveIDNames: idname.NewCache(nil), @@ -197,88 +170,6 @@ func (ctrl *Controller) CacheItemInfo(dii details.ItemInfo) { } } -// --------------------------------------------------------------------------- -// Resource Lookup Handling -// --------------------------------------------------------------------------- - -func getResourceClient(rc resource.Category, ac api.Client) (*resourceClient, error) { - switch rc { - case resource.Users: - return &resourceClient{enum: rc, getter: ac.Users()}, nil - case resource.Sites: - return &resourceClient{enum: rc, getter: ac.Sites()}, nil - case resource.Groups: - return &resourceClient{enum: rc, getter: ac.Groups()}, nil - default: - return nil, clues.New("unrecognized owner resource type").With("resource_enum", rc) - } -} - -type resourceClient struct { - enum resource.Category - getter getIDAndNamer -} - -type getIDAndNamer interface { - GetIDAndName( - ctx context.Context, - owner string, - cc api.CallConfig, - ) ( - ownerID string, - ownerName string, - err error, - ) -} - -var _ idname.GetResourceIDAndNamer = &resourceClient{} - -// GetResourceIDAndNameFrom looks up the resource's canonical id and display name. -// If the resource is present in the idNameSwapper, then that interface's id and -// name values are returned. As a fallback, the resource calls the discovery -// api to fetch the user or site using the resource value. This fallback assumes -// that the resource is a well formed ID or display name of appropriate design -// (PrincipalName for users, WebURL for sites). -func (r resourceClient) GetResourceIDAndNameFrom( - ctx context.Context, - owner string, - ins idname.Cacher, -) (idname.Provider, error) { - if ins != nil { - if n, ok := ins.NameOf(owner); ok { - return idname.NewProvider(owner, n), nil - } else if i, ok := ins.IDOf(owner); ok { - return idname.NewProvider(i, owner), nil - } - } - - ctx = clues.Add(ctx, "owner_identifier", owner) - - var ( - id, name string - err error - ) - - id, name, err = r.getter.GetIDAndName(ctx, owner, api.CallConfig{}) - if err != nil { - if graph.IsErrUserNotFound(err) { - return nil, clues.Stack(graph.ErrResourceOwnerNotFound, err) - } - - if graph.IsErrResourceLocked(err) { - return nil, clues.Stack(graph.ErrResourceLocked, err) - } - - return nil, err - } - - if len(id) == 0 || len(name) == 0 { - return nil, clues.Stack(graph.ErrResourceOwnerNotFound) - } - - return idname.NewProvider(id, name), nil -} - // PopulateProtectedResourceIDAndName takes the provided owner identifier and produces // the owner's name and ID from that value. Returns an error if the owner is // not recognized by the current tenant. diff --git a/src/internal/m365/controller_test.go b/src/internal/m365/controller_test.go index 2ae60d2ac..aff65e727 100644 --- a/src/internal/m365/controller_test.go +++ b/src/internal/m365/controller_test.go @@ -57,18 +57,18 @@ func (suite *ControllerUnitSuite) TestPopulateOwnerIDAndNamesFrom() { var ( itn = map[string]string{id: name} nti = map[string]string{name: id} - lookup = &resourceClient{ + lookup = &ResourceClient{ enum: resource.Users, getter: &mock.IDNameGetter{ID: id, Name: name}, } - noLookup = &resourceClient{enum: resource.Users, getter: &mock.IDNameGetter{}} + noLookup = &ResourceClient{enum: resource.Users, getter: &mock.IDNameGetter{}} ) table := []struct { name string protectedResource string ins inMock.Cache - rc *resourceClient + rc *ResourceClient expectID string expectName string expectErr require.ErrorAssertionFunc diff --git a/src/internal/m365/restore.go b/src/internal/m365/restore.go index b7c5f3746..881cb2034 100644 --- a/src/internal/m365/restore.go +++ b/src/internal/m365/restore.go @@ -5,6 +5,7 @@ import ( "github.com/alcionai/clues" + "github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/diagnostics" "github.com/alcionai/corso/src/internal/m365/collection/drive" @@ -16,11 +17,45 @@ import ( "github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/pkg/backup/details" + "github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/count" "github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/path" ) +func (ctrl *Controller) GetRestoreResource( + ctx context.Context, + service path.ServiceType, + rc control.RestoreConfig, + orig idname.Provider, +) (path.ServiceType, idname.Provider, error) { + var ( + svc path.ServiceType + pr idname.Provider + err error + ) + + switch service { + case path.ExchangeService: + svc, pr, err = exchange.GetRestoreResource(ctx, ctrl.AC, rc, ctrl.IDNameLookup, orig) + case path.OneDriveService: + svc, pr, err = onedrive.GetRestoreResource(ctx, ctrl.AC, rc, ctrl.IDNameLookup, orig) + case path.SharePointService: + svc, pr, err = sharepoint.GetRestoreResource(ctx, ctrl.AC, rc, ctrl.IDNameLookup, orig) + case path.GroupsService: + svc, pr, err = groups.GetRestoreResource(ctx, ctrl.AC, rc, ctrl.IDNameLookup, orig) + default: + err = clues.New("unknown service").With("service", service) + } + + if err != nil { + return path.UnknownService, nil, err + } + + ctrl.IDNameLookup = idname.NewCache(map[string]string{pr.ID(): pr.Name()}) + return svc, pr, nil +} + // ConsumeRestoreCollections restores data from the specified collections // into M365 using the GraphAPI. // SideEffect: status is updated at the completion of operation diff --git a/src/internal/m365/service/common/lookup.go b/src/internal/m365/service/common/lookup.go new file mode 100644 index 000000000..afaf62e8c --- /dev/null +++ b/src/internal/m365/service/common/lookup.go @@ -0,0 +1,94 @@ +package common + +import ( + "context" + + "github.com/alcionai/clues" + + "github.com/alcionai/corso/src/internal/common/idname" + "github.com/alcionai/corso/src/internal/m365/graph" + "github.com/alcionai/corso/src/internal/m365/resource" + "github.com/alcionai/corso/src/pkg/services/m365/api" +) + +// --------------------------------------------------------------------------- +// Resource Lookup Handling +// --------------------------------------------------------------------------- + +func GetResourceClient(rc resource.Category, ac api.Client) (*resourceClient, error) { + switch rc { + case resource.Users: + return &resourceClient{enum: rc, getter: ac.Users()}, nil + case resource.Sites: + return &resourceClient{enum: rc, getter: ac.Sites()}, nil + case resource.Groups: + return &resourceClient{enum: rc, getter: ac.Groups()}, nil + default: + return nil, clues.New("unrecognized owner resource type").With("resource_enum", rc) + } +} + +type resourceClient struct { + enum resource.Category + getter getIDAndNamer +} + +type getIDAndNamer interface { + GetIDAndName( + ctx context.Context, + owner string, + cc api.CallConfig, + ) ( + ownerID string, + ownerName string, + err error, + ) +} + +var _ idname.GetResourceIDAndNamer = &resourceClient{} + +// GetResourceIDAndNameFrom looks up the resource's canonical id and display name. +// If the resource is present in the idNameSwapper, then that interface's id and +// name values are returned. As a fallback, the resource calls the discovery +// api to fetch the user or site using the resource value. This fallback assumes +// that the resource is a well formed ID or display name of appropriate design +// (PrincipalName for users, WebURL for sites). +func (r resourceClient) GetResourceIDAndNameFrom( + ctx context.Context, + owner string, + ins idname.Cacher, +) (idname.Provider, error) { + if ins != nil { + if n, ok := ins.NameOf(owner); ok { + return idname.NewProvider(owner, n), nil + } else if i, ok := ins.IDOf(owner); ok { + return idname.NewProvider(i, owner), nil + } + } + + ctx = clues.Add(ctx, "owner_identifier", owner) + + var ( + id, name string + err error + ) + + id, name, err = r.getter.GetIDAndName(ctx, owner, api.CallConfig{}) + if err != nil { + if graph.IsErrUserNotFound(err) { + return nil, clues.Stack(graph.ErrResourceOwnerNotFound, err) + } + + if graph.IsErrResourceLocked(err) { + return nil, clues.Stack(graph.ErrResourceLocked, err) + } + + return nil, err + } + + if len(id) == 0 || len(name) == 0 { + return nil, clues.Stack(graph.ErrResourceOwnerNotFound) + } + + return idname.NewProvider(id, name), nil +} diff --git a/src/internal/m365/service/exchange/restore.go b/src/internal/m365/service/exchange/restore.go index 2298acbfb..ab5b65371 100644 --- a/src/internal/m365/service/exchange/restore.go +++ b/src/internal/m365/service/exchange/restore.go @@ -5,18 +5,46 @@ import ( "github.com/alcionai/clues" + "github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/m365/collection/exchange" "github.com/alcionai/corso/src/internal/m365/graph" + "github.com/alcionai/corso/src/internal/m365/resource" + "github.com/alcionai/corso/src/internal/m365/service/common" "github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/pkg/backup/details" + "github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/count" "github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/services/m365/api" ) +func GetRestoreResource( + ctx context.Context, + ac api.Client, + rc control.RestoreConfig, + ins idname.Cacher, + orig idname.Provider, +) (path.ServiceType, idname.Provider, error) { + if len(rc.ProtectedResource) == 0 { + return path.ExchangeService, orig, nil + } + + res, err := common.GetResourceClient(resource.Users, ac) + if err != nil { + return path.UnknownService, nil, err + } + + pr, err := res.GetResourceIDAndNameFrom(ctx, rc.ProtectedResource, ins) + if err != nil { + return path.UnknownService, nil, clues.Wrap(err, "identifying resource owner") + } + + return path.ExchangeService, pr, nil +} + // ConsumeRestoreCollections restores M365 objects in data.RestoreCollection to MSFT // store through GraphAPI. func ConsumeRestoreCollections( diff --git a/src/internal/m365/service/groups/restore.go b/src/internal/m365/service/groups/restore.go index be99f1c79..d7eb2ef9e 100644 --- a/src/internal/m365/service/groups/restore.go +++ b/src/internal/m365/service/groups/restore.go @@ -13,6 +13,8 @@ import ( "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/m365/collection/drive" "github.com/alcionai/corso/src/internal/m365/graph" + "github.com/alcionai/corso/src/internal/m365/resource" + "github.com/alcionai/corso/src/internal/m365/service/common" "github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/pkg/backup/details" @@ -24,6 +26,39 @@ import ( "github.com/alcionai/corso/src/pkg/services/m365/api" ) +func GetRestoreResource( + ctx context.Context, + ac api.Client, + rc control.RestoreConfig, + ins idname.Cacher, + orig idname.Provider, +) (path.ServiceType, idname.Provider, error) { + // As of now Groups can only restore sites and so we don't need + // any extra logic here, but if/when we start supporting restoring + // other data like messages or chat, we can probably pass that on via + // the restore config and choose the appropriate api client here. + res, err := common.GetResourceClient(resource.Sites, ac) + if err != nil { + return path.UnknownService, nil, err + } + + if len(rc.ProtectedResource) == 0 { + pr, err := res.GetResourceIDAndNameFrom(ctx, rc.SubService.ID, ins) + if err != nil { + return path.UnknownService, nil, clues.Wrap(err, "identifying resource owner") + } + + return path.SharePointService, pr, nil + } + + pr, err := res.GetResourceIDAndNameFrom(ctx, rc.ProtectedResource, ins) + if err != nil { + return path.UnknownService, nil, clues.Wrap(err, "identifying resource owner") + } + + return path.SharePointService, pr, nil +} + // ConsumeRestoreCollections will restore the specified data collections into OneDrive func ConsumeRestoreCollections( ctx context.Context, @@ -37,11 +72,10 @@ func ConsumeRestoreCollections( ctr *count.Bus, ) (*support.ControllerOperationStatus, error) { var ( - restoreMetrics support.CollectionMetrics - caches = drive.NewRestoreCaches(backupDriveIDNames) - lrh = drive.NewSiteRestoreHandler(ac, rcc.Selector.PathService()) - el = errs.Local() - webURLToSiteNames = map[string]string{} + restoreMetrics support.CollectionMetrics + caches = drive.NewRestoreCaches(backupDriveIDNames) + lrh = drive.NewSiteRestoreHandler(ac, rcc.Selector.PathService()) + el = errs.Local() ) // Reorder collections so that the parents directories are created @@ -56,7 +90,6 @@ func ConsumeRestoreCollections( var ( err error - siteName string category = dc.FullPath().Category() metrics support.CollectionMetrics ictx = clues.Add(ctx, @@ -68,34 +101,7 @@ func ConsumeRestoreCollections( switch dc.FullPath().Category() { case path.LibrariesCategory: - siteID := dc.FullPath().Folders()[1] - - webURL, ok := backupSiteIDWebURL.NameOf(siteID) - if !ok { - // This should not happen, but just in case - logger.Ctx(ctx).With("site_id", siteID).Info("site weburl not found, using site id") - } - - siteName, err = getSiteName(ctx, siteID, webURL, ac.Sites(), webURLToSiteNames) - if err != nil { - el.AddRecoverable(ctx, clues.Wrap(err, "getting site"). - With("web_url", webURL, "site_id", siteID)) - } else if len(siteName) == 0 { - // Site was deleted in between and restore and is not - // available anymore. - continue - } - - pr := idname.NewProvider(siteID, siteName) - srcc := inject.RestoreConsumerConfig{ - BackupVersion: rcc.BackupVersion, - Options: rcc.Options, - ProtectedResource: pr, - RestoreConfig: rcc.RestoreConfig, - Selector: rcc.Selector, - } - - err = caches.Populate(ctx, lrh, srcc.ProtectedResource.ID()) + err = caches.Populate(ctx, lrh, rcc.ProtectedResource.ID()) if err != nil { return nil, clues.Wrap(err, "initializing restore caches") } @@ -103,7 +109,7 @@ func ConsumeRestoreCollections( metrics, err = drive.RestoreCollection( ictx, lrh, - srcc, + rcc, dc, caches, deets, diff --git a/src/internal/m365/service/onedrive/restore.go b/src/internal/m365/service/onedrive/restore.go index b71626663..baf1ead2d 100644 --- a/src/internal/m365/service/onedrive/restore.go +++ b/src/internal/m365/service/onedrive/restore.go @@ -10,15 +10,43 @@ import ( "github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/m365/collection/drive" + "github.com/alcionai/corso/src/internal/m365/resource" + "github.com/alcionai/corso/src/internal/m365/service/common" "github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/internal/version" "github.com/alcionai/corso/src/pkg/backup/details" + "github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/count" "github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/path" + "github.com/alcionai/corso/src/pkg/services/m365/api" ) +func GetRestoreResource( + ctx context.Context, + ac api.Client, + rc control.RestoreConfig, + ins idname.Cacher, + orig idname.Provider, +) (path.ServiceType, idname.Provider, error) { + if len(rc.ProtectedResource) == 0 { + return path.OneDriveService, orig, nil + } + + res, err := common.GetResourceClient(resource.Users, ac) + if err != nil { + return path.UnknownService, nil, err + } + + pr, err := res.GetResourceIDAndNameFrom(ctx, rc.ProtectedResource, ins) + if err != nil { + return path.UnknownService, nil, clues.Wrap(err, "identifying resource owner") + } + + return path.OneDriveService, pr, nil +} + // ConsumeRestoreCollections will restore the specified data collections into OneDrive func ConsumeRestoreCollections( ctx context.Context, diff --git a/src/internal/m365/service/sharepoint/restore.go b/src/internal/m365/service/sharepoint/restore.go index e43a07dac..3b367e70a 100644 --- a/src/internal/m365/service/sharepoint/restore.go +++ b/src/internal/m365/service/sharepoint/restore.go @@ -11,6 +11,8 @@ import ( "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/m365/collection/drive" "github.com/alcionai/corso/src/internal/m365/collection/site" + "github.com/alcionai/corso/src/internal/m365/resource" + "github.com/alcionai/corso/src/internal/m365/service/common" "github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/pkg/backup/details" @@ -21,6 +23,30 @@ import ( "github.com/alcionai/corso/src/pkg/services/m365/api" ) +func GetRestoreResource( + ctx context.Context, + ac api.Client, + rc control.RestoreConfig, + ins idname.Cacher, + orig idname.Provider, +) (path.ServiceType, idname.Provider, error) { + if len(rc.ProtectedResource) == 0 { + return path.SharePointService, orig, nil + } + + res, err := common.GetResourceClient(resource.Sites, ac) + if err != nil { + return path.UnknownService, nil, err + } + + pr, err := res.GetResourceIDAndNameFrom(ctx, rc.ProtectedResource, ins) + if err != nil { + return path.UnknownService, nil, clues.Wrap(err, "identifying resource owner") + } + + return path.SharePointService, pr, nil +} + // ConsumeRestoreCollections will restore the specified data collections into OneDrive func ConsumeRestoreCollections( ctx context.Context, diff --git a/src/internal/operations/inject/inject.go b/src/internal/operations/inject/inject.go index 298e224b8..a43c368b8 100644 --- a/src/internal/operations/inject/inject.go +++ b/src/internal/operations/inject/inject.go @@ -45,6 +45,12 @@ type ( } RestoreConsumer interface { + GetRestoreResource( + ctx context.Context, + service path.ServiceType, + rc control.RestoreConfig, + orig idname.Provider, + ) (path.ServiceType, idname.Provider, error) ConsumeRestoreCollections( ctx context.Context, rcc RestoreConsumerConfig, diff --git a/src/internal/operations/restore.go b/src/internal/operations/restore.go index 08df5e133..69d752585 100644 --- a/src/internal/operations/restore.go +++ b/src/internal/operations/restore.go @@ -227,7 +227,11 @@ func (op *RestoreOperation) do( return nil, clues.Wrap(err, "getting backup and details") } - restoreToProtectedResource, err := chooseRestoreResource(ctx, op.rc, op.RestoreCfg, bup.Selector) + restoreService, restoreToProtectedResource, err := op.rc.GetRestoreResource( + ctx, + op.Selectors.PathService(), + op.RestoreCfg, + bup.Selector) if err != nil { return nil, clues.Wrap(err, "getting destination protected resource") } @@ -236,13 +240,14 @@ func (op *RestoreOperation) do( ctx, "backup_protected_resource_id", bup.Selector.ID(), "backup_protected_resource_name", clues.Hide(bup.Selector.Name()), + "restore_service", restoreService, "restore_protected_resource_id", restoreToProtectedResource.ID(), "restore_protected_resource_name", clues.Hide(restoreToProtectedResource.Name())) // Check if the resource has the service enabled to be able to restore. enabled, err := op.rc.IsServiceEnabled( ctx, - op.Selectors.PathService(), + restoreService, restoreToProtectedResource.ID()) if err != nil { return nil, clues.Wrap(err, "verifying service restore is enabled").WithClues(ctx) @@ -350,24 +355,6 @@ func (op *RestoreOperation) persistResults( return op.Errors.Failure() } -func chooseRestoreResource( - ctx context.Context, - pprian inject.PopulateProtectedResourceIDAndNamer, - restoreCfg control.RestoreConfig, - orig idname.Provider, -) (idname.Provider, error) { - if len(restoreCfg.ProtectedResource) == 0 { - return orig, nil - } - - resource, err := pprian.PopulateProtectedResourceIDAndName( - ctx, - restoreCfg.ProtectedResource, - nil) - - return resource, clues.Stack(err).OrNil() -} - // --------------------------------------------------------------------------- // Restorer funcs // --------------------------------------------------------------------------- diff --git a/src/internal/operations/restore_test.go b/src/internal/operations/restore_test.go index a22ab3998..6655ad2c6 100644 --- a/src/internal/operations/restore_test.go +++ b/src/internal/operations/restore_test.go @@ -196,7 +196,7 @@ func (suite *RestoreOpUnitSuite) TestChooseRestoreResource() { ctx, flush := tester.NewContext(t) defer flush() - result, err := chooseRestoreResource(ctx, test.ctrl, test.cfg, test.orig) + svc, result, err := chooseRestoreResource(ctx, test.ctrl, test.cfg, test.orig) test.expectErr(t, err, clues.ToCore(err)) require.NotNil(t, result) assert.Equal(t, test.expectID, result.ID()) diff --git a/src/pkg/control/restore.go b/src/pkg/control/restore.go index 228ffe303..213970ca6 100644 --- a/src/pkg/control/restore.go +++ b/src/pkg/control/restore.go @@ -38,6 +38,11 @@ func IsValidCollisionPolicy(cp CollisionPolicy) bool { const RootLocation = "/" +type RestoreConfigSubService struct { + ID string + Type path.ServiceType +} + // RestoreConfig contains type RestoreConfig struct { // Defines the per-item collision handling policy. @@ -49,6 +54,11 @@ type RestoreConfig struct { // Defaults to empty. ProtectedResource string `json:"protectedResource"` + // SubService specifies the sub-service which we are restoring in + // case of services that are constructed out of multiple services + // like Groups. + SubService RestoreConfigSubService + // Location specifies the container into which the data will be restored. // Only accepts container names, does not accept IDs. // If empty or "/", data will get restored in place, beginning at the root.