Set proper state and paths for moves in OneDrive (#2501)

## Description

Ties up the final piece of https://github.com/alcionai/corso/issues/2123. Add handling of moves for folder / files in delta response.

## Does this PR need a docs update or release note?

- [ ]  Yes, it's included
- [x] 🕐 Yes, but in a later PR
- [ ]  No 

## Type of change

<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Test
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

## Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* https://github.com/alcionai/corso/issues/2123

## Test Plan

<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
Abin Simon 2023-02-15 23:57:45 +05:30 committed by GitHub
parent 123aec2bd8
commit e60ae2351f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 440 additions and 76 deletions

View File

@ -151,6 +151,11 @@ func (oc Collection) PreviousPath() path.Path {
return oc.prevPath return oc.prevPath
} }
func (oc *Collection) SetFullPath(curPath path.Path) {
oc.folderPath = curPath
oc.state = data.StateOf(oc.prevPath, curPath)
}
func (oc Collection) State() data.CollectionState { func (oc Collection) State() data.CollectionState {
return oc.state return oc.state
} }

View File

@ -12,6 +12,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
@ -366,6 +367,54 @@ func (c *Collections) Get(
return collections, excludedItems, nil return collections, excludedItems, nil
} }
func updateCollectionPaths(
id string,
cmap map[string]data.BackupCollection,
curPath path.Path,
) (bool, error) {
var initialCurPath path.Path
col, found := cmap[id]
if found {
ocol, ok := col.(*Collection)
if !ok {
return found, clues.New("unable to cast onedrive collection")
}
initialCurPath = ocol.FullPath()
if initialCurPath.String() == curPath.String() {
return found, nil
}
ocol.SetFullPath(curPath)
}
if initialCurPath == nil {
return found, nil
}
for i, c := range cmap {
if i == id {
continue
}
ocol, ok := c.(*Collection)
if !ok {
return found, clues.New("unable to cast onedrive collection")
}
colPath := c.FullPath()
// Only updates if initialCurPath parent of colPath
updated := colPath.UpdateParent(initialCurPath, curPath)
if updated {
ocol.SetFullPath(colPath)
}
}
return found, nil
}
// UpdateCollections initializes and adds the provided drive items to Collections // UpdateCollections initializes and adds the provided drive items to Collections
// A new collection is created for every drive folder (or package). // A new collection is created for every drive folder (or package).
// oldPaths is the unchanged data that was loaded from the metadata file. // oldPaths is the unchanged data that was loaded from the metadata file.
@ -381,6 +430,11 @@ func (c *Collections) UpdateCollections(
invalidPrevDelta bool, invalidPrevDelta bool,
) error { ) error {
for _, item := range items { for _, item := range items {
var (
prevPath path.Path
prevCollectionPath path.Path
)
if item.GetRoot() != nil { if item.GetRoot() != nil {
// Skip the root item // Skip the root item
continue continue
@ -413,14 +467,21 @@ func (c *Collections) UpdateCollections(
switch { switch {
case item.GetFolder() != nil, item.GetPackage() != nil: case item.GetFolder() != nil, item.GetPackage() != nil:
prevPathStr, ok := oldPaths[*item.GetId()]
if ok {
prevPath, err = path.FromDataLayerPath(prevPathStr, false)
if err != nil {
return clues.Wrap(err, "invalid previous path").WithAll("path_string", prevPathStr)
}
}
if item.GetDeleted() != nil { if item.GetDeleted() != nil {
// Nested folders also return deleted delta results so we don't have to // Nested folders also return deleted delta results so we don't have to
// worry about doing a prefix search in the map to remove the subtree of // worry about doing a prefix search in the map to remove the subtree of
// the deleted folder/package. // the deleted folder/package.
delete(newPaths, *item.GetId()) delete(newPaths, *item.GetId())
prevColPath, ok := oldPaths[*item.GetId()] if prevPath == nil {
if !ok {
// It is possible that an item was created and // It is possible that an item was created and
// deleted between two delta invocations. In // deleted between two delta invocations. In
// that case, it will only produce a single // that case, it will only produce a single
@ -428,12 +489,6 @@ func (c *Collections) UpdateCollections(
continue continue
} }
prevPath, err := path.FromDataLayerPath(prevColPath, false)
if err != nil {
logger.Ctx(ctx).Errorw("invalid previous path for deleted item", "error", err)
return err
}
col := NewCollection( col := NewCollection(
c.itemClient, c.itemClient,
nil, nil,
@ -463,12 +518,34 @@ func (c *Collections) UpdateCollections(
// Moved folders don't cause delta results for any subfolders nested in // Moved folders don't cause delta results for any subfolders nested in
// them. We need to go through and update paths to handle that. We only // them. We need to go through and update paths to handle that. We only
// update newPaths so we don't accidentally clobber previous deletes. // update newPaths so we don't accidentally clobber previous deletes.
//
// TODO(ashmrtn): Since we're also getting notifications about folder
// moves we may need to handle updates to a path of a collection we've
// already created and partially populated.
updatePath(newPaths, *item.GetId(), folderPath.String()) updatePath(newPaths, *item.GetId(), folderPath.String())
found, err := updateCollectionPaths(*item.GetId(), c.CollectionMap, folderPath)
if err != nil {
return err
}
if !found {
// We only create collections for folder that are not
// new. This is so as to not create collections for
// new folders without any files within them.
if prevPath != nil {
col := NewCollection(
c.itemClient,
folderPath,
prevPath,
driveID,
c.service,
c.statusUpdater,
c.source,
c.ctrl,
invalidPrevDelta,
)
c.CollectionMap[*item.GetId()] = col
c.NumContainers++
}
}
if c.source != OneDriveSource { if c.source != OneDriveSource {
continue continue
} }
@ -476,8 +553,15 @@ func (c *Collections) UpdateCollections(
fallthrough fallthrough
case item.GetFile() != nil: case item.GetFile() != nil:
if item.GetDeleted() != nil { if !invalidPrevDelta && item.GetFile() != nil {
// Always add a file to the excluded list. If it was
// deleted, we want to avoid it. If it was
// renamed/moved/modified, we still have to drop the
// original one and download a fresh copy.
excluded[*item.GetId()] = struct{}{} excluded[*item.GetId()] = struct{}{}
}
if item.GetDeleted() != nil {
// Exchange counts items streamed through it which includes deletions so // Exchange counts items streamed through it which includes deletions so
// add that here too. // add that here too.
c.NumFiles++ c.NumFiles++
@ -486,17 +570,31 @@ func (c *Collections) UpdateCollections(
continue continue
} }
// TODO(ashmrtn): Figure what when an item was moved (maybe) and add it to oneDrivePath, err := path.ToOneDrivePath(collectionPath)
// the exclude list. if err != nil {
return clues.Wrap(err, "invalid path for backup")
}
if len(oneDrivePath.Folders) == 0 {
// path for root will never change
prevCollectionPath = collectionPath
} else {
prevCollectionPathStr, ok := oldPaths[collectionID]
if ok {
prevCollectionPath, err = path.FromDataLayerPath(prevCollectionPathStr, false)
if err != nil {
return clues.Wrap(err, "invalid previous path").WithAll("path_string", prevCollectionPathStr)
}
}
}
col, found := c.CollectionMap[collectionID] col, found := c.CollectionMap[collectionID]
if !found { if !found {
// TODO(ashmrtn): Compare old and new path and set collection state
// accordingly.
col = NewCollection( col = NewCollection(
c.itemClient, c.itemClient,
collectionPath, collectionPath,
nil, prevCollectionPath,
driveID, driveID,
c.service, c.service,
c.statusUpdater, c.statusUpdater,
@ -509,6 +607,10 @@ func (c *Collections) UpdateCollections(
c.NumContainers++ c.NumContainers++
} }
// TODO(meain): If a folder gets renamed/moved multiple
// times within a single delta response, we might end up
// storing the permissions multiple times. Switching the
// files to IDs should fix this.
collection := col.(*Collection) collection := col.(*Collection)
collection.Add(item) collection.Add(item)

View File

@ -33,20 +33,38 @@ type statePath struct {
func getExpectedStatePathGenerator( func getExpectedStatePathGenerator(
t *testing.T, t *testing.T,
tenant, user, base string, tenant, user, base string,
) func(data.CollectionState, string) statePath { ) func(data.CollectionState, ...string) statePath {
return func(state data.CollectionState, pth string) statePath { return func(state data.CollectionState, pths ...string) statePath {
p, err := GetCanonicalPath(base+pth, tenant, user, OneDriveSource)
require.NoError(t, err)
var ( var (
cp path.Path p1 path.Path
pp path.Path p2 path.Path
pp path.Path
cp path.Path
err error
) )
if state == data.NewState { if state != data.MovedState {
cp = p require.Len(t, pths, 1, "invalid number of paths to getExpectedStatePathGenerator")
} else { } else {
pp = p require.Len(t, pths, 2, "invalid number of paths to getExpectedStatePathGenerator")
p2, err = GetCanonicalPath(base+pths[1], tenant, user, OneDriveSource)
require.NoError(t, err)
}
p1, err = GetCanonicalPath(base+pths[0], tenant, user, OneDriveSource)
require.NoError(t, err)
switch state {
case data.NewState:
cp = p1
case data.NotMovedState:
cp = p1
pp = p1
case data.DeletedState:
pp = p1
case data.MovedState:
pp = p2
cp = p1
} }
return statePath{ return statePath{
@ -169,14 +187,14 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder, scope: anyFolder,
expect: assert.NoError, expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{ expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NewState, ""), "root": expectedStatePath(data.NotMovedState, ""),
}, },
expectedItemCount: 1, expectedItemCount: 1,
expectedFileCount: 1, expectedFileCount: 1,
expectedContainerCount: 1, expectedContainerCount: 1,
// Root folder is skipped since it's always present. // Root folder is skipped since it's always present.
expectedMetadataPaths: map[string]string{}, expectedMetadataPaths: map[string]string{},
expectedExcludes: map[string]struct{}{}, expectedExcludes: map[string]struct{}{"file": {}},
}, },
{ {
testCase: "Single Folder", testCase: "Single Folder",
@ -188,7 +206,7 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder, scope: anyFolder,
expect: assert.NoError, expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{ expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NewState, ""), "root": expectedStatePath(data.NotMovedState, ""),
}, },
expectedMetadataPaths: map[string]string{ expectedMetadataPaths: map[string]string{
"folder": expectedPath("/folder"), "folder": expectedPath("/folder"),
@ -207,7 +225,7 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder, scope: anyFolder,
expect: assert.NoError, expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{ expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NewState, ""), "root": expectedStatePath(data.NotMovedState, ""),
}, },
expectedMetadataPaths: map[string]string{ expectedMetadataPaths: map[string]string{
"package": expectedPath("/package"), "package": expectedPath("/package"),
@ -230,7 +248,7 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder, scope: anyFolder,
expect: assert.NoError, expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{ expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NewState, ""), "root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.NewState, folder), "folder": expectedStatePath(data.NewState, folder),
"package": expectedStatePath(data.NewState, pkg), "package": expectedStatePath(data.NewState, pkg),
}, },
@ -241,7 +259,7 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
"folder": expectedPath("/folder"), "folder": expectedPath("/folder"),
"package": expectedPath("/package"), "package": expectedPath("/package"),
}, },
expectedExcludes: map[string]struct{}{}, expectedExcludes: map[string]struct{}{"fileInRoot": {}, "fileInFolder": {}, "fileInPackage": {}},
}, },
{ {
testCase: "contains folder selector", testCase: "contains folder selector",
@ -273,7 +291,7 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
"subfolder": expectedPath("/folder/subfolder"), "subfolder": expectedPath("/folder/subfolder"),
"folder2": expectedPath("/folder/subfolder/folder"), "folder2": expectedPath("/folder/subfolder/folder"),
}, },
expectedExcludes: map[string]struct{}{}, expectedExcludes: map[string]struct{}{"fileInFolder": {}, "fileInFolder2": {}},
}, },
{ {
testCase: "prefix subfolder selector", testCase: "prefix subfolder selector",
@ -302,7 +320,7 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
expectedMetadataPaths: map[string]string{ expectedMetadataPaths: map[string]string{
"folder2": expectedPath("/folder/subfolder/folder"), "folder2": expectedPath("/folder/subfolder/folder"),
}, },
expectedExcludes: map[string]struct{}{}, expectedExcludes: map[string]struct{}{"fileInFolder2": {}},
}, },
{ {
testCase: "match subfolder selector", testCase: "match subfolder selector",
@ -327,7 +345,7 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
expectedContainerCount: 1, expectedContainerCount: 1,
// No child folders for subfolder so nothing here. // No child folders for subfolder so nothing here.
expectedMetadataPaths: map[string]string{}, expectedMetadataPaths: map[string]string{},
expectedExcludes: map[string]struct{}{}, expectedExcludes: map[string]struct{}{"fileInSubfolder": {}},
}, },
{ {
testCase: "not moved folder tree", testCase: "not moved folder tree",
@ -342,11 +360,12 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder, scope: anyFolder,
expect: assert.NoError, expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{ expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NewState, ""), "root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.NotMovedState, "/folder"),
}, },
expectedItemCount: 1, expectedItemCount: 1,
expectedFileCount: 0, expectedFileCount: 0,
expectedContainerCount: 1, expectedContainerCount: 2,
expectedMetadataPaths: map[string]string{ expectedMetadataPaths: map[string]string{
"folder": expectedPath("/folder"), "folder": expectedPath("/folder"),
"subfolder": expectedPath("/folder/subfolder"), "subfolder": expectedPath("/folder/subfolder"),
@ -366,17 +385,87 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder, scope: anyFolder,
expect: assert.NoError, expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{ expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NewState, ""), "root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.MovedState, "/folder", "/a-folder"),
}, },
expectedItemCount: 1, expectedItemCount: 1,
expectedFileCount: 0, expectedFileCount: 0,
expectedContainerCount: 1, expectedContainerCount: 2,
expectedMetadataPaths: map[string]string{ expectedMetadataPaths: map[string]string{
"folder": expectedPath("/folder"), "folder": expectedPath("/folder"),
"subfolder": expectedPath("/folder/subfolder"), "subfolder": expectedPath("/folder/subfolder"),
}, },
expectedExcludes: map[string]struct{}{}, expectedExcludes: map[string]struct{}{},
}, },
{
testCase: "moved folder tree with file with file first",
items: []models.DriveItemable{
driveRootItem("root"),
driveItem("file", "file", testBaseDrivePath+"/folder", "folder", true, false, false),
driveItem("folder", "folder", testBaseDrivePath, "root", false, true, false),
},
inputFolderMap: map[string]string{
"folder": expectedPath("/folder"),
},
scope: anyFolder,
expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.NotMovedState, "/folder"),
},
expectedItemCount: 2,
expectedFileCount: 1,
expectedContainerCount: 2,
expectedMetadataPaths: map[string]string{
"folder": expectedPath("/folder"),
},
expectedExcludes: map[string]struct{}{"file": {}},
},
{
testCase: "moved folder tree with file no previous",
items: []models.DriveItemable{
driveRootItem("root"),
driveItem("folder", "folder", testBaseDrivePath, "root", false, true, false),
driveItem("file", "file", testBaseDrivePath+"/folder", "folder", true, false, false),
driveItem("folder", "folder2", testBaseDrivePath, "root", false, true, false),
},
inputFolderMap: map[string]string{},
scope: anyFolder,
expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.NewState, "/folder2"),
},
expectedItemCount: 3, // permissions gets saved twice for folder
expectedFileCount: 1,
expectedContainerCount: 2,
expectedMetadataPaths: map[string]string{
"folder": expectedPath("/folder2"),
},
expectedExcludes: map[string]struct{}{"file": {}},
},
{
testCase: "moved folder tree with file no previous 1",
items: []models.DriveItemable{
driveRootItem("root"),
driveItem("folder", "folder", testBaseDrivePath, "root", false, true, false),
driveItem("file", "file", testBaseDrivePath+"/folder", "folder", true, false, false),
},
inputFolderMap: map[string]string{},
scope: anyFolder,
expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.NewState, "/folder"),
},
expectedItemCount: 2,
expectedFileCount: 1,
expectedContainerCount: 2,
expectedMetadataPaths: map[string]string{
"folder": expectedPath("/folder"),
},
expectedExcludes: map[string]struct{}{"file": {}},
},
{ {
testCase: "moved folder tree and subfolder 1", testCase: "moved folder tree and subfolder 1",
items: []models.DriveItemable{ items: []models.DriveItemable{
@ -391,11 +480,13 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder, scope: anyFolder,
expect: assert.NoError, expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{ expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NewState, ""), "root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.MovedState, "/folder", "/a-folder"),
"subfolder": expectedStatePath(data.MovedState, "/subfolder", "/a-folder/subfolder"),
}, },
expectedItemCount: 2, expectedItemCount: 2,
expectedFileCount: 0, expectedFileCount: 0,
expectedContainerCount: 1, expectedContainerCount: 3,
expectedMetadataPaths: map[string]string{ expectedMetadataPaths: map[string]string{
"folder": expectedPath("/folder"), "folder": expectedPath("/folder"),
"subfolder": expectedPath("/subfolder"), "subfolder": expectedPath("/subfolder"),
@ -416,17 +507,59 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder, scope: anyFolder,
expect: assert.NoError, expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{ expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NewState, ""), "root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.MovedState, "/folder", "/a-folder"),
"subfolder": expectedStatePath(data.MovedState, "/subfolder", "/a-folder/subfolder"),
}, },
expectedItemCount: 2, expectedItemCount: 2,
expectedFileCount: 0, expectedFileCount: 0,
expectedContainerCount: 1, expectedContainerCount: 3,
expectedMetadataPaths: map[string]string{ expectedMetadataPaths: map[string]string{
"folder": expectedPath("/folder"), "folder": expectedPath("/folder"),
"subfolder": expectedPath("/subfolder"), "subfolder": expectedPath("/subfolder"),
}, },
expectedExcludes: map[string]struct{}{}, expectedExcludes: map[string]struct{}{},
}, },
{
testCase: "move subfolder when moving parent",
items: []models.DriveItemable{
driveRootItem("root"),
driveItem("folder2", "folder2", testBaseDrivePath, "root", false, true, false),
driveItem("itemInFolder2", "itemInFolder2", testBaseDrivePath+"/folder2", "folder2", true, false, false),
driveItem("subfolder", "subfolder", testBaseDrivePath+"/a-folder", "folder", false, true, false),
driveItem(
"itemInSubfolder",
"itemInSubfolder",
testBaseDrivePath+"/a-folder/subfolder",
"subfolder",
true,
false,
false,
),
driveItem("folder", "folder", testBaseDrivePath, "root", false, true, false),
},
inputFolderMap: map[string]string{
"folder": expectedPath("/a-folder"),
"subfolder": expectedPath("/a-folder/subfolder"),
},
scope: anyFolder,
expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.MovedState, "/folder", "/a-folder"),
"folder2": expectedStatePath(data.NewState, "/folder2"),
"subfolder": expectedStatePath(data.MovedState, "/folder/subfolder", "/a-folder/subfolder"),
},
expectedItemCount: 5,
expectedFileCount: 2,
expectedContainerCount: 4,
expectedMetadataPaths: map[string]string{
"folder": expectedPath("/folder"),
"folder2": expectedPath("/folder2"),
"subfolder": expectedPath("/folder/subfolder"),
},
expectedExcludes: map[string]struct{}{"itemInSubfolder": {}, "itemInFolder2": {}},
},
{ {
testCase: "deleted folder and package", testCase: "deleted folder and package",
items: []models.DriveItemable{ items: []models.DriveItemable{
@ -450,6 +583,22 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
expectedMetadataPaths: map[string]string{}, expectedMetadataPaths: map[string]string{},
expectedExcludes: map[string]struct{}{}, expectedExcludes: map[string]struct{}{},
}, },
{
testCase: "delete folder without previous",
items: []models.DriveItemable{
driveRootItem("root"),
delItem("folder", testBaseDrivePath, "root", false, true, false),
},
inputFolderMap: map[string]string{},
scope: anyFolder,
expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{},
expectedItemCount: 0,
expectedFileCount: 0,
expectedContainerCount: 0,
expectedMetadataPaths: map[string]string{},
expectedExcludes: map[string]struct{}{},
},
{ {
testCase: "delete folder tree move subfolder", testCase: "delete folder tree move subfolder",
items: []models.DriveItemable{ items: []models.DriveItemable{
@ -464,12 +613,13 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder, scope: anyFolder,
expect: assert.NoError, expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{ expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NewState, ""), "root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.DeletedState, folder), "folder": expectedStatePath(data.DeletedState, folder),
"subfolder": expectedStatePath(data.MovedState, "/subfolder", "/folder/subfolder"),
}, },
expectedItemCount: 1, expectedItemCount: 1,
expectedFileCount: 0, expectedFileCount: 0,
expectedContainerCount: 1, expectedContainerCount: 2,
expectedMetadataPaths: map[string]string{ expectedMetadataPaths: map[string]string{
"subfolder": expectedPath("/subfolder"), "subfolder": expectedPath("/subfolder"),
}, },
@ -528,14 +678,14 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
assert.Equal(t, tt.expectedContainerCount, c.NumContainers, "container count") assert.Equal(t, tt.expectedContainerCount, c.NumContainers, "container count")
for id, sp := range tt.expectedCollectionIDs { for id, sp := range tt.expectedCollectionIDs {
assert.Contains(t, c.CollectionMap, id, "contains collection with id") assert.Containsf(t, c.CollectionMap, id, "contains collection with id %s", id)
assert.Equal(t, sp.state, c.CollectionMap[id].State(), "state for collection") assert.Equalf(t, sp.state, c.CollectionMap[id].State(), "state for collection %s", id)
assert.Equal(t, sp.curPath, c.CollectionMap[id].FullPath(), "current path for collection") assert.Equalf(t, sp.curPath, c.CollectionMap[id].FullPath(), "current path for collection %s", id)
assert.Equal(t, sp.prevPath, c.CollectionMap[id].PreviousPath(), "prev path for collection") assert.Equalf(t, sp.prevPath, c.CollectionMap[id].PreviousPath(), "prev path for collection %s", id)
} }
assert.Equal(t, tt.expectedMetadataPaths, outputFolderMap) assert.Equal(t, tt.expectedMetadataPaths, outputFolderMap, "metadata paths")
assert.Equal(t, tt.expectedExcludes, excludes) assert.Equal(t, tt.expectedExcludes, excludes, "exclude list")
}) })
} }
} }
@ -1041,7 +1191,7 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
}, },
errCheck: assert.NoError, errCheck: assert.NoError,
expectedCollections: map[string]map[data.CollectionState][]string{ expectedCollections: map[string]map[data.CollectionState][]string{
expectedPath1(""): {data.NewState: {"file"}}, expectedPath1(""): {data.NotMovedState: {"file"}},
}, },
expectedDeltaURLs: map[string]string{ expectedDeltaURLs: map[string]string{
driveID1: delta, driveID1: delta,
@ -1051,7 +1201,7 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
// token for this drive is valid. // token for this drive is valid.
driveID1: {}, driveID1: {},
}, },
expectedDelList: map[string]struct{}{}, expectedDelList: map[string]struct{}{"file": {}},
}, },
{ {
name: "OneDrive_OneItemPage_NoErrors", name: "OneDrive_OneItemPage_NoErrors",
@ -1071,7 +1221,7 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
errCheck: assert.NoError, errCheck: assert.NoError,
expectedCollections: map[string]map[data.CollectionState][]string{ expectedCollections: map[string]map[data.CollectionState][]string{
folderPath1: {data.NewState: {"file"}}, folderPath1: {data.NewState: {"file"}},
rootFolderPath1: {data.NewState: {"folder"}}, rootFolderPath1: {data.NotMovedState: {"folder"}},
}, },
expectedDeltaURLs: map[string]string{ expectedDeltaURLs: map[string]string{
driveID1: delta, driveID1: delta,
@ -1081,7 +1231,7 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
"folder": folderPath1, "folder": folderPath1,
}, },
}, },
expectedDelList: map[string]struct{}{}, expectedDelList: map[string]struct{}{"file": {}},
}, },
{ {
name: "OneDrive_OneItemPage_EmptyDelta_NoErrors", name: "OneDrive_OneItemPage_EmptyDelta_NoErrors",
@ -1101,11 +1251,11 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
errCheck: assert.NoError, errCheck: assert.NoError,
expectedCollections: map[string]map[data.CollectionState][]string{ expectedCollections: map[string]map[data.CollectionState][]string{
folderPath1: {data.NewState: {"file"}}, folderPath1: {data.NewState: {"file"}},
rootFolderPath1: {data.NewState: {"folder"}}, rootFolderPath1: {data.NotMovedState: {"folder"}},
}, },
expectedDeltaURLs: map[string]string{}, expectedDeltaURLs: map[string]string{},
expectedFolderPaths: map[string]map[string]string{}, expectedFolderPaths: map[string]map[string]string{},
expectedDelList: map[string]struct{}{}, expectedDelList: map[string]struct{}{"file": {}},
}, },
{ {
name: "OneDrive_TwoItemPages_NoErrors", name: "OneDrive_TwoItemPages_NoErrors",
@ -1133,7 +1283,7 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
errCheck: assert.NoError, errCheck: assert.NoError,
expectedCollections: map[string]map[data.CollectionState][]string{ expectedCollections: map[string]map[data.CollectionState][]string{
folderPath1: {data.NewState: {"file", "file2"}}, folderPath1: {data.NewState: {"file", "file2"}},
rootFolderPath1: {data.NewState: {"folder"}}, rootFolderPath1: {data.NotMovedState: {"folder"}},
}, },
expectedDeltaURLs: map[string]string{ expectedDeltaURLs: map[string]string{
driveID1: delta, driveID1: delta,
@ -1143,7 +1293,7 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
"folder": folderPath1, "folder": folderPath1,
}, },
}, },
expectedDelList: map[string]struct{}{}, expectedDelList: map[string]struct{}{"file": {}, "file2": {}},
}, },
{ {
name: "TwoDrives_OneItemPageEach_NoErrors", name: "TwoDrives_OneItemPageEach_NoErrors",
@ -1165,9 +1315,9 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
driveID2: { driveID2: {
{ {
items: []models.DriveItemable{ items: []models.DriveItemable{
driveRootItem("root"), driveRootItem("root2"),
driveItem("folder", "folder", driveBasePath2, "root", false, true, false), driveItem("folder2", "folder", driveBasePath2, "root2", false, true, false),
driveItem("file", "file", driveBasePath2+"/folder", "folder", true, false, false), driveItem("file2", "file", driveBasePath2+"/folder", "folder2", true, false, false),
}, },
deltaLink: &delta2, deltaLink: &delta2,
}, },
@ -1176,9 +1326,9 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
errCheck: assert.NoError, errCheck: assert.NoError,
expectedCollections: map[string]map[data.CollectionState][]string{ expectedCollections: map[string]map[data.CollectionState][]string{
folderPath1: {data.NewState: {"file"}}, folderPath1: {data.NewState: {"file"}},
folderPath2: {data.NewState: {"file"}}, folderPath2: {data.NewState: {"file2"}},
rootFolderPath1: {data.NewState: {"folder"}}, rootFolderPath1: {data.NotMovedState: {"folder"}},
rootFolderPath2: {data.NewState: {"folder"}}, rootFolderPath2: {data.NotMovedState: {"folder2"}},
}, },
expectedDeltaURLs: map[string]string{ expectedDeltaURLs: map[string]string{
driveID1: delta, driveID1: delta,
@ -1189,10 +1339,10 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
"folder": folderPath1, "folder": folderPath1,
}, },
driveID2: { driveID2: {
"folder": folderPath2, "folder2": folderPath2,
}, },
}, },
expectedDelList: map[string]struct{}{}, expectedDelList: map[string]struct{}{"file": {}, "file2": {}},
}, },
{ {
name: "OneDrive_OneItemPage_Errors", name: "OneDrive_OneItemPage_Errors",
@ -1229,7 +1379,7 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
}, },
errCheck: assert.NoError, errCheck: assert.NoError,
expectedCollections: map[string]map[data.CollectionState][]string{ expectedCollections: map[string]map[data.CollectionState][]string{
expectedPath1(""): {data.NewState: {"file"}}, expectedPath1(""): {data.NotMovedState: {"file"}},
}, },
expectedDeltaURLs: map[string]string{ expectedDeltaURLs: map[string]string{
driveID1: delta, driveID1: delta,
@ -1269,7 +1419,7 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
}, },
errCheck: assert.NoError, errCheck: assert.NoError,
expectedCollections: map[string]map[data.CollectionState][]string{ expectedCollections: map[string]map[data.CollectionState][]string{
expectedPath1(""): {data.NewState: {"file", "folder"}}, expectedPath1(""): {data.NotMovedState: {"file", "folder"}},
expectedPath1("/folder"): {data.NewState: {"file"}}, expectedPath1("/folder"): {data.NewState: {"file"}},
}, },
expectedDeltaURLs: map[string]string{ expectedDeltaURLs: map[string]string{
@ -1305,7 +1455,7 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
}, },
errCheck: assert.NoError, errCheck: assert.NoError,
expectedCollections: map[string]map[data.CollectionState][]string{ expectedCollections: map[string]map[data.CollectionState][]string{
expectedPath1(""): {data.NewState: {"file", "folder"}}, expectedPath1(""): {data.NotMovedState: {"file", "folder"}},
expectedPath1("/folder"): {data.NewState: {"file"}}, expectedPath1("/folder"): {data.NewState: {"file"}},
}, },
expectedDeltaURLs: map[string]string{ expectedDeltaURLs: map[string]string{
@ -1314,7 +1464,7 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
expectedFolderPaths: map[string]map[string]string{ expectedFolderPaths: map[string]map[string]string{
driveID1: {"folder": folderPath1}, driveID1: {"folder": folderPath1},
}, },
expectedDelList: map[string]struct{}{}, expectedDelList: map[string]struct{}{"file": {}},
doNotMergeItems: false, doNotMergeItems: false,
}, },
} }
@ -1422,11 +1572,17 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
itemIDs = append(itemIDs, id) itemIDs = append(itemIDs, id)
} }
assert.ElementsMatch(t, test.expectedCollections[folderPath][baseCol.State()], itemIDs) assert.ElementsMatchf(
t,
test.expectedCollections[folderPath][baseCol.State()],
itemIDs,
"items in collection %s",
folderPath,
)
assert.Equal(t, test.doNotMergeItems, baseCol.DoNotMergeItems(), "DoNotMergeItems") assert.Equal(t, test.doNotMergeItems, baseCol.DoNotMergeItems(), "DoNotMergeItems")
} }
assert.Equal(t, test.expectedDelList, delList) assert.Equal(t, test.expectedDelList, delList, "del list")
}) })
} }
} }

