diff --git a/src/internal/m365/collection/drive/collections_test.go b/src/internal/m365/collection/drive/collections_test.go index 30c756eb4..6421447a7 100644 --- a/src/internal/m365/collection/drive/collections_test.go +++ b/src/internal/m365/collection/drive/collections_test.go @@ -173,9 +173,8 @@ func malwareItem( } func driveRootItem() models.DriveItemable { - name := rootName item := models.NewDriveItem() - item.SetName(&name) + item.SetName(ptr.To(rootName)) item.SetId(ptr.To(rootID)) item.SetRoot(models.NewRoot()) item.SetFolder(models.NewFolder()) diff --git a/src/internal/m365/collection/drive/collections_tree.go b/src/internal/m365/collection/drive/collections_tree.go index 96a1463fc..f02886ffe 100644 --- a/src/internal/m365/collection/drive/collections_tree.go +++ b/src/internal/m365/collection/drive/collections_tree.go @@ -171,7 +171,12 @@ func (c *Collections) makeDriveCollections( return nil, nil, pagers.DeltaUpdate{}, clues.Wrap(err, "generating backup tree prefix") } - tree := newFolderyMcFolderFace(ppfx) + root, err := c.handler.GetRootFolder(ctx, ptr.Val(drv.GetId())) + if err != nil { + return nil, nil, pagers.DeltaUpdate{}, clues.Wrap(err, "getting root folder") + } + + tree := newFolderyMcFolderFace(ppfx, ptr.Val(root.GetId())) counter.Add(count.PrevPaths, int64(len(prevPaths))) diff --git a/src/internal/m365/collection/drive/collections_tree_test.go b/src/internal/m365/collection/drive/collections_tree_test.go index 9786b728f..a4c381565 100644 --- a/src/internal/m365/collection/drive/collections_tree_test.go +++ b/src/internal/m365/collection/drive/collections_tree_test.go @@ -585,7 +585,7 @@ func (suite *CollectionsTreeUnitSuite) TestCollections_PopulateTree() { }{ { name: "nil page", - tree: newFolderyMcFolderFace(nil), + tree: newFolderyMcFolderFace(nil, rootID), enumerator: mock.EnumerateItemsDeltaByDrive{ DrivePagers: map[string]*mock.DriveItemsDeltaPager{ id(drive): { @@ -608,7 +608,7 @@ func (suite *CollectionsTreeUnitSuite) TestCollections_PopulateTree() { }, { name: "root only", - tree: newFolderyMcFolderFace(nil), + tree: newFolderyMcFolderFace(nil, rootID), enumerator: mock.EnumerateItemsDeltaByDrive{ DrivePagers: map[string]*mock.DriveItemsDeltaPager{ id(drive): { @@ -637,7 +637,7 @@ func (suite *CollectionsTreeUnitSuite) TestCollections_PopulateTree() { }, { name: "root only on two pages", - tree: newFolderyMcFolderFace(nil), + tree: newFolderyMcFolderFace(nil, rootID), enumerator: mock.EnumerateItemsDeltaByDrive{ DrivePagers: map[string]*mock.DriveItemsDeltaPager{ id(drive): { @@ -666,7 +666,7 @@ func (suite *CollectionsTreeUnitSuite) TestCollections_PopulateTree() { }, { name: "many folders in a hierarchy across multiple pages", - tree: newFolderyMcFolderFace(nil), + tree: newFolderyMcFolderFace(nil, rootID), enumerator: mock.EnumerateItemsDeltaByDrive{ DrivePagers: map[string]*mock.DriveItemsDeltaPager{ id(drive): { @@ -703,7 +703,7 @@ func (suite *CollectionsTreeUnitSuite) TestCollections_PopulateTree() { }, { name: "many folders with files", - tree: newFolderyMcFolderFace(nil), + tree: newFolderyMcFolderFace(nil, rootID), enumerator: mock.EnumerateItemsDeltaByDrive{ DrivePagers: map[string]*mock.DriveItemsDeltaPager{ id(drive): { @@ -751,7 +751,7 @@ func (suite *CollectionsTreeUnitSuite) TestCollections_PopulateTree() { // technically you won't see this behavior from graph deltas, since deletes always // precede creates/updates. But it's worth checking that we can handle it anyways. name: "create, delete on next page", - tree: newFolderyMcFolderFace(nil), + tree: newFolderyMcFolderFace(nil, rootID), enumerator: mock.EnumerateItemsDeltaByDrive{ DrivePagers: map[string]*mock.DriveItemsDeltaPager{ id(drive): { @@ -870,7 +870,7 @@ func (suite *CollectionsTreeUnitSuite) TestCollections_PopulateTree() { }, { name: "hit folder limit during enumeration", - tree: newFolderyMcFolderFace(nil), + tree: newFolderyMcFolderFace(nil, rootID), enumerator: mock.EnumerateItemsDeltaByDrive{ DrivePagers: map[string]*mock.DriveItemsDeltaPager{ id(drive): { @@ -1305,7 +1305,7 @@ func (suite *CollectionsTreeUnitSuite) TestCollections_AddFolderToTree() { }, { name: "tombstone new folder in unpopulated tree", - tree: newFolderyMcFolderFace(nil), + tree: newFolderyMcFolderFace(nil, rootID), folder: del, limiter: newPagerLimiter(control.DefaultOptions()), expect: expected{ @@ -1553,7 +1553,7 @@ func (suite *CollectionsTreeUnitSuite) TestCollections_EnumeratePageOfItems_file }, { name: "one file in a folder", - tree: newFolderyMcFolderFace(nil), + tree: newFolderyMcFolderFace(nil, rootID), page: pageItems( driveItem(id(folder), name(folder), parentDir(), rootID, isFolder), driveItem(id(file), name(file), parentDir(name(folder)), id(folder), isFile)), diff --git a/src/internal/m365/collection/drive/delta_tree.go b/src/internal/m365/collection/drive/delta_tree.go index 8bfafce44..4bfad289b 100644 --- a/src/internal/m365/collection/drive/delta_tree.go +++ b/src/internal/m365/collection/drive/delta_tree.go @@ -6,7 +6,6 @@ import ( "github.com/alcionai/clues" - odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts" "github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/path" ) @@ -23,6 +22,10 @@ type folderyMcFolderFace struct { // new, moved, and notMoved root root *nodeyMcNodeFace + // the ID of the actual root folder. + // required to ensure correct population of the root node. + rootID string + // the majority of operations we perform can be handled with // a folder ID lookup instead of re-walking the entire tree. // Ex: adding a new file to its parent folder. @@ -45,9 +48,11 @@ type folderyMcFolderFace struct { func newFolderyMcFolderFace( prefix path.Path, + rootID string, ) *folderyMcFolderFace { return &folderyMcFolderFace{ prefix: prefix, + rootID: rootID, folderIDToNode: map[string]*nodeyMcNodeFace{}, tombstones: map[string]*nodeyMcNodeFace{}, fileIDToParentID: map[string]string{}, @@ -150,17 +155,12 @@ func (face *folderyMcFolderFace) setFolder( return clues.NewWC(ctx, "missing folder name") } - // drive doesn't normally allow the `:` character in folder names. - // so `root:` is, by default, the only folder that can match this - // name. That makes this check a little bit brittle, but generally - // reliable, since we should always see the root first and can rely - // on the naming structure. - if len(parentID) == 0 && name != odConsts.RootPathDir { + if len(parentID) == 0 && id != face.rootID { return clues.NewWC(ctx, "non-root folder missing parent id") } // only set the root node once. - if name == odConsts.RootPathDir { + if id == face.rootID { if face.root == nil { root := newNodeyMcNodeFace(nil, id, name, isPackage) face.root = root diff --git a/src/internal/m365/collection/drive/delta_tree_test.go b/src/internal/m365/collection/drive/delta_tree_test.go index 161f1e6b4..738f9d8f8 100644 --- a/src/internal/m365/collection/drive/delta_tree_test.go +++ b/src/internal/m365/collection/drive/delta_tree_test.go @@ -20,7 +20,7 @@ import ( var loc = path.NewElements("root:/foo/bar/baz/qux/fnords/smarf/voi/zumba/bangles/howdyhowdyhowdy") func treeWithRoot() *folderyMcFolderFace { - tree := newFolderyMcFolderFace(nil) + tree := newFolderyMcFolderFace(nil, rootID) rootey := newNodeyMcNodeFace(nil, rootID, rootName, false) tree.root = rootey tree.folderIDToNode[rootID] = rootey @@ -38,13 +38,13 @@ func treeWithTombstone() *folderyMcFolderFace { func treeWithFolders() *folderyMcFolderFace { tree := treeWithRoot() - o := newNodeyMcNodeFace(tree.root, idx(folder, "parent"), namex(folder, "parent"), true) - tree.folderIDToNode[o.id] = o - tree.root.children[o.id] = o + parent := newNodeyMcNodeFace(tree.root, idx(folder, "parent"), namex(folder, "parent"), true) + tree.folderIDToNode[parent.id] = parent + tree.root.children[parent.id] = parent - f := newNodeyMcNodeFace(o, id(folder), name(folder), false) + f := newNodeyMcNodeFace(parent, id(folder), name(folder), false) tree.folderIDToNode[f.id] = f - o.children[f.id] = f + parent.children[f.id] = f return tree } @@ -102,7 +102,7 @@ func (suite *DeltaTreeUnitSuite) TestNewFolderyMcFolderFace() { require.NoError(t, err, clues.ToCore(err)) - folderFace := newFolderyMcFolderFace(p) + folderFace := newFolderyMcFolderFace(p, rootID) assert.Equal(t, p, folderFace.prefix) assert.Nil(t, folderFace.root) assert.NotNil(t, folderFace.folderIDToNode) @@ -144,7 +144,7 @@ func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_SetFolder() { }{ { tname: "add root", - tree: newFolderyMcFolderFace(nil), + tree: newFolderyMcFolderFace(nil, rootID), id: rootID, name: rootName, isPackage: true, @@ -272,7 +272,7 @@ func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_AddTombstone() { { name: "add tombstone", id: id(folder), - tree: newFolderyMcFolderFace(nil), + tree: newFolderyMcFolderFace(nil, rootID), expectErr: assert.NoError, }, { @@ -283,7 +283,7 @@ func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_AddTombstone() { }, { name: "missing ID", - tree: newFolderyMcFolderFace(nil), + tree: newFolderyMcFolderFace(nil, rootID), expectErr: assert.Error, }, { diff --git a/src/internal/m365/collection/drive/handlers.go b/src/internal/m365/collection/drive/handlers.go index 9d3ca774d..1a2b98479 100644 --- a/src/internal/m365/collection/drive/handlers.go +++ b/src/internal/m365/collection/drive/handlers.go @@ -39,6 +39,7 @@ type BackupHandler interface { api.Getter GetItemPermissioner GetItemer + GetRootFolderer NewDrivePagerer EnumerateDriveItemsDeltaer diff --git a/src/internal/m365/collection/drive/site_handler.go b/src/internal/m365/collection/drive/site_handler.go index b489ad1e0..11adf8fa6 100644 --- a/src/internal/m365/collection/drive/site_handler.go +++ b/src/internal/m365/collection/drive/site_handler.go @@ -182,6 +182,13 @@ func (h siteBackupHandler) EnumerateDriveItemsDelta( return h.ac.EnumerateDriveItemsDelta(ctx, driveID, prevDeltaLink, cc) } +func (h siteBackupHandler) GetRootFolder( + ctx context.Context, + driveID string, +) (models.DriveItemable, error) { + return h.ac.Drives().GetRootFolder(ctx, driveID) +} + // --------------------------------------------------------------------------- // Restore // --------------------------------------------------------------------------- diff --git a/src/internal/m365/collection/drive/user_drive_handler.go b/src/internal/m365/collection/drive/user_drive_handler.go index 784f8b471..56d10c67f 100644 --- a/src/internal/m365/collection/drive/user_drive_handler.go +++ b/src/internal/m365/collection/drive/user_drive_handler.go @@ -182,6 +182,13 @@ func (h userDriveBackupHandler) EnumerateDriveItemsDelta( return h.ac.EnumerateDriveItemsDelta(ctx, driveID, prevDeltaLink, cc) } +func (h userDriveBackupHandler) GetRootFolder( + ctx context.Context, + driveID string, +) (models.DriveItemable, error) { + return h.ac.Drives().GetRootFolder(ctx, driveID) +} + // --------------------------------------------------------------------------- // Restore // --------------------------------------------------------------------------- diff --git a/src/internal/m365/service/onedrive/mock/handlers.go b/src/internal/m365/service/onedrive/mock/handlers.go index e22be0803..157da4271 100644 --- a/src/internal/m365/service/onedrive/mock/handlers.go +++ b/src/internal/m365/service/onedrive/mock/handlers.go @@ -9,6 +9,7 @@ import ( "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/alcionai/corso/src/internal/common/idname" + "github.com/alcionai/corso/src/internal/common/ptr" odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts" "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/control" @@ -57,6 +58,18 @@ type BackupHandler[T any] struct { getCall int GetResps []*http.Response GetErrs []error + + RootFolder models.DriveItemable +} + +func stubRootFolder() models.DriveItemable { + item := models.NewDriveItem() + item.SetName(ptr.To(odConsts.RootPathDir)) + item.SetId(ptr.To(odConsts.RootID)) + item.SetRoot(models.NewRoot()) + item.SetFolder(models.NewFolder()) + + return item } func DefaultOneDriveBH(resourceOwner string) *BackupHandler[models.DriveItemable] { @@ -81,6 +94,7 @@ func DefaultOneDriveBH(resourceOwner string) *BackupHandler[models.DriveItemable LocationIDFn: defaultOneDriveLocationIDer, GetResps: []*http.Response{nil}, GetErrs: []error{clues.New("not defined")}, + RootFolder: stubRootFolder(), } } @@ -105,6 +119,7 @@ func DefaultSharePointBH(resourceOwner string) *BackupHandler[models.DriveItemab LocationIDFn: defaultSharePointLocationIDer, GetResps: []*http.Response{nil}, GetErrs: []error{clues.New("not defined")}, + RootFolder: stubRootFolder(), } } @@ -287,6 +302,10 @@ func (h BackupHandler[T]) IncludesDir(dir string) bool { selectors.OneDriveScope(scope).Matches(selectors.OneDriveFolder, dir) } +func (h BackupHandler[T]) GetRootFolder(context.Context, string) (models.DriveItemable, error) { + return h.RootFolder, nil +} + // --------------------------------------------------------------------------- // Get Itemer // ---------------------------------------------------------------------------