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 {