View File

@ -89,6 +89,9 @@ type Path interface {
Folder(bool) string Folder(bool) string
Folders() []string Folders() []string
Item() string Item() string
// UpdateParent updates parent from old to new if the item/folder was
// parented by old path
UpdateParent(prev, cur Path) bool
// PopFront returns a Builder object with the first element (left-side) // PopFront returns a Builder object with the first element (left-side)
// removed. As the resulting set of elements is no longer a valid resource // removed. As the resulting set of elements is no longer a valid resource
// path a Builder is returned instead. // path a Builder is returned instead.

View File

@ -271,3 +271,26 @@ func (rp dataLayerResourcePath) ToBuilder() *Builder {
// Safe to directly return the Builder because Builders are immutable. // Safe to directly return the Builder because Builders are immutable.
return &rp.Builder return &rp.Builder
} }
func (rp *dataLayerResourcePath) UpdateParent(prev, cur Path) bool {
if prev == cur || len(prev.Elements()) > len(rp.Elements()) {
return false
}
parent := true
for i, e := range prev.Elements() {
if rp.elements[i] != e {
parent = false
break
}
}
if !parent {
return false
}
rp.elements = append(cur.Elements(), rp.elements[len(prev.Elements()):]...)
return true
}

View File

@ -532,3 +532,78 @@ func (suite *PopulatedDataLayerResourcePath) TestAppend() {
}) })
} }
} }
func (suite *PopulatedDataLayerResourcePath) TestUpdateParent() {
cases := []struct {
name string
item string
prev string
cur string
expected string
updated bool
}{
{
name: "basic",
item: "folder/item",
prev: "folder",
cur: "new-folder",
expected: "new-folder/item",
updated: true,
},
{
name: "long path",
item: "folder/folder1/folder2/item",
prev: "folder/folder1",
cur: "new-folder/new-folder1",
expected: "new-folder/new-folder1/folder2/item",
updated: true,
},
{
name: "change to shorter path",
item: "folder/folder1/folder2/item",
prev: "folder/folder1/folder2",
cur: "new-folder",
expected: "new-folder/item",
updated: true,
},
{
name: "change to longer path",
item: "folder/item",
prev: "folder",
cur: "folder/folder1/folder2/folder3",
expected: "folder/folder1/folder2/folder3/item",
updated: true,
},
{
name: "not parent",
item: "folder/folder1/folder2/item",
prev: "folder1",
cur: "new-folder1",
expected: "dummy",
updated: false,
},
}
buildPath := func(t *testing.T, pth string, isItem bool) path.Path {
pathBuilder := path.Builder{}.Append(strings.Split(pth, "/")...)
item, err := pathBuilder.ToDataLayerOneDrivePath("tenant", "user", isItem)
require.NoError(t, err, "err building path")
return item
}
for _, tc := range cases {
suite.T().Run(tc.name, func(t *testing.T) {
item := buildPath(t, tc.item, true)
prev := buildPath(t, tc.prev, false)
cur := buildPath(t, tc.cur, false)
expected := buildPath(t, tc.expected, true)
updated := item.UpdateParent(prev, cur)
assert.Equal(t, tc.updated, updated, "path updated")
if tc.updated {
assert.Equal(t, expected, item, "modified path")
}
})
}
}