Fix restoring to different resource
This commit is contained in:
parent
7e831e6d44
commit
34caca8a5d
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
94
src/internal/m365/service/common/lookup.go
Normal file
94
src/internal/m365/service/common/lookup.go
Normal file
@ -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
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user