Compare commits
12 Commits
main
...
group-alte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04c3d202fa | ||
|
|
4a2dc774b3 | ||
|
|
9a647aa422 | ||
|
|
f64e2fe875 | ||
|
|
ad9c3482ff | ||
|
|
b592ef4749 | ||
|
|
ec06158b37 | ||
|
|
064c1206a3 | ||
|
|
04d3e9019b | ||
|
|
34caca8a5d | ||
|
|
7e831e6d44 | ||
|
|
b24f533327 |
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
- Skips graph calls for expired item download URLs.
|
- Skips graph calls for expired item download URLs.
|
||||||
- Export operation now shows the stats at the end of the run
|
- Export operation now shows the stats at the end of the run
|
||||||
|
- Group backup data can now be restored to a different resource than the one it was backed up from
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Catch and report cases where a protected resource is locked out of access. SDK consumers have a new errs sentinel that allows them to check for this case.
|
- Catch and report cases where a protected resource is locked out of access. SDK consumers have a new errs sentinel that allows them to check for this case.
|
||||||
|
|||||||
@ -19,7 +19,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// AddRestoreConfigFlags adds the restore config flag set.
|
// AddRestoreConfigFlags adds the restore config flag set.
|
||||||
func AddRestoreConfigFlags(cmd *cobra.Command, canRestoreToAlternate bool) {
|
func AddRestoreConfigFlags(cmd *cobra.Command) {
|
||||||
fs := cmd.Flags()
|
fs := cmd.Flags()
|
||||||
fs.StringVar(
|
fs.StringVar(
|
||||||
&CollisionsFV, CollisionsFN, string(control.Skip),
|
&CollisionsFV, CollisionsFN, string(control.Skip),
|
||||||
@ -29,9 +29,7 @@ func AddRestoreConfigFlags(cmd *cobra.Command, canRestoreToAlternate bool) {
|
|||||||
&DestinationFV, DestinationFN, "",
|
&DestinationFV, DestinationFN, "",
|
||||||
"Overrides the folder where items get restored; '/' places items into their original location")
|
"Overrides the folder where items get restored; '/' places items into their original location")
|
||||||
|
|
||||||
if canRestoreToAlternate {
|
fs.StringVar(
|
||||||
fs.StringVar(
|
&ToResourceFV, ToResourceFN, "",
|
||||||
&ToResourceFV, ToResourceFN, "",
|
"Overrides the protected resource (mailbox, site, user, etc) where data gets restored")
|
||||||
"Overrides the protected resource (mailbox, site, user, etc) where data gets restored")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,7 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command {
|
|||||||
|
|
||||||
flags.AddBackupIDFlag(c, true)
|
flags.AddBackupIDFlag(c, true)
|
||||||
flags.AddExchangeDetailsAndRestoreFlags(c)
|
flags.AddExchangeDetailsAndRestoreFlags(c)
|
||||||
flags.AddRestoreConfigFlags(c, true)
|
flags.AddRestoreConfigFlags(c)
|
||||||
flags.AddFailFastFlag(c)
|
flags.AddFailFastFlag(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,7 @@ func addGroupsCommands(cmd *cobra.Command) *cobra.Command {
|
|||||||
flags.AddSiteIDFlag(c, false)
|
flags.AddSiteIDFlag(c, false)
|
||||||
flags.AddNoPermissionsFlag(c)
|
flags.AddNoPermissionsFlag(c)
|
||||||
flags.AddSharePointDetailsAndRestoreFlags(c)
|
flags.AddSharePointDetailsAndRestoreFlags(c)
|
||||||
flags.AddRestoreConfigFlags(c, false)
|
flags.AddRestoreConfigFlags(c)
|
||||||
flags.AddFailFastFlag(c)
|
flags.AddFailFastFlag(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,7 @@ func (suite *GroupsUnitSuite) TestAddGroupsCommands() {
|
|||||||
"--" + flags.RunModeFN, flags.RunModeFlagTest,
|
"--" + flags.RunModeFN, flags.RunModeFlagTest,
|
||||||
"--" + flags.BackupFN, flagsTD.BackupInput,
|
"--" + flags.BackupFN, flagsTD.BackupInput,
|
||||||
"--" + flags.SiteFN, flagsTD.SiteInput,
|
"--" + flags.SiteFN, flagsTD.SiteInput,
|
||||||
|
"--" + flags.SiteIDFN, flagsTD.SiteInput,
|
||||||
"--" + flags.LibraryFN, flagsTD.LibraryInput,
|
"--" + flags.LibraryFN, flagsTD.LibraryInput,
|
||||||
"--" + flags.FileFN, flagsTD.FlgInputs(flagsTD.FileNameInput),
|
"--" + flags.FileFN, flagsTD.FlgInputs(flagsTD.FileNameInput),
|
||||||
"--" + flags.FolderFN, flagsTD.FlgInputs(flagsTD.FolderPathInput),
|
"--" + flags.FolderFN, flagsTD.FlgInputs(flagsTD.FolderPathInput),
|
||||||
@ -66,7 +67,7 @@ func (suite *GroupsUnitSuite) TestAddGroupsCommands() {
|
|||||||
"--" + flags.PageFolderFN, flagsTD.FlgInputs(flagsTD.PageFolderInput),
|
"--" + flags.PageFolderFN, flagsTD.FlgInputs(flagsTD.PageFolderInput),
|
||||||
"--" + flags.CollisionsFN, flagsTD.Collisions,
|
"--" + flags.CollisionsFN, flagsTD.Collisions,
|
||||||
"--" + flags.DestinationFN, flagsTD.Destination,
|
"--" + flags.DestinationFN, flagsTD.Destination,
|
||||||
// "--" + flags.ToResourceFN, flagsTD.ToResource,
|
"--" + flags.ToResourceFN, flagsTD.ToResource,
|
||||||
"--" + flags.NoPermissionsFN,
|
"--" + flags.NoPermissionsFN,
|
||||||
},
|
},
|
||||||
flagsTD.PreparedProviderFlags(),
|
flagsTD.PreparedProviderFlags(),
|
||||||
@ -83,6 +84,8 @@ func (suite *GroupsUnitSuite) TestAddGroupsCommands() {
|
|||||||
opts := utils.MakeGroupsOpts(cmd)
|
opts := utils.MakeGroupsOpts(cmd)
|
||||||
|
|
||||||
assert.Equal(t, flagsTD.BackupInput, flags.BackupIDFV)
|
assert.Equal(t, flagsTD.BackupInput, flags.BackupIDFV)
|
||||||
|
assert.Equal(t, flagsTD.SiteInput, opts.SiteID[0])
|
||||||
|
assert.Equal(t, flagsTD.SiteInput, opts.WebURL[0])
|
||||||
assert.Equal(t, flagsTD.LibraryInput, opts.Library)
|
assert.Equal(t, flagsTD.LibraryInput, opts.Library)
|
||||||
assert.ElementsMatch(t, flagsTD.FileNameInput, opts.FileName)
|
assert.ElementsMatch(t, flagsTD.FileNameInput, opts.FileName)
|
||||||
assert.ElementsMatch(t, flagsTD.FolderPathInput, opts.FolderPath)
|
assert.ElementsMatch(t, flagsTD.FolderPathInput, opts.FolderPath)
|
||||||
@ -92,7 +95,7 @@ func (suite *GroupsUnitSuite) TestAddGroupsCommands() {
|
|||||||
assert.Equal(t, flagsTD.FileModifiedBeforeInput, opts.FileModifiedBefore)
|
assert.Equal(t, flagsTD.FileModifiedBeforeInput, opts.FileModifiedBefore)
|
||||||
assert.Equal(t, flagsTD.Collisions, opts.RestoreCfg.Collisions)
|
assert.Equal(t, flagsTD.Collisions, opts.RestoreCfg.Collisions)
|
||||||
assert.Equal(t, flagsTD.Destination, opts.RestoreCfg.Destination)
|
assert.Equal(t, flagsTD.Destination, opts.RestoreCfg.Destination)
|
||||||
// assert.Equal(t, flagsTD.ToResource, opts.RestoreCfg.ProtectedResource)
|
assert.Equal(t, flagsTD.ToResource, opts.RestoreCfg.ProtectedResource)
|
||||||
assert.True(t, flags.NoPermissionsFV)
|
assert.True(t, flags.NoPermissionsFV)
|
||||||
flagsTD.AssertProviderFlags(t, cmd)
|
flagsTD.AssertProviderFlags(t, cmd)
|
||||||
flagsTD.AssertStorageFlags(t, cmd)
|
flagsTD.AssertStorageFlags(t, cmd)
|
||||||
|
|||||||
@ -29,7 +29,7 @@ func addOneDriveCommands(cmd *cobra.Command) *cobra.Command {
|
|||||||
flags.AddBackupIDFlag(c, true)
|
flags.AddBackupIDFlag(c, true)
|
||||||
flags.AddOneDriveDetailsAndRestoreFlags(c)
|
flags.AddOneDriveDetailsAndRestoreFlags(c)
|
||||||
flags.AddNoPermissionsFlag(c)
|
flags.AddNoPermissionsFlag(c)
|
||||||
flags.AddRestoreConfigFlags(c, true)
|
flags.AddRestoreConfigFlags(c)
|
||||||
flags.AddFailFastFlag(c)
|
flags.AddFailFastFlag(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,7 @@ func addSharePointCommands(cmd *cobra.Command) *cobra.Command {
|
|||||||
flags.AddBackupIDFlag(c, true)
|
flags.AddBackupIDFlag(c, true)
|
||||||
flags.AddSharePointDetailsAndRestoreFlags(c)
|
flags.AddSharePointDetailsAndRestoreFlags(c)
|
||||||
flags.AddNoPermissionsFlag(c)
|
flags.AddNoPermissionsFlag(c)
|
||||||
flags.AddRestoreConfigFlags(c, true)
|
flags.AddRestoreConfigFlags(c)
|
||||||
flags.AddFailFastFlag(c)
|
flags.AddFailFastFlag(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -59,7 +59,7 @@ func MakeExchangeOpts(cmd *cobra.Command) ExchangeOpts {
|
|||||||
EventStartsBefore: flags.EventStartsBeforeFV,
|
EventStartsBefore: flags.EventStartsBeforeFV,
|
||||||
EventSubject: flags.EventSubjectFV,
|
EventSubject: flags.EventSubjectFV,
|
||||||
|
|
||||||
RestoreCfg: makeRestoreCfgOpts(cmd),
|
RestoreCfg: makeBaseRestoreCfgOpts(cmd),
|
||||||
|
|
||||||
// populated contains the list of flags that appear in the
|
// populated contains the list of flags that appear in the
|
||||||
// command, according to pflags. Use this to differentiate
|
// command, according to pflags. Use this to differentiate
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/cli/flags"
|
"github.com/alcionai/corso/src/cli/flags"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
"github.com/alcionai/corso/src/pkg/selectors"
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -67,6 +68,15 @@ func AddGroupsCategories(sel *selectors.GroupsBackup, cats []string) *selectors.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func MakeGroupsOpts(cmd *cobra.Command) GroupsOpts {
|
func MakeGroupsOpts(cmd *cobra.Command) GroupsOpts {
|
||||||
|
restoreCfg := makeBaseRestoreCfgOpts(cmd)
|
||||||
|
|
||||||
|
sites := append(flags.SiteIDFV, flags.WebURLFV...)
|
||||||
|
if len(sites) > 0 {
|
||||||
|
// There will either be zero or one site, this is ensured by ValidateGroupsRestoreFlags
|
||||||
|
restoreCfg.SubServiceType = path.SharePointService
|
||||||
|
restoreCfg.SubService = sites[0]
|
||||||
|
}
|
||||||
|
|
||||||
return GroupsOpts{
|
return GroupsOpts{
|
||||||
Groups: flags.GroupFV,
|
Groups: flags.GroupFV,
|
||||||
Channels: flags.ChannelFV,
|
Channels: flags.ChannelFV,
|
||||||
@ -92,7 +102,7 @@ func MakeGroupsOpts(cmd *cobra.Command) GroupsOpts {
|
|||||||
Page: flags.PageFV,
|
Page: flags.PageFV,
|
||||||
PageFolder: flags.PageFolderFV,
|
PageFolder: flags.PageFolderFV,
|
||||||
|
|
||||||
RestoreCfg: makeRestoreCfgOpts(cmd),
|
RestoreCfg: restoreCfg,
|
||||||
ExportCfg: makeExportCfgOpts(cmd),
|
ExportCfg: makeExportCfgOpts(cmd),
|
||||||
|
|
||||||
// populated contains the list of flags that appear in the
|
// populated contains the list of flags that appear in the
|
||||||
@ -108,15 +118,15 @@ func ValidateGroupsRestoreFlags(backupID string, opts GroupsOpts, isRestore bool
|
|||||||
return clues.New("a backup ID is required")
|
return clues.New("a backup ID is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// The user has to explicitly specify which resource to restore. In
|
// When restoring the user has to select a single site to
|
||||||
// this case, since we can only restore sites, the user is supposed
|
// restore. During exports or other operations, they can either select
|
||||||
// to specify which site to restore.
|
// the entire backup or just a single site.
|
||||||
if isRestore {
|
if isRestore && len(opts.WebURL)+len(opts.SiteID) == 0 {
|
||||||
if len(opts.WebURL)+len(opts.SiteID) == 0 {
|
return clues.New("web URL of the site to restore is required. Use --" + flags.SiteFN + " to provide one.")
|
||||||
return clues.New("web URL of the site to restore is required. Use --" + flags.SiteFN + " to provide one.")
|
}
|
||||||
} else if len(opts.WebURL)+len(opts.SiteID) > 1 {
|
|
||||||
return clues.New("only a single site can be selected for restore")
|
if len(opts.WebURL)+len(opts.SiteID) > 1 {
|
||||||
}
|
return clues.New("only a single site can be selected when processing sub resources")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := opts.Populated[flags.FileCreatedAfterFN]; ok && !IsValidTimeFormat(opts.FileCreatedAfter) {
|
if _, ok := opts.Populated[flags.FileCreatedAfterFN]; ok && !IsValidTimeFormat(opts.FileCreatedAfter) {
|
||||||
|
|||||||
@ -35,7 +35,7 @@ func MakeOneDriveOpts(cmd *cobra.Command) OneDriveOpts {
|
|||||||
FileModifiedAfter: flags.FileModifiedAfterFV,
|
FileModifiedAfter: flags.FileModifiedAfterFV,
|
||||||
FileModifiedBefore: flags.FileModifiedBeforeFV,
|
FileModifiedBefore: flags.FileModifiedBeforeFV,
|
||||||
|
|
||||||
RestoreCfg: makeRestoreCfgOpts(cmd),
|
RestoreCfg: makeBaseRestoreCfgOpts(cmd),
|
||||||
ExportCfg: makeExportCfgOpts(cmd),
|
ExportCfg: makeExportCfgOpts(cmd),
|
||||||
|
|
||||||
// populated contains the list of flags that appear in the
|
// 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/cli/print"
|
||||||
"github.com/alcionai/corso/src/internal/common/dttm"
|
"github.com/alcionai/corso/src/internal/common/dttm"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RestoreCfgOpts struct {
|
type RestoreCfgOpts struct {
|
||||||
@ -23,10 +24,18 @@ type RestoreCfgOpts struct {
|
|||||||
ProtectedResource string
|
ProtectedResource string
|
||||||
SkipPermissions bool
|
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
|
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{
|
return RestoreCfgOpts{
|
||||||
Collisions: flags.CollisionsFV,
|
Collisions: flags.CollisionsFV,
|
||||||
Destination: flags.DestinationFV,
|
Destination: flags.DestinationFV,
|
||||||
@ -74,6 +83,9 @@ func MakeRestoreConfig(
|
|||||||
restoreCfg.ProtectedResource = opts.ProtectedResource
|
restoreCfg.ProtectedResource = opts.ProtectedResource
|
||||||
restoreCfg.IncludePermissions = !opts.SkipPermissions
|
restoreCfg.IncludePermissions = !opts.SkipPermissions
|
||||||
|
|
||||||
|
restoreCfg.SubService.Type = opts.SubServiceType
|
||||||
|
restoreCfg.SubService.ID = opts.SubService
|
||||||
|
|
||||||
Infof(ctx, "Restoring to folder %s", restoreCfg.Location)
|
Infof(ctx, "Restoring to folder %s", restoreCfg.Location)
|
||||||
|
|
||||||
return restoreCfg
|
return restoreCfg
|
||||||
|
|||||||
@ -56,7 +56,7 @@ func MakeSharePointOpts(cmd *cobra.Command) SharePointOpts {
|
|||||||
Page: flags.PageFV,
|
Page: flags.PageFV,
|
||||||
PageFolder: flags.PageFolderFV,
|
PageFolder: flags.PageFolderFV,
|
||||||
|
|
||||||
RestoreCfg: makeRestoreCfgOpts(cmd),
|
RestoreCfg: makeBaseRestoreCfgOpts(cmd),
|
||||||
ExportCfg: makeExportCfgOpts(cmd),
|
ExportCfg: makeExportCfgOpts(cmd),
|
||||||
|
|
||||||
// populated contains the list of flags that appear in the
|
// populated contains the list of flags that appear in the
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
"github.com/alcionai/corso/src/internal/m365/graph"
|
"github.com/alcionai/corso/src/internal/m365/graph"
|
||||||
"github.com/alcionai/corso/src/internal/m365/resource"
|
"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/m365/support"
|
||||||
"github.com/alcionai/corso/src/internal/operations/inject"
|
"github.com/alcionai/corso/src/internal/operations/inject"
|
||||||
"github.com/alcionai/corso/src/pkg/account"
|
"github.com/alcionai/corso/src/pkg/account"
|
||||||
@ -81,7 +82,7 @@ func NewController(
|
|||||||
return nil, clues.Wrap(err, "creating api client").WithClues(ctx)
|
return nil, clues.Wrap(err, "creating api client").WithClues(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
var rCli *resourceClient
|
var rCli *common.ResourceClient
|
||||||
|
|
||||||
// no failure for unknown service.
|
// no failure for unknown service.
|
||||||
// In that case we create a controller that doesn't attempt to look up any resource
|
// In that case we create a controller that doesn't attempt to look up any resource
|
||||||
@ -100,7 +101,7 @@ func NewController(
|
|||||||
rc = resource.Sites
|
rc = resource.Sites
|
||||||
}
|
}
|
||||||
|
|
||||||
rCli, err = getResourceClient(rc, ac)
|
rCli, err = common.GetResourceClient(rc, ac)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, clues.Wrap(err, "creating resource client").WithClues(ctx)
|
return nil, clues.Wrap(err, "creating resource client").WithClues(ctx)
|
||||||
}
|
}
|
||||||
@ -197,88 +198,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
|
// PopulateProtectedResourceIDAndName takes the provided owner identifier and produces
|
||||||
// the owner's name and ID from that value. Returns an error if the owner is
|
// the owner's name and ID from that value. Returns an error if the owner is
|
||||||
// not recognized by the current tenant.
|
// not recognized by the current tenant.
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
"github.com/alcionai/corso/src/internal/m365/mock"
|
"github.com/alcionai/corso/src/internal/m365/mock"
|
||||||
"github.com/alcionai/corso/src/internal/m365/resource"
|
"github.com/alcionai/corso/src/internal/m365/resource"
|
||||||
|
"github.com/alcionai/corso/src/internal/m365/service/common"
|
||||||
exchMock "github.com/alcionai/corso/src/internal/m365/service/exchange/mock"
|
exchMock "github.com/alcionai/corso/src/internal/m365/service/exchange/mock"
|
||||||
"github.com/alcionai/corso/src/internal/m365/stub"
|
"github.com/alcionai/corso/src/internal/m365/stub"
|
||||||
"github.com/alcionai/corso/src/internal/m365/support"
|
"github.com/alcionai/corso/src/internal/m365/support"
|
||||||
@ -57,18 +58,18 @@ func (suite *ControllerUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
|
|||||||
var (
|
var (
|
||||||
itn = map[string]string{id: name}
|
itn = map[string]string{id: name}
|
||||||
nti = map[string]string{name: id}
|
nti = map[string]string{name: id}
|
||||||
lookup = &resourceClient{
|
lookup = &common.ResourceClient{
|
||||||
enum: resource.Users,
|
Enum: resource.Users,
|
||||||
getter: &mock.IDNameGetter{ID: id, Name: name},
|
Getter: &mock.IDNameGetter{ID: id, Name: name},
|
||||||
}
|
}
|
||||||
noLookup = &resourceClient{enum: resource.Users, getter: &mock.IDNameGetter{}}
|
noLookup = &common.ResourceClient{Enum: resource.Users, Getter: &mock.IDNameGetter{}}
|
||||||
)
|
)
|
||||||
|
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
protectedResource string
|
protectedResource string
|
||||||
ins inMock.Cache
|
ins inMock.Cache
|
||||||
rc *resourceClient
|
rc *common.ResourceClient
|
||||||
expectID string
|
expectID string
|
||||||
expectName string
|
expectName string
|
||||||
expectErr require.ErrorAssertionFunc
|
expectErr require.ErrorAssertionFunc
|
||||||
|
|||||||
@ -50,6 +50,15 @@ func (ctrl Controller) ProduceBackupCollections(
|
|||||||
return ctrl.Collections, ctrl.Exclude, ctrl.Err == nil, ctrl.Err
|
return ctrl.Collections, ctrl.Exclude, ctrl.Err == nil, ctrl.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctrl Controller) GetRestoreResource(
|
||||||
|
ctx context.Context,
|
||||||
|
service path.ServiceType,
|
||||||
|
rc control.RestoreConfig,
|
||||||
|
orig idname.Provider,
|
||||||
|
) (path.ServiceType, idname.Provider, error) {
|
||||||
|
return service, orig, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ctrl *Controller) GetMetadataPaths(
|
func (ctrl *Controller) GetMetadataPaths(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
r kinject.RestoreProducer,
|
r kinject.RestoreProducer,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/alcionai/clues"
|
"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/data"
|
||||||
"github.com/alcionai/corso/src/internal/diagnostics"
|
"github.com/alcionai/corso/src/internal/diagnostics"
|
||||||
"github.com/alcionai/corso/src/internal/m365/collection/drive"
|
"github.com/alcionai/corso/src/internal/m365/collection/drive"
|
||||||
@ -16,11 +17,46 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/m365/support"
|
"github.com/alcionai/corso/src/internal/m365/support"
|
||||||
"github.com/alcionai/corso/src/internal/operations/inject"
|
"github.com/alcionai/corso/src/internal/operations/inject"
|
||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
"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/count"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"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
|
// ConsumeRestoreCollections restores data from the specified collections
|
||||||
// into M365 using the GraphAPI.
|
// into M365 using the GraphAPI.
|
||||||
// SideEffect: status is updated at the completion of operation
|
// 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/clues"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/idname"
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
"github.com/alcionai/corso/src/internal/m365/collection/exchange"
|
"github.com/alcionai/corso/src/internal/m365/collection/exchange"
|
||||||
"github.com/alcionai/corso/src/internal/m365/graph"
|
"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/m365/support"
|
||||||
"github.com/alcionai/corso/src/internal/operations/inject"
|
"github.com/alcionai/corso/src/internal/operations/inject"
|
||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
"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/count"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
"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
|
// ConsumeRestoreCollections restores M365 objects in data.RestoreCollection to MSFT
|
||||||
// store through GraphAPI.
|
// store through GraphAPI.
|
||||||
func ConsumeRestoreCollections(
|
func ConsumeRestoreCollections(
|
||||||
|
|||||||
84
src/internal/m365/service/exchange/restore_test.go
Normal file
84
src/internal/m365/service/exchange/restore_test.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package exchange
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/dttm"
|
||||||
|
"github.com/alcionai/corso/src/internal/common/idname"
|
||||||
|
"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/services/m365/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExchangeRestoreUnitSuite struct {
|
||||||
|
tester.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExchangeRestoreUnitSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &ExchangeRestoreUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ExchangeRestoreUnitSuite) TestGetRestoreResource() {
|
||||||
|
var (
|
||||||
|
id = "id"
|
||||||
|
name = "name"
|
||||||
|
cfgWithPR = control.DefaultRestoreConfig(dttm.HumanReadable)
|
||||||
|
)
|
||||||
|
|
||||||
|
cfgWithPR.ProtectedResource = id
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
cfg control.RestoreConfig
|
||||||
|
orig idname.Provider
|
||||||
|
cache map[string]string
|
||||||
|
expectErr assert.ErrorAssertionFunc
|
||||||
|
expectProvider assert.ValueAssertionFunc
|
||||||
|
expectID string
|
||||||
|
expectName string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "use original",
|
||||||
|
cfg: control.DefaultRestoreConfig(dttm.HumanReadable),
|
||||||
|
orig: idname.NewProvider("oid", "oname"),
|
||||||
|
expectErr: assert.NoError,
|
||||||
|
expectID: "oid",
|
||||||
|
expectName: "oname",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "use new",
|
||||||
|
cfg: cfgWithPR,
|
||||||
|
orig: idname.NewProvider("oid", "oname"),
|
||||||
|
cache: map[string]string{id: name},
|
||||||
|
expectErr: assert.NoError,
|
||||||
|
expectID: id,
|
||||||
|
expectName: name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
svc, result, err := GetRestoreResource(
|
||||||
|
ctx,
|
||||||
|
api.Client{},
|
||||||
|
test.cfg,
|
||||||
|
idname.NewCache(test.cache),
|
||||||
|
test.orig)
|
||||||
|
test.expectErr(t, err, clues.ToCore(err))
|
||||||
|
require.NotNil(t, result)
|
||||||
|
assert.Equal(t, path.ExchangeService, svc)
|
||||||
|
assert.Equal(t, test.expectID, result.ID())
|
||||||
|
assert.Equal(t, test.expectName, result.Name())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,6 +13,8 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
"github.com/alcionai/corso/src/internal/m365/collection/drive"
|
"github.com/alcionai/corso/src/internal/m365/collection/drive"
|
||||||
"github.com/alcionai/corso/src/internal/m365/graph"
|
"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/m365/support"
|
||||||
"github.com/alcionai/corso/src/internal/operations/inject"
|
"github.com/alcionai/corso/src/internal/operations/inject"
|
||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
@ -24,6 +26,43 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
"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 {
|
||||||
|
if len(rc.SubService.ID) == 0 {
|
||||||
|
return path.UnknownService, nil, errors.New("missing subservice id for restore")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// ConsumeRestoreCollections will restore the specified data collections into OneDrive
|
||||||
func ConsumeRestoreCollections(
|
func ConsumeRestoreCollections(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
@ -37,11 +76,10 @@ func ConsumeRestoreCollections(
|
|||||||
ctr *count.Bus,
|
ctr *count.Bus,
|
||||||
) (*support.ControllerOperationStatus, error) {
|
) (*support.ControllerOperationStatus, error) {
|
||||||
var (
|
var (
|
||||||
restoreMetrics support.CollectionMetrics
|
restoreMetrics support.CollectionMetrics
|
||||||
caches = drive.NewRestoreCaches(backupDriveIDNames)
|
caches = drive.NewRestoreCaches(backupDriveIDNames)
|
||||||
lrh = drive.NewSiteRestoreHandler(ac, rcc.Selector.PathService())
|
lrh = drive.NewSiteRestoreHandler(ac, rcc.Selector.PathService())
|
||||||
el = errs.Local()
|
el = errs.Local()
|
||||||
webURLToSiteNames = map[string]string{}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reorder collections so that the parents directories are created
|
// Reorder collections so that the parents directories are created
|
||||||
@ -56,7 +94,6 @@ func ConsumeRestoreCollections(
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
siteName string
|
|
||||||
category = dc.FullPath().Category()
|
category = dc.FullPath().Category()
|
||||||
metrics support.CollectionMetrics
|
metrics support.CollectionMetrics
|
||||||
ictx = clues.Add(ctx,
|
ictx = clues.Add(ctx,
|
||||||
@ -68,34 +105,7 @@ func ConsumeRestoreCollections(
|
|||||||
|
|
||||||
switch dc.FullPath().Category() {
|
switch dc.FullPath().Category() {
|
||||||
case path.LibrariesCategory:
|
case path.LibrariesCategory:
|
||||||
siteID := dc.FullPath().Folders()[1]
|
err = caches.Populate(ctx, lrh, rcc.ProtectedResource.ID())
|
||||||
|
|
||||||
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())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, clues.Wrap(err, "initializing restore caches")
|
return nil, clues.Wrap(err, "initializing restore caches")
|
||||||
}
|
}
|
||||||
@ -103,7 +113,7 @@ func ConsumeRestoreCollections(
|
|||||||
metrics, err = drive.RestoreCollection(
|
metrics, err = drive.RestoreCollection(
|
||||||
ictx,
|
ictx,
|
||||||
lrh,
|
lrh,
|
||||||
srcc,
|
rcc,
|
||||||
dc,
|
dc,
|
||||||
caches,
|
caches,
|
||||||
deets,
|
deets,
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/dttm"
|
||||||
"github.com/alcionai/corso/src/internal/common/idname"
|
"github.com/alcionai/corso/src/internal/common/idname"
|
||||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
@ -65,6 +66,73 @@ func (suite *GroupsUnitSuite) TestConsumeRestoreCollections_noErrorOnGroups() {
|
|||||||
assert.NoError(t, err, "Groups Channels restore")
|
assert.NoError(t, err, "Groups Channels restore")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *GroupsUnitSuite) TestGetRestoreResource() {
|
||||||
|
var (
|
||||||
|
sid = "site-id"
|
||||||
|
sname = "site-name"
|
||||||
|
nsid = "new-site-id"
|
||||||
|
nsname = "new-site-name"
|
||||||
|
cfgWithoutPR = control.DefaultRestoreConfig(dttm.HumanReadable)
|
||||||
|
cfgWithPR = control.DefaultRestoreConfig(dttm.HumanReadable)
|
||||||
|
)
|
||||||
|
|
||||||
|
cfgWithoutPR.SubService.Type = path.SharePointService
|
||||||
|
cfgWithoutPR.SubService.ID = sid
|
||||||
|
cfgWithPR.ProtectedResource = nsid
|
||||||
|
cfgWithPR.SubService.Type = path.SharePointService
|
||||||
|
cfgWithPR.SubService.ID = sid
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
cfg control.RestoreConfig
|
||||||
|
orig idname.Provider
|
||||||
|
cache map[string]string
|
||||||
|
expectErr assert.ErrorAssertionFunc
|
||||||
|
expectProvider assert.ValueAssertionFunc
|
||||||
|
expectID string
|
||||||
|
expectName string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "use original",
|
||||||
|
cfg: cfgWithoutPR,
|
||||||
|
orig: idname.NewProvider("oid", "oname"),
|
||||||
|
cache: map[string]string{sid: sname},
|
||||||
|
expectErr: assert.NoError,
|
||||||
|
expectID: sid,
|
||||||
|
expectName: sname,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "use new",
|
||||||
|
cfg: cfgWithPR,
|
||||||
|
orig: idname.NewProvider("oid", "oname"),
|
||||||
|
cache: map[string]string{nsid: nsname},
|
||||||
|
expectErr: assert.NoError,
|
||||||
|
expectID: nsid,
|
||||||
|
expectName: nsname,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
svc, result, err := GetRestoreResource(
|
||||||
|
ctx,
|
||||||
|
api.Client{},
|
||||||
|
test.cfg,
|
||||||
|
idname.NewCache(test.cache),
|
||||||
|
test.orig)
|
||||||
|
test.expectErr(t, err, clues.ToCore(err))
|
||||||
|
require.NotNil(t, result)
|
||||||
|
assert.Equal(t, path.SharePointService, svc)
|
||||||
|
assert.Equal(t, test.expectID, result.ID())
|
||||||
|
assert.Equal(t, test.expectName, result.Name())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type groupsIntegrationSuite struct {
|
type groupsIntegrationSuite struct {
|
||||||
tester.Suite
|
tester.Suite
|
||||||
resource string
|
resource string
|
||||||
|
|||||||
@ -10,15 +10,43 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/common/idname"
|
"github.com/alcionai/corso/src/internal/common/idname"
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
"github.com/alcionai/corso/src/internal/m365/collection/drive"
|
"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/m365/support"
|
||||||
"github.com/alcionai/corso/src/internal/operations/inject"
|
"github.com/alcionai/corso/src/internal/operations/inject"
|
||||||
"github.com/alcionai/corso/src/internal/version"
|
"github.com/alcionai/corso/src/internal/version"
|
||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
"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/count"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"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
|
// ConsumeRestoreCollections will restore the specified data collections into OneDrive
|
||||||
func ConsumeRestoreCollections(
|
func ConsumeRestoreCollections(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|||||||
@ -8,9 +8,13 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/dttm"
|
||||||
|
"github.com/alcionai/corso/src/internal/common/idname"
|
||||||
"github.com/alcionai/corso/src/internal/tester"
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
"github.com/alcionai/corso/src/internal/version"
|
"github.com/alcionai/corso/src/internal/version"
|
||||||
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RestoreUnitSuite struct {
|
type RestoreUnitSuite struct {
|
||||||
@ -315,3 +319,62 @@ func (suite *RestoreUnitSuite) TestAugmentRestorePaths_DifferentRestorePath() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *RestoreUnitSuite) TestGetRestoreResource() {
|
||||||
|
var (
|
||||||
|
id = "id"
|
||||||
|
name = "name"
|
||||||
|
cfgWithPR = control.DefaultRestoreConfig(dttm.HumanReadable)
|
||||||
|
)
|
||||||
|
|
||||||
|
cfgWithPR.ProtectedResource = id
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
cfg control.RestoreConfig
|
||||||
|
orig idname.Provider
|
||||||
|
cache map[string]string
|
||||||
|
expectErr assert.ErrorAssertionFunc
|
||||||
|
expectProvider assert.ValueAssertionFunc
|
||||||
|
expectID string
|
||||||
|
expectName string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "use original",
|
||||||
|
cfg: control.DefaultRestoreConfig(dttm.HumanReadable),
|
||||||
|
orig: idname.NewProvider("oid", "oname"),
|
||||||
|
expectErr: assert.NoError,
|
||||||
|
expectID: "oid",
|
||||||
|
expectName: "oname",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "use new",
|
||||||
|
cfg: cfgWithPR,
|
||||||
|
orig: idname.NewProvider("oid", "oname"),
|
||||||
|
cache: map[string]string{id: name},
|
||||||
|
expectErr: assert.NoError,
|
||||||
|
expectID: id,
|
||||||
|
expectName: name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
svc, result, err := GetRestoreResource(
|
||||||
|
ctx,
|
||||||
|
api.Client{},
|
||||||
|
test.cfg,
|
||||||
|
idname.NewCache(test.cache),
|
||||||
|
test.orig)
|
||||||
|
test.expectErr(t, err, clues.ToCore(err))
|
||||||
|
require.NotNil(t, result)
|
||||||
|
assert.Equal(t, path.OneDriveService, svc)
|
||||||
|
assert.Equal(t, test.expectID, result.ID())
|
||||||
|
assert.Equal(t, test.expectName, result.Name())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
"github.com/alcionai/corso/src/internal/m365/collection/drive"
|
"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/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/m365/support"
|
||||||
"github.com/alcionai/corso/src/internal/operations/inject"
|
"github.com/alcionai/corso/src/internal/operations/inject"
|
||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
@ -21,6 +23,30 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
"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
|
// ConsumeRestoreCollections will restore the specified data collections into OneDrive
|
||||||
func ConsumeRestoreCollections(
|
func ConsumeRestoreCollections(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|||||||
84
src/internal/m365/service/sharepoint/restore_test.go
Normal file
84
src/internal/m365/service/sharepoint/restore_test.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package sharepoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/dttm"
|
||||||
|
"github.com/alcionai/corso/src/internal/common/idname"
|
||||||
|
"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/services/m365/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SharepointRestoreUnitSuite struct {
|
||||||
|
tester.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSharepointRestoreUnitSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &SharepointRestoreUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SharepointRestoreUnitSuite) TestGetRestoreResource() {
|
||||||
|
var (
|
||||||
|
id = "id"
|
||||||
|
name = "name"
|
||||||
|
cfgWithPR = control.DefaultRestoreConfig(dttm.HumanReadable)
|
||||||
|
)
|
||||||
|
|
||||||
|
cfgWithPR.ProtectedResource = id
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
cfg control.RestoreConfig
|
||||||
|
orig idname.Provider
|
||||||
|
cache map[string]string
|
||||||
|
expectErr assert.ErrorAssertionFunc
|
||||||
|
expectProvider assert.ValueAssertionFunc
|
||||||
|
expectID string
|
||||||
|
expectName string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "use original",
|
||||||
|
cfg: control.DefaultRestoreConfig(dttm.HumanReadable),
|
||||||
|
orig: idname.NewProvider("oid", "oname"),
|
||||||
|
expectErr: assert.NoError,
|
||||||
|
expectID: "oid",
|
||||||
|
expectName: "oname",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "use new",
|
||||||
|
cfg: cfgWithPR,
|
||||||
|
orig: idname.NewProvider("oid", "oname"),
|
||||||
|
cache: map[string]string{id: name},
|
||||||
|
expectErr: assert.NoError,
|
||||||
|
expectID: id,
|
||||||
|
expectName: name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
svc, result, err := GetRestoreResource(
|
||||||
|
ctx,
|
||||||
|
api.Client{},
|
||||||
|
test.cfg,
|
||||||
|
idname.NewCache(test.cache),
|
||||||
|
test.orig)
|
||||||
|
test.expectErr(t, err, clues.ToCore(err))
|
||||||
|
require.NotNil(t, result)
|
||||||
|
assert.Equal(t, path.SharePointService, svc)
|
||||||
|
assert.Equal(t, test.expectID, result.ID())
|
||||||
|
assert.Equal(t, test.expectName, result.Name())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -45,6 +45,12 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
RestoreConsumer interface {
|
RestoreConsumer interface {
|
||||||
|
GetRestoreResource(
|
||||||
|
ctx context.Context,
|
||||||
|
service path.ServiceType,
|
||||||
|
rc control.RestoreConfig,
|
||||||
|
orig idname.Provider,
|
||||||
|
) (path.ServiceType, idname.Provider, error)
|
||||||
ConsumeRestoreCollections(
|
ConsumeRestoreCollections(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
rcc RestoreConsumerConfig,
|
rcc RestoreConsumerConfig,
|
||||||
|
|||||||
@ -227,7 +227,11 @@ func (op *RestoreOperation) do(
|
|||||||
return nil, clues.Wrap(err, "getting backup and details")
|
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 {
|
if err != nil {
|
||||||
return nil, clues.Wrap(err, "getting destination protected resource")
|
return nil, clues.Wrap(err, "getting destination protected resource")
|
||||||
}
|
}
|
||||||
@ -236,13 +240,14 @@ func (op *RestoreOperation) do(
|
|||||||
ctx,
|
ctx,
|
||||||
"backup_protected_resource_id", bup.Selector.ID(),
|
"backup_protected_resource_id", bup.Selector.ID(),
|
||||||
"backup_protected_resource_name", clues.Hide(bup.Selector.Name()),
|
"backup_protected_resource_name", clues.Hide(bup.Selector.Name()),
|
||||||
|
"restore_service", restoreService,
|
||||||
"restore_protected_resource_id", restoreToProtectedResource.ID(),
|
"restore_protected_resource_id", restoreToProtectedResource.ID(),
|
||||||
"restore_protected_resource_name", clues.Hide(restoreToProtectedResource.Name()))
|
"restore_protected_resource_name", clues.Hide(restoreToProtectedResource.Name()))
|
||||||
|
|
||||||
// Check if the resource has the service enabled to be able to restore.
|
// Check if the resource has the service enabled to be able to restore.
|
||||||
enabled, err := op.rc.IsServiceEnabled(
|
enabled, err := op.rc.IsServiceEnabled(
|
||||||
ctx,
|
ctx,
|
||||||
op.Selectors.PathService(),
|
restoreService,
|
||||||
restoreToProtectedResource.ID())
|
restoreToProtectedResource.ID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, clues.Wrap(err, "verifying service restore is enabled").WithClues(ctx)
|
return nil, clues.Wrap(err, "verifying service restore is enabled").WithClues(ctx)
|
||||||
@ -350,24 +355,6 @@ func (op *RestoreOperation) persistResults(
|
|||||||
return op.Errors.Failure()
|
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
|
// Restorer funcs
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -10,8 +10,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/common/dttm"
|
|
||||||
"github.com/alcionai/corso/src/internal/common/idname"
|
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
"github.com/alcionai/corso/src/internal/events"
|
"github.com/alcionai/corso/src/internal/events"
|
||||||
evmock "github.com/alcionai/corso/src/internal/events/mock"
|
evmock "github.com/alcionai/corso/src/internal/events/mock"
|
||||||
@ -136,75 +134,6 @@ func (suite *RestoreOpUnitSuite) TestRestoreOperation_PersistResults() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *RestoreOpUnitSuite) TestChooseRestoreResource() {
|
|
||||||
var (
|
|
||||||
id = "id"
|
|
||||||
name = "name"
|
|
||||||
cfgWithPR = control.DefaultRestoreConfig(dttm.HumanReadable)
|
|
||||||
)
|
|
||||||
|
|
||||||
cfgWithPR.ProtectedResource = "cfgid"
|
|
||||||
|
|
||||||
table := []struct {
|
|
||||||
name string
|
|
||||||
cfg control.RestoreConfig
|
|
||||||
ctrl *mock.Controller
|
|
||||||
orig idname.Provider
|
|
||||||
expectErr assert.ErrorAssertionFunc
|
|
||||||
expectProvider assert.ValueAssertionFunc
|
|
||||||
expectID string
|
|
||||||
expectName string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "use original",
|
|
||||||
cfg: control.DefaultRestoreConfig(dttm.HumanReadable),
|
|
||||||
ctrl: &mock.Controller{
|
|
||||||
ProtectedResourceID: id,
|
|
||||||
ProtectedResourceName: name,
|
|
||||||
},
|
|
||||||
orig: idname.NewProvider("oid", "oname"),
|
|
||||||
expectErr: assert.NoError,
|
|
||||||
expectID: "oid",
|
|
||||||
expectName: "oname",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "look up resource with iface",
|
|
||||||
cfg: cfgWithPR,
|
|
||||||
ctrl: &mock.Controller{
|
|
||||||
ProtectedResourceID: id,
|
|
||||||
ProtectedResourceName: name,
|
|
||||||
},
|
|
||||||
orig: idname.NewProvider("oid", "oname"),
|
|
||||||
expectErr: assert.NoError,
|
|
||||||
expectID: id,
|
|
||||||
expectName: name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "error looking up protected resource",
|
|
||||||
cfg: cfgWithPR,
|
|
||||||
ctrl: &mock.Controller{
|
|
||||||
ProtectedResourceErr: assert.AnError,
|
|
||||||
},
|
|
||||||
orig: idname.NewProvider("oid", "oname"),
|
|
||||||
expectErr: assert.Error,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test := range table {
|
|
||||||
suite.Run(test.name, func() {
|
|
||||||
t := suite.T()
|
|
||||||
|
|
||||||
ctx, flush := tester.NewContext(t)
|
|
||||||
defer flush()
|
|
||||||
|
|
||||||
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())
|
|
||||||
assert.Equal(t, test.expectName, result.Name())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// integration
|
// integration
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -42,6 +42,8 @@ func (suite *GroupsBackupIntgSuite) SetupSuite() {
|
|||||||
func (suite *GroupsBackupIntgSuite) TestBackup_Run_incrementalGroups() {
|
func (suite *GroupsBackupIntgSuite) TestBackup_Run_incrementalGroups() {
|
||||||
sel := selectors.NewGroupsRestore([]string{suite.its.group.ID})
|
sel := selectors.NewGroupsRestore([]string{suite.its.group.ID})
|
||||||
|
|
||||||
|
// sel.Filter(sel.Site(suite.its.group.RootSite.ID))
|
||||||
|
|
||||||
ic := func(cs []string) selectors.Selector {
|
ic := func(cs []string) selectors.Selector {
|
||||||
sel.Include(sel.LibraryFolders(cs, selectors.PrefixMatch()))
|
sel.Include(sel.LibraryFolders(cs, selectors.PrefixMatch()))
|
||||||
return sel.Selector
|
return sel.Selector
|
||||||
@ -199,7 +201,7 @@ type GroupsRestoreNightlyIntgSuite struct {
|
|||||||
its intgTesterSetup
|
its intgTesterSetup
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGroupsRestoreIntgSuite(t *testing.T) {
|
func TestGroupsRestoreNightlyIntgSuite(t *testing.T) {
|
||||||
suite.Run(t, &GroupsRestoreNightlyIntgSuite{
|
suite.Run(t, &GroupsRestoreNightlyIntgSuite{
|
||||||
Suite: tester.NewNightlySuite(
|
Suite: tester.NewNightlySuite(
|
||||||
t,
|
t,
|
||||||
@ -222,22 +224,21 @@ func (suite *GroupsRestoreNightlyIntgSuite) TestRestore_Run_groupsWithAdvancedOp
|
|||||||
suite,
|
suite,
|
||||||
suite.its.ac,
|
suite.its.ac,
|
||||||
sel.Selector,
|
sel.Selector,
|
||||||
suite.its.group.RootSite.DriveID,
|
suite.its.group.RootSite)
|
||||||
suite.its.group.RootSite.DriveRootFolderID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (suite *GroupsRestoreNightlyIntgSuite) TestRestore_Run_groupsAlternateProtectedResource() {
|
func (suite *GroupsRestoreNightlyIntgSuite) TestRestore_Run_groupsAlternateProtectedResource() {
|
||||||
// sel := selectors.NewGroupsBackup([]string{suite.its.group.ID})
|
sel := selectors.NewGroupsBackup([]string{suite.its.group.ID})
|
||||||
// sel.Include(selTD.GroupsBackupLibraryFolderScope(sel))
|
sel.Include(selTD.GroupsBackupLibraryFolderScope(sel))
|
||||||
// sel.Filter(sel.Library("documents"))
|
sel.Filter(sel.Library("documents"))
|
||||||
// sel.DiscreteOwner = suite.its.group.ID
|
sel.DiscreteOwner = suite.its.group.ID
|
||||||
|
|
||||||
// runDriveRestoreToAlternateProtectedResource(
|
runDriveRestoreToAlternateProtectedResource(
|
||||||
// suite.T(),
|
suite.T(),
|
||||||
// suite,
|
suite,
|
||||||
// suite.its.ac,
|
suite.its.ac,
|
||||||
// sel.Selector,
|
sel.Selector,
|
||||||
// suite.its.group.RootSite,
|
suite.its.group.RootSite,
|
||||||
// suite.its.secondaryGroup.RootSite,
|
suite.its.secondaryGroup.RootSite,
|
||||||
// suite.its.secondaryGroup.ID)
|
suite.its.secondaryGroup.RootSite.ID)
|
||||||
// }
|
}
|
||||||
|
|||||||
@ -398,6 +398,21 @@ func generateContainerOfItems(
|
|||||||
restoreCfg.Location = destFldr
|
restoreCfg.Location = destFldr
|
||||||
restoreCfg.IncludePermissions = true
|
restoreCfg.IncludePermissions = true
|
||||||
|
|
||||||
|
if sel.Service == selectors.ServiceGroups {
|
||||||
|
restoreCfg.SubService.Type = path.SharePointService
|
||||||
|
restoreCfg.SubService.ID = siteID
|
||||||
|
}
|
||||||
|
|
||||||
|
var protectedResource idname.Provider = sel
|
||||||
|
|
||||||
|
// In case of groups, we use the root site as the restore target
|
||||||
|
if sel.Service == selectors.ServiceGroups {
|
||||||
|
rootSite, err := ctrl.AC.Sites().GetByID(ctx, siteID, api.CallConfig{})
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
protectedResource = idname.NewProvider(siteID, ptr.Val(rootSite.GetName()))
|
||||||
|
}
|
||||||
|
|
||||||
dataColls := buildCollections(
|
dataColls := buildCollections(
|
||||||
t,
|
t,
|
||||||
service,
|
service,
|
||||||
@ -410,7 +425,7 @@ func generateContainerOfItems(
|
|||||||
rcc := inject.RestoreConsumerConfig{
|
rcc := inject.RestoreConsumerConfig{
|
||||||
BackupVersion: backupVersion,
|
BackupVersion: backupVersion,
|
||||||
Options: opts,
|
Options: opts,
|
||||||
ProtectedResource: sel,
|
ProtectedResource: protectedResource,
|
||||||
RestoreConfig: restoreCfg,
|
RestoreConfig: restoreCfg,
|
||||||
Selector: sel,
|
Selector: sel,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1015,8 +1015,7 @@ func (suite *OneDriveRestoreNightlyIntgSuite) TestRestore_Run_onedriveWithAdvanc
|
|||||||
suite,
|
suite,
|
||||||
suite.its.ac,
|
suite.its.ac,
|
||||||
sel.Selector,
|
sel.Selector,
|
||||||
suite.its.user.DriveID,
|
suite.its.user)
|
||||||
suite.its.user.DriveRootFolderID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDriveRestoreWithAdvancedOptions(
|
func runDriveRestoreWithAdvancedOptions(
|
||||||
@ -1024,7 +1023,7 @@ func runDriveRestoreWithAdvancedOptions(
|
|||||||
suite tester.Suite,
|
suite tester.Suite,
|
||||||
ac api.Client,
|
ac api.Client,
|
||||||
sel selectors.Selector, // both Restore and Backup types work.
|
sel selectors.Selector, // both Restore and Backup types work.
|
||||||
driveID, rootFolderID string,
|
driveFrom ids,
|
||||||
) {
|
) {
|
||||||
ctx, flush := tester.NewContext(t)
|
ctx, flush := tester.NewContext(t)
|
||||||
defer flush()
|
defer flush()
|
||||||
@ -1050,6 +1049,12 @@ func runDriveRestoreWithAdvancedOptions(
|
|||||||
acd = ac.Drives()
|
acd = ac.Drives()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Groups restore needs subservice information
|
||||||
|
if sel.Service == selectors.ServiceGroups {
|
||||||
|
restoreCfg.SubService.Type = path.SharePointService
|
||||||
|
restoreCfg.SubService.ID = driveFrom.ID
|
||||||
|
}
|
||||||
|
|
||||||
// initial restore
|
// initial restore
|
||||||
|
|
||||||
suite.Run("baseline", func() {
|
suite.Run("baseline", func() {
|
||||||
@ -1078,18 +1083,18 @@ func runDriveRestoreWithAdvancedOptions(
|
|||||||
|
|
||||||
// get all files in folder, use these as the base
|
// get all files in folder, use these as the base
|
||||||
// set of files to compare against.
|
// set of files to compare against.
|
||||||
contGC, err := acd.GetFolderByName(ctx, driveID, rootFolderID, restoreCfg.Location)
|
contGC, err := acd.GetFolderByName(ctx, driveFrom.DriveID, driveFrom.DriveRootFolderID, restoreCfg.Location)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
// the folder containing the files is a child of the folder created by the restore.
|
// the folder containing the files is a child of the folder created by the restore.
|
||||||
contGC, err = acd.GetFolderByName(ctx, driveID, ptr.Val(contGC.GetId()), selTD.TestFolderName)
|
contGC, err = acd.GetFolderByName(ctx, driveFrom.DriveID, ptr.Val(contGC.GetId()), selTD.TestFolderName)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
containerID = ptr.Val(contGC.GetId())
|
containerID = ptr.Val(contGC.GetId())
|
||||||
|
|
||||||
collKeys, err = acd.GetItemsInContainerByCollisionKey(
|
collKeys, err = acd.GetItemsInContainerByCollisionKey(
|
||||||
ctx,
|
ctx,
|
||||||
driveID,
|
driveFrom.DriveID,
|
||||||
containerID)
|
containerID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
@ -1097,7 +1102,7 @@ func runDriveRestoreWithAdvancedOptions(
|
|||||||
|
|
||||||
checkRestoreCounts(t, ctr, 0, 0, countItemsInRestore)
|
checkRestoreCounts(t, ctr, 0, 0, countItemsInRestore)
|
||||||
|
|
||||||
fileIDs, err = acd.GetItemIDsInContainer(ctx, driveID, containerID)
|
fileIDs, err = acd.GetItemIDsInContainer(ctx, driveFrom.DriveID, containerID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1139,14 +1144,14 @@ func runDriveRestoreWithAdvancedOptions(
|
|||||||
result := filterCollisionKeyResults(
|
result := filterCollisionKeyResults(
|
||||||
t,
|
t,
|
||||||
ctx,
|
ctx,
|
||||||
driveID,
|
driveFrom.DriveID,
|
||||||
containerID,
|
containerID,
|
||||||
GetItemsInContainerByCollisionKeyer[api.DriveItemIDType](acd),
|
GetItemsInContainerByCollisionKeyer[api.DriveItemIDType](acd),
|
||||||
collKeys)
|
collKeys)
|
||||||
|
|
||||||
assert.Len(t, result, 0, "no new items should get added")
|
assert.Len(t, result, 0, "no new items should get added")
|
||||||
|
|
||||||
currentFileIDs, err := acd.GetItemIDsInContainer(ctx, driveID, containerID)
|
currentFileIDs, err := acd.GetItemIDsInContainer(ctx, driveFrom.DriveID, containerID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
assert.Equal(t, fileIDs, currentFileIDs, "ids are equal")
|
assert.Equal(t, fileIDs, currentFileIDs, "ids are equal")
|
||||||
@ -1195,7 +1200,7 @@ func runDriveRestoreWithAdvancedOptions(
|
|||||||
result := filterCollisionKeyResults(
|
result := filterCollisionKeyResults(
|
||||||
t,
|
t,
|
||||||
ctx,
|
ctx,
|
||||||
driveID,
|
driveFrom.DriveID,
|
||||||
containerID,
|
containerID,
|
||||||
GetItemsInContainerByCollisionKeyer[api.DriveItemIDType](acd),
|
GetItemsInContainerByCollisionKeyer[api.DriveItemIDType](acd),
|
||||||
collKeys)
|
collKeys)
|
||||||
@ -1206,7 +1211,7 @@ func runDriveRestoreWithAdvancedOptions(
|
|||||||
assert.NotEqual(t, v, collKeys[k], "replaced items should have new IDs")
|
assert.NotEqual(t, v, collKeys[k], "replaced items should have new IDs")
|
||||||
}
|
}
|
||||||
|
|
||||||
currentFileIDs, err := acd.GetItemIDsInContainer(ctx, driveID, containerID)
|
currentFileIDs, err := acd.GetItemIDsInContainer(ctx, driveFrom.DriveID, containerID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
assert.Equal(t, len(fileIDs), len(currentFileIDs), "count of ids ids are equal")
|
assert.Equal(t, len(fileIDs), len(currentFileIDs), "count of ids ids are equal")
|
||||||
@ -1260,14 +1265,14 @@ func runDriveRestoreWithAdvancedOptions(
|
|||||||
result := filterCollisionKeyResults(
|
result := filterCollisionKeyResults(
|
||||||
t,
|
t,
|
||||||
ctx,
|
ctx,
|
||||||
driveID,
|
driveFrom.DriveID,
|
||||||
containerID,
|
containerID,
|
||||||
GetItemsInContainerByCollisionKeyer[api.DriveItemIDType](acd),
|
GetItemsInContainerByCollisionKeyer[api.DriveItemIDType](acd),
|
||||||
collKeys)
|
collKeys)
|
||||||
|
|
||||||
assert.Len(t, result, len(collKeys), "all items should have been added as copies")
|
assert.Len(t, result, len(collKeys), "all items should have been added as copies")
|
||||||
|
|
||||||
currentFileIDs, err := acd.GetItemIDsInContainer(ctx, driveID, containerID)
|
currentFileIDs, err := acd.GetItemIDsInContainer(ctx, driveFrom.DriveID, containerID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
assert.Equal(t, 2*len(fileIDs), len(currentFileIDs), "count of ids should be double from before")
|
assert.Equal(t, 2*len(fileIDs), len(currentFileIDs), "count of ids should be double from before")
|
||||||
@ -1320,8 +1325,13 @@ func runDriveRestoreToAlternateProtectedResource(
|
|||||||
acd = ac.Drives()
|
acd = ac.Drives()
|
||||||
)
|
)
|
||||||
|
|
||||||
// first restore to the 'from' resource
|
// Groups restore needs subservice information
|
||||||
|
if sel.Service == selectors.ServiceGroups {
|
||||||
|
restoreCfg.SubService.Type = path.SharePointService
|
||||||
|
restoreCfg.SubService.ID = driveFrom.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// first restore to the 'from' resource
|
||||||
suite.Run("restore original resource", func() {
|
suite.Run("restore original resource", func() {
|
||||||
mb = evmock.NewBus()
|
mb = evmock.NewBus()
|
||||||
fromCtr := count.New()
|
fromCtr := count.New()
|
||||||
|
|||||||
@ -209,8 +209,7 @@ func (suite *SharePointRestoreNightlyIntgSuite) TestRestore_Run_sharepointWithAd
|
|||||||
suite,
|
suite,
|
||||||
suite.its.ac,
|
suite.its.ac,
|
||||||
sel.Selector,
|
sel.Selector,
|
||||||
suite.its.site.DriveID,
|
suite.its.site)
|
||||||
suite.its.site.DriveRootFolderID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *SharePointRestoreNightlyIntgSuite) TestRestore_Run_sharepointAlternateProtectedResource() {
|
func (suite *SharePointRestoreNightlyIntgSuite) TestRestore_Run_sharepointAlternateProtectedResource() {
|
||||||
|
|||||||
@ -38,6 +38,11 @@ func IsValidCollisionPolicy(cp CollisionPolicy) bool {
|
|||||||
|
|
||||||
const RootLocation = "/"
|
const RootLocation = "/"
|
||||||
|
|
||||||
|
type RestoreConfigSubService struct {
|
||||||
|
ID string
|
||||||
|
Type path.ServiceType
|
||||||
|
}
|
||||||
|
|
||||||
// RestoreConfig contains
|
// RestoreConfig contains
|
||||||
type RestoreConfig struct {
|
type RestoreConfig struct {
|
||||||
// Defines the per-item collision handling policy.
|
// Defines the per-item collision handling policy.
|
||||||
@ -49,6 +54,11 @@ type RestoreConfig struct {
|
|||||||
// Defaults to empty.
|
// Defaults to empty.
|
||||||
ProtectedResource string `json:"protectedResource"`
|
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.
|
// Location specifies the container into which the data will be restored.
|
||||||
// Only accepts container names, does not accept IDs.
|
// Only accepts container names, does not accept IDs.
|
||||||
// If empty or "/", data will get restored in place, beginning at the root.
|
// If empty or "/", data will get restored in place, beginning at the root.
|
||||||
|
|||||||
@ -116,15 +116,18 @@ func (suite *RestoreUnitSuite) TestRestoreConfig_piiHandling() {
|
|||||||
expectPlain string
|
expectPlain string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty",
|
name: "empty",
|
||||||
expectSafe: `{"onCollision":"","protectedResource":"","location":"","drive":"","includePermissions":false}`,
|
expectSafe: `{"onCollision":"","protectedResource":"","SubService":{"ID":"","Type":0}` +
|
||||||
expectPlain: `{"onCollision":"","protectedResource":"","location":"","drive":"","includePermissions":false}`,
|
`,"location":"","drive":"","includePermissions":false}`,
|
||||||
|
expectPlain: `{"onCollision":"","protectedResource":"","SubService":{"ID":"","Type":0}` +
|
||||||
|
`,"location":"","drive":"","includePermissions":false}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "defaults",
|
name: "defaults",
|
||||||
rc: cdrc,
|
rc: cdrc,
|
||||||
expectSafe: `{"onCollision":"skip","protectedResource":"","location":"***","drive":"","includePermissions":false}`,
|
expectSafe: `{"onCollision":"skip","protectedResource":"","SubService":{"ID":"","Type":0}` +
|
||||||
expectPlain: `{"onCollision":"skip","protectedResource":"","location":"` +
|
`,"location":"***","drive":"","includePermissions":false}`,
|
||||||
|
expectPlain: `{"onCollision":"skip","protectedResource":"","SubService":{"ID":"","Type":0},"location":"` +
|
||||||
cdrc.Location + `","drive":"","includePermissions":false}`,
|
cdrc.Location + `","drive":"","includePermissions":false}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -136,9 +139,11 @@ func (suite *RestoreUnitSuite) TestRestoreConfig_piiHandling() {
|
|||||||
Drive: "somedriveid",
|
Drive: "somedriveid",
|
||||||
IncludePermissions: true,
|
IncludePermissions: true,
|
||||||
},
|
},
|
||||||
expectSafe: `{"onCollision":"copy","protectedResource":"***","location":"***/exchange/***/email/***/***/***",` +
|
expectSafe: `{"onCollision":"copy","protectedResource":"***","SubService":{"ID":"","Type":0}` +
|
||||||
|
`,"location":"***/exchange/***/email/***/***/***",` +
|
||||||
`"drive":"***","includePermissions":true}`,
|
`"drive":"***","includePermissions":true}`,
|
||||||
expectPlain: `{"onCollision":"copy","protectedResource":"snoob","location":"tid/exchange/ro/email/foo/bar/baz",` +
|
expectPlain: `{"onCollision":"copy","protectedResource":"snoob","SubService":{"ID":"","Type":0}` +
|
||||||
|
`,"location":"tid/exchange/ro/email/foo/bar/baz",` +
|
||||||
`"drive":"somedriveid","includePermissions":true}`,
|
`"drive":"somedriveid","includePermissions":true}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,5 +33,3 @@ Below is a list of known Corso issues and limitations:
|
|||||||
* Teams messages don't support Restore due to limited Graph API support for message creation.
|
* Teams messages don't support Restore due to limited Graph API support for message creation.
|
||||||
|
|
||||||
* Groups and Teams support is available in an early-access status, and may be subject to breaking changes.
|
* Groups and Teams support is available in an early-access status, and may be subject to breaking changes.
|
||||||
|
|
||||||
* Restoring the data into a different Group from the one it was backed up from isn't currently supported
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user