From 2e4fc71310e5942148571adb71bb5ed0c4a3d9df Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Wed, 10 May 2023 19:49:32 -0700 Subject: [PATCH] Add restore path generation code (#3362) In preparation for switching to folder IDs, add logic to generate the restore path based on prefix information from the RepoRef and LocationRef of items Contains fallback code (and tests) to handle older details versions that may not have had LocationRef Manually tested restore from old backup that didn't have any LocationRef information Manually tested restore checking that calendar names are shown instead of IDs in progress bar --- #### Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No #### Type of change - [x] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Supportability/Tests - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup #### Issue(s) * #3197 * fixes #3218 #### Test Plan - [x] :muscle: Manual - [x] :zap: Unit test - [ ] :green_heart: E2E --- .../restore_path_transformer.go | 180 ++++++++++ .../restore_path_transformer_test.go | 340 ++++++++++++++++++ src/internal/operations/restore.go | 43 +-- src/pkg/backup/details/testdata/testdata.go | 101 +++--- src/pkg/path/drive.go | 10 + src/pkg/path/drive_test.go | 47 +++ src/pkg/selectors/selectors_reduce_test.go | 12 - 7 files changed, 632 insertions(+), 101 deletions(-) create mode 100644 src/internal/operations/pathtransformer/restore_path_transformer.go create mode 100644 src/internal/operations/pathtransformer/restore_path_transformer_test.go diff --git a/src/internal/operations/pathtransformer/restore_path_transformer.go b/src/internal/operations/pathtransformer/restore_path_transformer.go new file mode 100644 index 000000000..db8b2befd --- /dev/null +++ b/src/internal/operations/pathtransformer/restore_path_transformer.go @@ -0,0 +1,180 @@ +package pathtransformer + +import ( + "context" + + "github.com/alcionai/clues" + + "github.com/alcionai/corso/src/internal/version" + "github.com/alcionai/corso/src/pkg/backup/details" + "github.com/alcionai/corso/src/pkg/fault" + "github.com/alcionai/corso/src/pkg/logger" + "github.com/alcionai/corso/src/pkg/path" +) + +func locationRef( + ent *details.Entry, + repoRef path.Path, + backupVersion int, +) (*path.Builder, error) { + loc := ent.LocationRef + + // At this backup version all data types should populate LocationRef. + if len(loc) > 0 || backupVersion >= version.OneDrive7LocationRef { + return path.Builder{}.SplitUnescapeAppend(loc) + } + + // We could get an empty LocationRef either because it wasn't populated or it + // was in the root of the data type. + elems := repoRef.Folders() + + if ent.OneDrive != nil || ent.SharePoint != nil { + dp, err := path.ToDrivePath(repoRef) + if err != nil { + return nil, clues.Wrap(err, "fallback for LocationRef") + } + + elems = append([]string{dp.Root}, dp.Folders...) + } + + return path.Builder{}.Append(elems...), nil +} + +func basicLocationPath(repoRef path.Path, locRef *path.Builder) (path.Path, error) { + if len(locRef.Elements()) == 0 { + res, err := path.ServicePrefix( + repoRef.Tenant(), + repoRef.ResourceOwner(), + repoRef.Service(), + repoRef.Category()) + if err != nil { + return nil, clues.Wrap(err, "getting prefix for empty location") + } + + return res, nil + } + + return locRef.ToDataLayerPath( + repoRef.Tenant(), + repoRef.ResourceOwner(), + repoRef.Service(), + repoRef.Category(), + false) +} + +func drivePathMerge( + ent *details.Entry, + repoRef path.Path, + locRef *path.Builder, +) (path.Path, error) { + // Try getting the drive ID from the item. Not all details versions had it + // though. + var driveID string + + if ent.SharePoint != nil { + driveID = ent.SharePoint.DriveID + } else if ent.OneDrive != nil { + driveID = ent.OneDrive.DriveID + } + + // Fallback to trying to get from RepoRef. + if len(driveID) == 0 { + odp, err := path.ToDrivePath(repoRef) + if err != nil { + return nil, clues.Wrap(err, "fallback getting DriveID") + } + + driveID = odp.DriveID + } + + return basicLocationPath( + repoRef, + path.BuildDriveLocation(driveID, locRef.Elements()...)) +} + +func makeRestorePathsForEntry( + ctx context.Context, + backupVersion int, + ent *details.Entry, +) (path.RestorePaths, error) { + res := path.RestorePaths{} + + repoRef, err := path.FromDataLayerPath(ent.RepoRef, true) + if err != nil { + err = clues.Wrap(err, "parsing RepoRef"). + WithClues(ctx). + With("repo_ref", clues.Hide(ent.RepoRef), "location_ref", clues.Hide(ent.LocationRef)) + + return res, err + } + + res.StoragePath = repoRef + ctx = clues.Add(ctx, "repo_ref", repoRef) + + // Get the LocationRef so we can munge it onto our path. + locRef, err := locationRef(ent, repoRef, backupVersion) + if err != nil { + err = clues.Wrap(err, "parsing LocationRef after reduction"). + WithClues(ctx). + With("location_ref", clues.Hide(ent.LocationRef)) + + return res, err + } + + ctx = clues.Add(ctx, "location_ref", locRef) + + // Now figure out what type of ent it is and munge the path accordingly. + // Eventually we're going to need munging for: + // * Exchange Calendars (different folder handling) + // * Exchange Email/Contacts + // * OneDrive/SharePoint (needs drive information) + if ent.Exchange != nil { + // TODO(ashmrtn): Eventually make Events have it's own function to handle + // setting the restore destination properly. + res.RestorePath, err = basicLocationPath(repoRef, locRef) + } else if ent.OneDrive != nil || + (ent.SharePoint != nil && ent.SharePoint.ItemType == details.SharePointLibrary) || + (ent.SharePoint != nil && ent.SharePoint.ItemType == details.OneDriveItem) { + res.RestorePath, err = drivePathMerge(ent, repoRef, locRef) + } else { + return res, clues.New("unknown entry type").WithClues(ctx) + } + + if err != nil { + return res, clues.Wrap(err, "generating RestorePath").WithClues(ctx) + } + + return res, nil +} + +// GetPaths takes a set of filtered details entries and returns a set of +// RestorePaths for the entries. +func GetPaths( + ctx context.Context, + backupVersion int, + items []*details.Entry, + errs *fault.Bus, +) ([]path.RestorePaths, error) { + var ( + paths = make([]path.RestorePaths, len(items)) + el = errs.Local() + ) + + for i, ent := range items { + if el.Failure() != nil { + break + } + + restorePaths, err := makeRestorePathsForEntry(ctx, backupVersion, ent) + if err != nil { + el.AddRecoverable(clues.Wrap(err, "getting restore paths")) + continue + } + + paths[i] = restorePaths + } + + logger.Ctx(ctx).Infof("found %d details entries to restore", len(paths)) + + return paths, el.Failure() +} diff --git a/src/internal/operations/pathtransformer/restore_path_transformer_test.go b/src/internal/operations/pathtransformer/restore_path_transformer_test.go new file mode 100644 index 000000000..57381c3cf --- /dev/null +++ b/src/internal/operations/pathtransformer/restore_path_transformer_test.go @@ -0,0 +1,340 @@ +package pathtransformer_test + +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/operations/pathtransformer" + "github.com/alcionai/corso/src/internal/tester" + "github.com/alcionai/corso/src/internal/version" + "github.com/alcionai/corso/src/pkg/backup/details" + "github.com/alcionai/corso/src/pkg/backup/details/testdata" + "github.com/alcionai/corso/src/pkg/fault" + "github.com/alcionai/corso/src/pkg/path" +) + +type RestorePathTransformerUnitSuite struct { + tester.Suite +} + +func TestRestorePathTransformerUnitSuite(t *testing.T) { + suite.Run(t, &RestorePathTransformerUnitSuite{Suite: tester.NewUnitSuite(t)}) +} + +func (suite *RestorePathTransformerUnitSuite) TestGetPaths() { + type expectPaths struct { + storage string + restore string + isRestorePrefix bool + } + + toRestore := func( + repoRef path.Path, + unescapedFolders ...string, + ) string { + return path.Builder{}. + Append( + repoRef.Tenant(), + repoRef.Service().String(), + repoRef.ResourceOwner(), + repoRef.Category().String()). + Append(unescapedFolders...). + String() + } + + var ( + driveID = "some-drive-id" + extraItemName = "some-item" + SharePointRootItemPath = testdata.SharePointRootPath.MustAppend(extraItemName, true) + ) + + table := []struct { + name string + backupVersion int + input []*details.Entry + expectErr assert.ErrorAssertionFunc + expected []expectPaths + }{ + { + name: "SharePoint List Errors", + // No version bump for the change so we always have to check for this. + backupVersion: version.All8MigrateUserPNToID, + input: []*details.Entry{ + { + RepoRef: SharePointRootItemPath.RR.String(), + LocationRef: SharePointRootItemPath.Loc.String(), + ItemInfo: details.ItemInfo{ + SharePoint: &details.SharePointInfo{ + ItemType: details.SharePointList, + }, + }, + }, + }, + expectErr: assert.Error, + }, + { + name: "SharePoint Page Errors", + // No version bump for the change so we always have to check for this. + backupVersion: version.All8MigrateUserPNToID, + input: []*details.Entry{ + { + RepoRef: SharePointRootItemPath.RR.String(), + LocationRef: SharePointRootItemPath.Loc.String(), + ItemInfo: details.ItemInfo{ + SharePoint: &details.SharePointInfo{ + ItemType: details.SharePointPage, + }, + }, + }, + }, + expectErr: assert.Error, + }, + { + name: "SharePoint old format, item in root", + // No version bump for the change so we always have to check for this. + backupVersion: version.All8MigrateUserPNToID, + input: []*details.Entry{ + { + RepoRef: SharePointRootItemPath.RR.String(), + LocationRef: SharePointRootItemPath.Loc.String(), + ItemInfo: details.ItemInfo{ + SharePoint: &details.SharePointInfo{ + ItemType: details.OneDriveItem, + DriveID: driveID, + }, + }, + }, + }, + expectErr: assert.NoError, + expected: []expectPaths{ + { + storage: SharePointRootItemPath.RR.String(), + restore: toRestore( + SharePointRootItemPath.RR, + append( + []string{"drives", driveID}, + SharePointRootItemPath.Loc.Elements()...)...), + }, + }, + }, + { + name: "SharePoint, no LocationRef, no DriveID, item in root", + backupVersion: version.OneDrive6NameInMeta, + input: []*details.Entry{ + { + RepoRef: SharePointRootItemPath.RR.String(), + ItemInfo: details.ItemInfo{ + SharePoint: &details.SharePointInfo{ + ItemType: details.SharePointLibrary, + }, + }, + }, + }, + expectErr: assert.NoError, + expected: []expectPaths{ + { + storage: SharePointRootItemPath.RR.String(), + restore: toRestore( + SharePointRootItemPath.RR, + append( + []string{"drives"}, + // testdata path has '.d' on the drives folder we need to remove. + SharePointRootItemPath.RR.Folders()[1:]...)...), + }, + }, + }, + { + name: "OneDrive, nested item", + backupVersion: version.All8MigrateUserPNToID, + input: []*details.Entry{ + { + RepoRef: testdata.OneDriveItemPath2.RR.String(), + LocationRef: testdata.OneDriveItemPath2.Loc.String(), + ItemInfo: details.ItemInfo{ + OneDrive: &details.OneDriveInfo{ + ItemType: details.OneDriveItem, + DriveID: driveID, + }, + }, + }, + }, + expectErr: assert.NoError, + expected: []expectPaths{ + { + storage: testdata.OneDriveItemPath2.RR.String(), + restore: toRestore( + testdata.OneDriveItemPath2.RR, + append( + []string{"drives", driveID}, + testdata.OneDriveItemPath2.Loc.Elements()...)...), + }, + }, + }, + { + name: "Exchange Email, extra / in path", + backupVersion: version.All8MigrateUserPNToID, + input: []*details.Entry{ + { + RepoRef: testdata.ExchangeEmailItemPath3.RR.String(), + LocationRef: testdata.ExchangeEmailItemPath3.Loc.String(), + ItemInfo: details.ItemInfo{ + Exchange: &details.ExchangeInfo{ + ItemType: details.ExchangeMail, + }, + }, + }, + }, + expectErr: assert.NoError, + expected: []expectPaths{ + { + storage: testdata.ExchangeEmailItemPath3.RR.String(), + restore: toRestore( + testdata.ExchangeEmailItemPath3.RR, + testdata.ExchangeEmailItemPath3.Loc.Elements()...), + }, + }, + }, + { + name: "Exchange Email, no LocationRef, extra / in path", + backupVersion: version.OneDrive7LocationRef, + input: []*details.Entry{ + { + RepoRef: testdata.ExchangeEmailItemPath3.RR.String(), + LocationRef: testdata.ExchangeEmailItemPath3.Loc.String(), + ItemInfo: details.ItemInfo{ + Exchange: &details.ExchangeInfo{ + ItemType: details.ExchangeMail, + }, + }, + }, + }, + expectErr: assert.NoError, + expected: []expectPaths{ + { + storage: testdata.ExchangeEmailItemPath3.RR.String(), + restore: toRestore( + testdata.ExchangeEmailItemPath3.RR, + testdata.ExchangeEmailItemPath3.Loc.Elements()...), + }, + }, + }, + { + name: "Exchange Contact", + backupVersion: version.All8MigrateUserPNToID, + input: []*details.Entry{ + { + RepoRef: testdata.ExchangeContactsItemPath1.RR.String(), + LocationRef: testdata.ExchangeContactsItemPath1.Loc.String(), + ItemInfo: details.ItemInfo{ + Exchange: &details.ExchangeInfo{ + ItemType: details.ExchangeContact, + }, + }, + }, + }, + expectErr: assert.NoError, + expected: []expectPaths{ + { + storage: testdata.ExchangeContactsItemPath1.RR.String(), + restore: toRestore( + testdata.ExchangeContactsItemPath1.RR, + testdata.ExchangeContactsItemPath1.Loc.Elements()...), + }, + }, + }, + { + name: "Exchange Contact, root dir", + backupVersion: version.All8MigrateUserPNToID, + input: []*details.Entry{ + { + RepoRef: testdata.ExchangeContactsItemPath1.RR.String(), + ItemInfo: details.ItemInfo{ + Exchange: &details.ExchangeInfo{ + ItemType: details.ExchangeContact, + }, + }, + }, + }, + expectErr: assert.NoError, + expected: []expectPaths{ + { + storage: testdata.ExchangeContactsItemPath1.RR.String(), + restore: toRestore(testdata.ExchangeContactsItemPath1.RR, "tmp"), + isRestorePrefix: true, + }, + }, + }, + { + name: "Exchange Event", + backupVersion: version.All8MigrateUserPNToID, + input: []*details.Entry{ + { + RepoRef: testdata.ExchangeEmailItemPath3.RR.String(), + LocationRef: testdata.ExchangeEmailItemPath3.Loc.String(), + ItemInfo: details.ItemInfo{ + Exchange: &details.ExchangeInfo{ + ItemType: details.ExchangeMail, + }, + }, + }, + }, + expectErr: assert.NoError, + expected: []expectPaths{ + { + storage: testdata.ExchangeEmailItemPath3.RR.String(), + restore: toRestore( + testdata.ExchangeEmailItemPath3.RR, + testdata.ExchangeEmailItemPath3.Loc.Elements()...), + }, + }, + }, + } + + for _, test := range table { + suite.Run(test.name, func() { + ctx, flush := tester.NewContext() + defer flush() + + t := suite.T() + + paths, err := pathtransformer.GetPaths( + ctx, + test.backupVersion, + test.input, + fault.New(true)) + test.expectErr(t, err, clues.ToCore(err)) + + if err != nil { + return + } + + expected := make([]path.RestorePaths, 0, len(test.expected)) + + for _, e := range test.expected { + tmp := path.RestorePaths{} + p, err := path.FromDataLayerPath(e.storage, true) + require.NoError(t, err, "parsing expected storage path", clues.ToCore(err)) + + tmp.StoragePath = p + + p, err = path.FromDataLayerPath(e.restore, false) + require.NoError(t, err, "parsing expected restore path", clues.ToCore(err)) + + if e.isRestorePrefix { + p, err = p.Dir() + require.NoError(t, err, "getting service prefix", clues.ToCore(err)) + } + + tmp.RestorePath = p + + expected = append(expected, tmp) + } + + assert.ElementsMatch(t, expected, paths) + }) + } +} diff --git a/src/internal/operations/restore.go b/src/internal/operations/restore.go index 2dd5cd40c..28dbb5e1a 100644 --- a/src/internal/operations/restore.go +++ b/src/internal/operations/restore.go @@ -18,6 +18,7 @@ import ( "github.com/alcionai/corso/src/internal/model" "github.com/alcionai/corso/src/internal/observe" "github.com/alcionai/corso/src/internal/operations/inject" + "github.com/alcionai/corso/src/internal/operations/pathtransformer" "github.com/alcionai/corso/src/internal/stats" "github.com/alcionai/corso/src/internal/streamstore" "github.com/alcionai/corso/src/pkg/account" @@ -355,41 +356,9 @@ func formatDetailsForRestoration( return nil, err } - var ( - fdsPaths = fds.Paths() - paths = make([]path.RestorePaths, len(fdsPaths)) - shortRefs = make([]string, len(fdsPaths)) - el = errs.Local() - ) - - for i := range fdsPaths { - if el.Failure() != nil { - break - } - - p, err := path.FromDataLayerPath(fdsPaths[i], true) - if err != nil { - el.AddRecoverable(clues. - Wrap(err, "parsing details path after reduction"). - WithMap(clues.In(ctx)). - With("path", fdsPaths[i])) - - continue - } - - dir, err := p.Dir() - if err != nil { - el.AddRecoverable(clues. - Wrap(err, "getting restore directory after reduction"). - WithClues(ctx). - With("path", fdsPaths[i])) - - continue - } - - paths[i].StoragePath = p - paths[i].RestorePath = dir - shortRefs[i] = p.ShortRef() + paths, err := pathtransformer.GetPaths(ctx, backupVersion, fds.Items(), errs) + if err != nil { + return nil, clues.Wrap(err, "getting restore paths") } if sel.Service == selectors.ServiceOneDrive { @@ -399,7 +368,5 @@ func formatDetailsForRestoration( } } - logger.Ctx(ctx).With("short_refs", shortRefs).Infof("found %d details entries to restore", len(shortRefs)) - - return paths, el.Failure() + return paths, nil } diff --git a/src/pkg/backup/details/testdata/testdata.go b/src/pkg/backup/details/testdata/testdata.go index 0b770c050..a406d838a 100644 --- a/src/pkg/backup/details/testdata/testdata.go +++ b/src/pkg/backup/details/testdata/testdata.go @@ -54,10 +54,10 @@ func locFromRepo(rr path.Path, isItem bool) *path.Builder { type repoRefAndLocRef struct { RR path.Path - loc *path.Builder + Loc *path.Builder } -func (p repoRefAndLocRef) mustAppend(newElement string, isItem bool) repoRefAndLocRef { +func (p repoRefAndLocRef) MustAppend(newElement string, isItem bool) repoRefAndLocRef { e := newElement + folderSuffix if isItem { @@ -68,7 +68,7 @@ func (p repoRefAndLocRef) mustAppend(newElement string, isItem bool) repoRefAndL RR: mustAppendPath(p.RR, e, isItem), } - res.loc = locFromRepo(res.RR, isItem) + res.Loc = locFromRepo(res.RR, isItem) return res } @@ -85,7 +85,7 @@ func (p repoRefAndLocRef) FolderLocation() string { lastElem = f[len(f)-2] } - return p.loc.Append(strings.TrimSuffix(lastElem, folderSuffix)).String() + return p.Loc.Append(strings.TrimSuffix(lastElem, folderSuffix)).String() } func mustPathRep(ref string, isItem bool) repoRefAndLocRef { @@ -115,7 +115,7 @@ func mustPathRep(ref string, isItem bool) repoRefAndLocRef { } res.RR = rr - res.loc = locFromRepo(rr, isItem) + res.Loc = locFromRepo(rr, isItem) return res } @@ -138,12 +138,12 @@ var ( Time4 = time.Date(2023, 10, 21, 10, 0, 0, 0, time.UTC) ExchangeEmailInboxPath = mustPathRep("tenant-id/exchange/user-id/email/Inbox", false) - ExchangeEmailBasePath = ExchangeEmailInboxPath.mustAppend("subfolder", false) - ExchangeEmailBasePath2 = ExchangeEmailInboxPath.mustAppend("othersubfolder/", false) - ExchangeEmailBasePath3 = ExchangeEmailBasePath2.mustAppend("subsubfolder", false) - ExchangeEmailItemPath1 = ExchangeEmailBasePath.mustAppend(ItemName1, true) - ExchangeEmailItemPath2 = ExchangeEmailBasePath2.mustAppend(ItemName2, true) - ExchangeEmailItemPath3 = ExchangeEmailBasePath3.mustAppend(ItemName3, true) + ExchangeEmailBasePath = ExchangeEmailInboxPath.MustAppend("subfolder", false) + ExchangeEmailBasePath2 = ExchangeEmailInboxPath.MustAppend("othersubfolder/", false) + ExchangeEmailBasePath3 = ExchangeEmailBasePath2.MustAppend("subsubfolder", false) + ExchangeEmailItemPath1 = ExchangeEmailBasePath.MustAppend(ItemName1, true) + ExchangeEmailItemPath2 = ExchangeEmailBasePath2.MustAppend(ItemName2, true) + ExchangeEmailItemPath3 = ExchangeEmailBasePath3.MustAppend(ItemName3, true) ExchangeEmailItems = []details.Entry{ { @@ -151,7 +151,7 @@ var ( ShortRef: ExchangeEmailItemPath1.RR.ShortRef(), ParentRef: ExchangeEmailItemPath1.RR.ToBuilder().Dir().ShortRef(), ItemRef: ExchangeEmailItemPath1.ItemLocation(), - LocationRef: ExchangeEmailItemPath1.loc.String(), + LocationRef: ExchangeEmailItemPath1.Loc.String(), ItemInfo: details.ItemInfo{ Exchange: &details.ExchangeInfo{ ItemType: details.ExchangeMail, @@ -166,7 +166,7 @@ var ( ShortRef: ExchangeEmailItemPath2.RR.ShortRef(), ParentRef: ExchangeEmailItemPath2.RR.ToBuilder().Dir().ShortRef(), ItemRef: ExchangeEmailItemPath2.ItemLocation(), - LocationRef: ExchangeEmailItemPath2.loc.String(), + LocationRef: ExchangeEmailItemPath2.Loc.String(), ItemInfo: details.ItemInfo{ Exchange: &details.ExchangeInfo{ ItemType: details.ExchangeMail, @@ -181,7 +181,7 @@ var ( ShortRef: ExchangeEmailItemPath3.RR.ShortRef(), ParentRef: ExchangeEmailItemPath3.RR.ToBuilder().Dir().ShortRef(), ItemRef: ExchangeEmailItemPath3.ItemLocation(), - LocationRef: ExchangeEmailItemPath3.loc.String(), + LocationRef: ExchangeEmailItemPath3.Loc.String(), ItemInfo: details.ItemInfo{ Exchange: &details.ExchangeInfo{ ItemType: details.ExchangeMail, @@ -194,10 +194,10 @@ var ( } ExchangeContactsRootPath = mustPathRep("tenant-id/exchange/user-id/contacts/contacts", false) - ExchangeContactsBasePath = ExchangeContactsRootPath.mustAppend("contacts", false) - ExchangeContactsBasePath2 = ExchangeContactsRootPath.mustAppend("morecontacts", false) - ExchangeContactsItemPath1 = ExchangeContactsBasePath.mustAppend(ItemName1, true) - ExchangeContactsItemPath2 = ExchangeContactsBasePath2.mustAppend(ItemName2, true) + ExchangeContactsBasePath = ExchangeContactsRootPath.MustAppend("contacts", false) + ExchangeContactsBasePath2 = ExchangeContactsRootPath.MustAppend("morecontacts", false) + ExchangeContactsItemPath1 = ExchangeContactsBasePath.MustAppend(ItemName1, true) + ExchangeContactsItemPath2 = ExchangeContactsBasePath2.MustAppend(ItemName2, true) ExchangeContactsItems = []details.Entry{ { @@ -205,7 +205,7 @@ var ( ShortRef: ExchangeContactsItemPath1.RR.ShortRef(), ParentRef: ExchangeContactsItemPath1.RR.ToBuilder().Dir().ShortRef(), ItemRef: ExchangeContactsItemPath1.ItemLocation(), - LocationRef: ExchangeContactsItemPath1.loc.String(), + LocationRef: ExchangeContactsItemPath1.Loc.String(), ItemInfo: details.ItemInfo{ Exchange: &details.ExchangeInfo{ ItemType: details.ExchangeContact, @@ -218,7 +218,7 @@ var ( ShortRef: ExchangeContactsItemPath2.RR.ShortRef(), ParentRef: ExchangeContactsItemPath2.RR.ToBuilder().Dir().ShortRef(), ItemRef: ExchangeContactsItemPath2.ItemLocation(), - LocationRef: ExchangeContactsItemPath2.loc.String(), + LocationRef: ExchangeContactsItemPath2.Loc.String(), ItemInfo: details.ItemInfo{ Exchange: &details.ExchangeInfo{ ItemType: details.ExchangeContact, @@ -228,11 +228,10 @@ var ( }, } - ExchangeEventsRootPath = mustPathRep("tenant-id/exchange/user-id/events/holidays", false) - ExchangeEventsBasePath = ExchangeEventsRootPath.mustAppend("holidays", false) - ExchangeEventsBasePath2 = ExchangeEventsRootPath.mustAppend("moreholidays", false) - ExchangeEventsItemPath1 = ExchangeEventsBasePath.mustAppend(ItemName1, true) - ExchangeEventsItemPath2 = ExchangeEventsBasePath2.mustAppend(ItemName2, true) + ExchangeEventsBasePath = mustPathRep("tenant-id/exchange/user-id/events/holidays", false) + ExchangeEventsBasePath2 = mustPathRep("tenant-id/exchange/user-id/events/moreholidays", false) + ExchangeEventsItemPath1 = ExchangeEventsBasePath.MustAppend(ItemName1, true) + ExchangeEventsItemPath2 = ExchangeEventsBasePath2.MustAppend(ItemName2, true) ExchangeEventsItems = []details.Entry{ { @@ -240,7 +239,7 @@ var ( ShortRef: ExchangeEventsItemPath1.RR.ShortRef(), ParentRef: ExchangeEventsItemPath1.RR.ToBuilder().Dir().ShortRef(), ItemRef: ExchangeEventsItemPath1.ItemLocation(), - LocationRef: ExchangeEventsItemPath1.loc.String(), + LocationRef: ExchangeEventsItemPath1.Loc.String(), ItemInfo: details.ItemInfo{ Exchange: &details.ExchangeInfo{ ItemType: details.ExchangeEvent, @@ -256,7 +255,7 @@ var ( ShortRef: ExchangeEventsItemPath2.RR.ShortRef(), ParentRef: ExchangeEventsItemPath2.RR.ToBuilder().Dir().ShortRef(), ItemRef: ExchangeEventsItemPath2.ItemLocation(), - LocationRef: ExchangeEventsItemPath2.loc.String(), + LocationRef: ExchangeEventsItemPath2.Loc.String(), ItemInfo: details.ItemInfo{ Exchange: &details.ExchangeInfo{ ItemType: details.ExchangeEvent, @@ -270,17 +269,17 @@ var ( } OneDriveRootPath = mustPathRep("tenant-id/onedrive/user-id/files/drives/foo/root:", false) - OneDriveFolderPath = OneDriveRootPath.mustAppend("folder", false) - OneDriveBasePath1 = OneDriveFolderPath.mustAppend("a", false) - OneDriveBasePath2 = OneDriveFolderPath.mustAppend("b", false) + OneDriveFolderPath = OneDriveRootPath.MustAppend("folder", false) + OneDriveBasePath1 = OneDriveFolderPath.MustAppend("a", false) + OneDriveBasePath2 = OneDriveFolderPath.MustAppend("b", false) - OneDriveItemPath1 = OneDriveFolderPath.mustAppend(ItemName1, true) - OneDriveItemPath2 = OneDriveBasePath1.mustAppend(ItemName2, true) - OneDriveItemPath3 = OneDriveBasePath2.mustAppend(ItemName3, true) + OneDriveItemPath1 = OneDriveFolderPath.MustAppend(ItemName1, true) + OneDriveItemPath2 = OneDriveBasePath1.MustAppend(ItemName2, true) + OneDriveItemPath3 = OneDriveBasePath2.MustAppend(ItemName3, true) - OneDriveFolderFolder = OneDriveFolderPath.loc.PopFront().String() - OneDriveParentFolder1 = OneDriveBasePath1.loc.PopFront().String() - OneDriveParentFolder2 = OneDriveBasePath2.loc.PopFront().String() + OneDriveFolderFolder = OneDriveFolderPath.Loc.PopFront().String() + OneDriveParentFolder1 = OneDriveBasePath1.Loc.PopFront().String() + OneDriveParentFolder2 = OneDriveBasePath2.Loc.PopFront().String() OneDriveItems = []details.Entry{ { @@ -288,7 +287,7 @@ var ( ShortRef: OneDriveItemPath1.RR.ShortRef(), ParentRef: OneDriveItemPath1.RR.ToBuilder().Dir().ShortRef(), ItemRef: OneDriveItemPath1.ItemLocation(), - LocationRef: OneDriveItemPath1.loc.String(), + LocationRef: OneDriveItemPath1.Loc.String(), ItemInfo: details.ItemInfo{ OneDrive: &details.OneDriveInfo{ ItemType: details.OneDriveItem, @@ -306,7 +305,7 @@ var ( ShortRef: OneDriveItemPath2.RR.ShortRef(), ParentRef: OneDriveItemPath2.RR.ToBuilder().Dir().ShortRef(), ItemRef: OneDriveItemPath2.ItemLocation(), - LocationRef: OneDriveItemPath2.loc.String(), + LocationRef: OneDriveItemPath2.Loc.String(), ItemInfo: details.ItemInfo{ OneDrive: &details.OneDriveInfo{ ItemType: details.OneDriveItem, @@ -324,7 +323,7 @@ var ( ShortRef: OneDriveItemPath3.RR.ShortRef(), ParentRef: OneDriveItemPath3.RR.ToBuilder().Dir().ShortRef(), ItemRef: OneDriveItemPath3.ItemLocation(), - LocationRef: OneDriveItemPath3.loc.String(), + LocationRef: OneDriveItemPath3.Loc.String(), ItemInfo: details.ItemInfo{ OneDrive: &details.OneDriveInfo{ ItemType: details.OneDriveItem, @@ -340,17 +339,17 @@ var ( } SharePointRootPath = mustPathRep("tenant-id/sharepoint/site-id/libraries/drives/foo/root:", false) - SharePointLibraryPath = SharePointRootPath.mustAppend("library", false) - SharePointBasePath1 = SharePointLibraryPath.mustAppend("a", false) - SharePointBasePath2 = SharePointLibraryPath.mustAppend("b", false) + SharePointLibraryPath = SharePointRootPath.MustAppend("library", false) + SharePointBasePath1 = SharePointLibraryPath.MustAppend("a", false) + SharePointBasePath2 = SharePointLibraryPath.MustAppend("b", false) - SharePointLibraryItemPath1 = SharePointLibraryPath.mustAppend(ItemName1, true) - SharePointLibraryItemPath2 = SharePointBasePath1.mustAppend(ItemName2, true) - SharePointLibraryItemPath3 = SharePointBasePath2.mustAppend(ItemName3, true) + SharePointLibraryItemPath1 = SharePointLibraryPath.MustAppend(ItemName1, true) + SharePointLibraryItemPath2 = SharePointBasePath1.MustAppend(ItemName2, true) + SharePointLibraryItemPath3 = SharePointBasePath2.MustAppend(ItemName3, true) - SharePointLibraryFolder = SharePointLibraryPath.loc.PopFront().String() - SharePointParentLibrary1 = SharePointBasePath1.loc.PopFront().String() - SharePointParentLibrary2 = SharePointBasePath2.loc.PopFront().String() + SharePointLibraryFolder = SharePointLibraryPath.Loc.PopFront().String() + SharePointParentLibrary1 = SharePointBasePath1.Loc.PopFront().String() + SharePointParentLibrary2 = SharePointBasePath2.Loc.PopFront().String() SharePointLibraryItems = []details.Entry{ { @@ -358,7 +357,7 @@ var ( ShortRef: SharePointLibraryItemPath1.RR.ShortRef(), ParentRef: SharePointLibraryItemPath1.RR.ToBuilder().Dir().ShortRef(), ItemRef: SharePointLibraryItemPath1.ItemLocation(), - LocationRef: SharePointLibraryItemPath1.loc.String(), + LocationRef: SharePointLibraryItemPath1.Loc.String(), ItemInfo: details.ItemInfo{ SharePoint: &details.SharePointInfo{ ItemType: details.SharePointLibrary, @@ -376,7 +375,7 @@ var ( ShortRef: SharePointLibraryItemPath2.RR.ShortRef(), ParentRef: SharePointLibraryItemPath2.RR.ToBuilder().Dir().ShortRef(), ItemRef: SharePointLibraryItemPath2.ItemLocation(), - LocationRef: SharePointLibraryItemPath2.loc.String(), + LocationRef: SharePointLibraryItemPath2.Loc.String(), ItemInfo: details.ItemInfo{ SharePoint: &details.SharePointInfo{ ItemType: details.SharePointLibrary, @@ -394,7 +393,7 @@ var ( ShortRef: SharePointLibraryItemPath3.RR.ShortRef(), ParentRef: SharePointLibraryItemPath3.RR.ToBuilder().Dir().ShortRef(), ItemRef: SharePointLibraryItemPath3.ItemLocation(), - LocationRef: SharePointLibraryItemPath3.loc.String(), + LocationRef: SharePointLibraryItemPath3.Loc.String(), ItemInfo: details.ItemInfo{ SharePoint: &details.SharePointInfo{ ItemType: details.SharePointLibrary, diff --git a/src/pkg/path/drive.go b/src/pkg/path/drive.go index b073ff125..033f9934b 100644 --- a/src/pkg/path/drive.go +++ b/src/pkg/path/drive.go @@ -38,3 +38,13 @@ func GetDriveFolderPath(p Path) (string, error) { return Builder{}.Append(drivePath.Folders...).String(), nil } + +// BuildDriveLocation takes a driveID and a set of unescaped element names, +// including the root folder, and returns a *path.Builder containing the +// canonical path representation for the drive path. +func BuildDriveLocation( + driveID string, + unescapedElements ...string, +) *Builder { + return Builder{}.Append("drives", driveID).Append(unescapedElements...) +} diff --git a/src/pkg/path/drive_test.go b/src/pkg/path/drive_test.go index cddd050bf..bdbf09d9c 100644 --- a/src/pkg/path/drive_test.go +++ b/src/pkg/path/drive_test.go @@ -1,6 +1,7 @@ package path_test import ( + "strings" "testing" "github.com/alcionai/clues" @@ -63,3 +64,49 @@ func (suite *OneDrivePathSuite) Test_ToOneDrivePath() { }) } } + +func (suite *OneDrivePathSuite) TestFormatDriveFolders() { + const ( + driveID = "some-drive-id" + drivePrefix = "drives/" + driveID + ) + + table := []struct { + name string + input []string + expected string + }{ + { + name: "normal", + input: []string{ + "root:", + "foo", + "bar", + }, + expected: strings.Join( + append([]string{drivePrefix}, "root:", "foo", "bar"), + "/"), + }, + { + name: "has character that would be escaped", + input: []string{ + "root:", + "foo/", + "bar", + }, + // Element "foo/" should end up escaped in the string output. + expected: strings.Join( + append([]string{drivePrefix}, "root:", `foo\/`, "bar"), + "/"), + }, + } + + for _, test := range table { + suite.Run(test.name, func() { + assert.Equal( + suite.T(), + test.expected, + path.BuildDriveLocation(driveID, test.input...).String()) + }) + } +} diff --git a/src/pkg/selectors/selectors_reduce_test.go b/src/pkg/selectors/selectors_reduce_test.go index b72a4c65e..c57cde409 100644 --- a/src/pkg/selectors/selectors_reduce_test.go +++ b/src/pkg/selectors/selectors_reduce_test.go @@ -249,18 +249,6 @@ func (suite *SelectorReduceSuite) TestReduce() { }, expected: []details.Entry{testdata.ExchangeEventsItems[0]}, }, - { - name: "ExchangeEventsByFolderRoot", - selFunc: func() selectors.Reducer { - sel := selectors.NewExchangeRestore(selectors.Any()) - sel.Include(sel.EventCalendars( - []string{testdata.ExchangeEventsRootPath.FolderLocation()}, - )) - - return sel - }, - expected: testdata.ExchangeEventsItems, - }, } for _, test := range table {