Store OneDrive directory metadata in directory (#2604)

## Description

This PR does a couple of things, some of which are to make storing the directory metadata in the directory easier:
* backup empty folders
* expand what selectors match on slightly so it includes the folder specified in the selector (e.x. match on prefix `/foo` will now include the folder `/foo` and subfolders under it)
* add restore code path for having directory metadata in the directory
* update backup code path to store directory metadata in the directory
* bump the backup version so we can tell this apart from the previous version

The above should mostly be split out by commit if that makes reviewing easier

Storing the directory metadata in the directory allows removing the data dependency between
restoring parent directories and child directories (though there may still be a dependency there for permissions inheritance). This makes it so that the order kopia returns restore data does not matter

It also unblocks some of the OneDrive delta token-based incremental backup work as we no longer have to worry about directory metadata when moving/deleting a directory

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

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

## Type of change

- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Test
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

## Issue(s)

* closes #2447
* closes #2532

## Test Plan

- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
ashmrtn 2023-02-23 12:58:12 -08:00 committed by GitHub
parent 9793d81670
commit 13a8c72f28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 250 additions and 183 deletions

View File

@ -704,11 +704,18 @@ func compareOneDriveItem(
t *testing.T,
expected map[string][]byte,
item data.Stream,
dest control.RestoreDestination,
restorePermissions bool,
) {
) bool {
// Skip OneDrive permissions in the folder that used to be the root. We don't
// have a good way to materialize these in the test right now.
if item.UUID() == dest.ContainerName+onedrive.DirMetaFileSuffix {
return false
}
buf, err := io.ReadAll(item.ToReader())
if !assert.NoError(t, err) {
return
return true
}
name := item.UUID()
@ -722,7 +729,7 @@ func compareOneDriveItem(
err = json.Unmarshal(buf, &itemMeta)
if !assert.NoErrorf(t, err, "unmarshalling retrieved metadata for file %s", name) {
return
return true
}
expectedData := expected[name]
@ -732,12 +739,12 @@ func compareOneDriveItem(
"unexpected metadata file with name %s",
name,
) {
return
return true
}
err = json.Unmarshal(expectedData, &expectedMeta)
if !assert.NoError(t, err, "unmarshalling expected metadata") {
return
return true
}
// Only compare file names if we're using a version that expects them to be
@ -748,7 +755,7 @@ func compareOneDriveItem(
if !restorePermissions {
assert.Equal(t, 0, len(itemMeta.Permissions))
return
return true
}
testElementsMatch(
@ -758,19 +765,19 @@ func compareOneDriveItem(
permissionEqual,
)
return
return true
}
var fileData testOneDriveData
err = json.Unmarshal(buf, &fileData)
if !assert.NoErrorf(t, err, "unmarshalling file data for file %s", name) {
return
return true
}
expectedData := expected[fileData.FileName]
if !assert.NotNil(t, expectedData, "unexpected file with name %s", name) {
return
return true
}
// OneDrive data items are just byte buffers of the data. Nothing special to
@ -779,16 +786,22 @@ func compareOneDriveItem(
// Compare against the version with the file name embedded because that's what
// the auto-generated expected data has.
assert.Equal(t, expectedData, buf)
return true
}
// compareItem compares the data returned by backup with the expected data.
// Returns true if a comparison was done else false. Bool return is mostly used
// to exclude OneDrive permissions for the root right now.
func compareItem(
t *testing.T,
expected map[string][]byte,
service path.ServiceType,
category path.CategoryType,
item data.Stream,
dest control.RestoreDestination,
restorePermissions bool,
) {
) bool {
if mt, ok := item.(data.StreamModTime); ok {
assert.NotZero(t, mt.ModTime())
}
@ -807,11 +820,13 @@ func compareItem(
}
case path.OneDriveService:
compareOneDriveItem(t, expected, item, restorePermissions)
return compareOneDriveItem(t, expected, item, dest, restorePermissions)
default:
assert.FailNowf(t, "unexpected service: %s", service.String())
}
return true
}
func checkHasCollections(
@ -832,7 +847,7 @@ func checkHasCollections(
gotNames = append(gotNames, g.FullPath().String())
}
assert.ElementsMatch(t, expectedNames, gotNames)
assert.ElementsMatch(t, expectedNames, gotNames, "returned collections")
}
//revive:disable:context-as-argument
@ -842,6 +857,7 @@ func checkCollections(
expectedItems int,
expected map[string]map[string][]byte,
got []data.BackupCollection,
dest control.RestoreDestination,
restorePermissions bool,
) int {
//revive:enable:context-as-argument
@ -851,10 +867,12 @@ func checkCollections(
gotItems := 0
for _, returned := range got {
startingItems := gotItems
service := returned.FullPath().Service()
category := returned.FullPath().Category()
expectedColData := expected[returned.FullPath().String()]
var (
hasItems bool
service = returned.FullPath().Service()
category = returned.FullPath().Category()
expectedColData = expected[returned.FullPath().String()]
)
// Need to iterate through all items even if we don't expect to find a match
// because otherwise we'll deadlock waiting for GC status. Unexpected or
@ -872,16 +890,19 @@ func checkCollections(
continue
}
hasItems = true
gotItems++
if expectedColData == nil {
continue
}
compareItem(t, expectedColData, service, category, item, restorePermissions)
if !compareItem(t, expectedColData, service, category, item, dest, restorePermissions) {
gotItems--
}
}
if gotItems != startingItems {
if hasItems {
collectionsWithItems = append(collectionsWithItems, returned)
}
}

View File

@ -173,7 +173,7 @@ func (c *onedriveCollection) withFile(
name+onedrive.DataFileSuffix,
fileData))
case 1, 2, 3:
case version.OneDrive1DataAndMetaFiles, 2, version.OneDrive3IsMetaMarker, version.OneDrive4DirIncludesPermissions:
c.items = append(c.items, onedriveItemWithData(
c.t,
name+onedrive.DataFileSuffix,
@ -202,10 +202,10 @@ func (c *onedriveCollection) withFolder(
roles []string,
) *onedriveCollection {
switch c.backupVersion {
case 0:
case 0, version.OneDrive4DirIncludesPermissions:
return c
case 1, 2, 3:
case version.OneDrive1DataAndMetaFiles, 2, version.OneDrive3IsMetaMarker:
c.items = append(
c.items,
onedriveMetadata(
@ -230,7 +230,7 @@ func (c *onedriveCollection) withPermissions(
) *onedriveCollection {
// These versions didn't store permissions for the folder or didn't store them
// in the folder's collection.
if c.backupVersion < version.OneDriveXIncludesPermissions {
if c.backupVersion < version.OneDrive4DirIncludesPermissions {
return c
}
@ -240,15 +240,15 @@ func (c *onedriveCollection) withPermissions(
return c
}
c.items = append(
c.items,
onedriveMetadata(
c.t,
name,
name+onedrive.DirMetaFileSuffix,
user,
roles),
)
metadata := onedriveMetadata(
c.t,
name,
name+onedrive.DirMetaFileSuffix,
user,
roles)
c.items = append(c.items, metadata)
c.aux = append(c.aux, metadata)
return c
}
@ -349,7 +349,7 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestRestoreAndBackup_Multip
table := []onedriveTest{
{
name: "WithMetadata",
startVersion: 1,
startVersion: version.OneDrive1DataAndMetaFiles,
cols: []onedriveColInfo{
{
pathElements: rootPath,
@ -375,6 +375,10 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestRestoreAndBackup_Multip
},
{
pathElements: folderBPath,
perms: permData{
user: suite.secondaryUser,
roles: readPerm,
},
files: []itemData{
{
name: fileName,
@ -516,7 +520,7 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsRestoreAndBa
folderBName,
}
startVersion := 1
startVersion := version.OneDrive1DataAndMetaFiles
table := []onedriveTest{
{
@ -731,7 +735,7 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsBackupAndNoR
suite.user,
)
startVersion := 1
startVersion := version.OneDrive1DataAndMetaFiles
table := []onedriveTest{
{
@ -891,6 +895,12 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsRestoreAndNo
"",
nil,
).
// Call this to generate a meta file with the folder name that we can
// check.
withPermissions(
"",
nil,
).
collection(),
},
}

View File

@ -493,7 +493,14 @@ func runBackupAndCompare(
// Pull the data prior to waiting for the status as otherwise it will
// deadlock.
skipped := checkCollections(t, ctx, totalKopiaItems, expectedData, dcs, config.opts.RestorePermissions)
skipped := checkCollections(
t,
ctx,
totalKopiaItems,
expectedData,
dcs,
config.dest,
config.opts.RestorePermissions)
status := backupGC.AwaitStatus()
@ -1000,7 +1007,15 @@ func (suite *GraphConnectorIntegrationSuite) TestMultiFolderBackupDifferentNames
// Pull the data prior to waiting for the status as otherwise it will
// deadlock.
skipped := checkCollections(t, ctx, allItems, allExpectedData, dcs, true)
skipped := checkCollections(
t,
ctx,
allItems,
allExpectedData,
dcs,
// Alright to be empty, needed for OneDrive.
control.RestoreDestination{},
true)
status := backupGC.AwaitStatus()
assert.Equal(t, allItems+skipped, status.ObjectCount, "status.ObjectCount")

View File

@ -13,6 +13,7 @@ import (
"github.com/pkg/errors"
"golang.org/x/exp/maps"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/data"
@ -562,8 +563,26 @@ func (c *Collections) UpdateCollections(
return err
}
var (
itemPath path.Path
isFolder = item.GetFolder() != nil || item.GetPackage() != nil
)
if item.GetDeleted() == nil {
name := ptr.Val(item.GetName())
if len(name) == 0 {
return clues.New("non-deleted item with empty name").With("item_id", name)
}
itemPath, err = collectionPath.Append(name, !isFolder)
if err != nil {
return err
}
}
// Skip items that don't match the folder selectors we were given.
if shouldSkipDrive(ctx, collectionPath, c.matcher, driveName) {
if shouldSkipDrive(ctx, itemPath, c.matcher, driveName) &&
shouldSkipDrive(ctx, collectionPath, c.matcher, driveName) {
logger.Ctx(ctx).Infof("Skipping path %s", collectionPath.String())
continue
}
@ -584,13 +603,6 @@ func (c *Collections) UpdateCollections(
// the deleted folder/package.
delete(newPaths, *item.GetId())
// TODO(meain): Directory metadata files should be
// moved into the directory instead of having a
// `.dirmeta` file at the same level as the
// directory. This way we can make sure it is moved
// and deleted along with the directory and don't have
// to be handled separately.
if prevPath == nil {
// It is possible that an item was created and
// deleted between two delta invocations. In
@ -616,51 +628,45 @@ func (c *Collections) UpdateCollections(
break
}
// Deletions of folders are handled in this case so we may as well start
// off by saving the path.Path of the item instead of just the OneDrive
// parentRef or such.
folderPath, err := collectionPath.Append(*item.GetName(), false)
if err != nil {
logger.Ctx(ctx).Errorw("failed building collection path", "error", err)
return err
}
// 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
// update newPaths so we don't accidentally clobber previous deletes.
updatePath(newPaths, *item.GetId(), folderPath.String())
updatePath(newPaths, *item.GetId(), itemPath.String())
found, err := updateCollectionPaths(*item.GetId(), c.CollectionMap, folderPath)
found, err := updateCollectionPaths(*item.GetId(), c.CollectionMap, itemPath)
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++
}
col := NewCollection(
c.itemClient,
itemPath,
prevPath,
driveID,
c.service,
c.statusUpdater,
c.source,
c.ctrl,
invalidPrevDelta,
)
c.CollectionMap[*item.GetId()] = col
c.NumContainers++
}
if c.source != OneDriveSource {
continue
}
fallthrough
if col := c.CollectionMap[*item.GetId()]; col != nil {
// Add an entry to fetch permissions into this collection. This assumes
// that OneDrive always returns all folders on the path of an item
// before the item. This seems to hold true for now at least.
collection := col.(*Collection)
if collection.Add(item) {
c.NumItems++
}
}
case item.GetFile() != nil:
if !invalidPrevDelta && item.GetFile() != nil {
@ -702,6 +708,11 @@ func (c *Collections) UpdateCollections(
col, found := c.CollectionMap[collectionID]
if !found {
// TODO(ashmrtn): We should probably tighten the restrictions on this
// and just make it return an error if the collection doesn't already
// exist. Graph seems pretty consistent about returning all folders on
// the path from the root to the item in question. Removing this will
// also ensure we always add an entry to get the folder metadata.
col = NewCollection(
c.itemClient,
collectionPath,
@ -739,14 +750,6 @@ func (c *Collections) UpdateCollections(
if !removed {
return clues.New("removing from prev collection").With("item_id", *item.GetId())
}
// If that was the only item in that collection and is
// not getting added back, delete the collection
if itemColID != collectionID &&
pcollection.IsEmpty() &&
pcollection.State() == data.NewState {
delete(c.CollectionMap, itemColID)
}
}
itemCollection[*item.GetId()] = collectionID
@ -754,11 +757,7 @@ func (c *Collections) UpdateCollections(
if collection.Add(item) {
c.NumItems++
if item.GetFile() != nil {
// This is necessary as we have a fallthrough for
// folders and packages
c.NumFiles++
}
c.NumFiles++
}
default:
@ -770,6 +769,10 @@ func (c *Collections) UpdateCollections(
}
func shouldSkipDrive(ctx context.Context, drivePath path.Path, m folderMatcher, driveName string) bool {
if drivePath == nil {
return false
}
return !includePath(ctx, m, drivePath) ||
(drivePath.Category() == path.LibrariesCategory && restrictedDirectory == driveName)
}

View File

@ -222,7 +222,7 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder,
expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.NewState, folder),
},
expectedMetadataPaths: map[string]string{
"root": expectedPath(""),
@ -242,7 +242,7 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder,
expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NotMovedState, ""),
"package": expectedStatePath(data.NewState, pkg),
},
expectedMetadataPaths: map[string]string{
"root": expectedPath(""),
@ -301,15 +301,16 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
"subfolder": expectedStatePath(data.NewState, folderSub),
"folder2": expectedStatePath(data.NewState, folderSub+folder),
},
expectedItemCount: 4,
expectedItemCount: 5,
expectedFileCount: 2,
expectedContainerCount: 3,
// just "folder" isn't added here because the include check is done on the
// parent path since we only check later if something is a folder or not.
expectedMetadataPaths: map[string]string{
"root": expectedPath(""),
"subfolder": expectedPath("/folder/subfolder"),
"folder2": expectedPath("/folder/subfolder/folder"),
"folder": expectedPath(folder),
"subfolder": expectedPath(folderSub),
"folder2": expectedPath(folderSub + folder),
},
expectedExcludes: getDelList("fileInFolder", "fileInFolder2"),
},
@ -334,12 +335,13 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
"subfolder": expectedStatePath(data.NewState, folderSub),
"folder2": expectedStatePath(data.NewState, folderSub+folder),
},
expectedItemCount: 2,
expectedItemCount: 3,
expectedFileCount: 1,
expectedContainerCount: 2,
expectedMetadataPaths: map[string]string{
"root": expectedPath(""),
"folder2": expectedPath("/folder/subfolder/folder"),
"root": expectedPath(""),
"subfolder": expectedPath(folderSub),
"folder2": expectedPath(folderSub + folder),
},
expectedExcludes: getDelList("fileInFolder2"),
},
@ -361,12 +363,13 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
expectedCollectionIDs: map[string]statePath{
"subfolder": expectedStatePath(data.NewState, folderSub),
},
expectedItemCount: 1,
expectedItemCount: 2,
expectedFileCount: 1,
expectedContainerCount: 1,
// No child folders for subfolder so nothing here.
expectedMetadataPaths: map[string]string{
"root": expectedPath(""),
"root": expectedPath(""),
"subfolder": expectedPath(folderSub),
},
expectedExcludes: getDelList("fileInSubfolder"),
},
@ -377,22 +380,21 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
driveItem("folder", "folder", testBaseDrivePath, "root", false, true, false),
},
inputFolderMap: map[string]string{
"folder": expectedPath("/folder"),
"subfolder": expectedPath("/folder/subfolder"),
"folder": expectedPath(folder),
"subfolder": expectedPath(folderSub),
},
scope: anyFolder,
expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.NotMovedState, "/folder"),
"folder": expectedStatePath(data.NotMovedState, folder),
},
expectedItemCount: 1,
expectedFileCount: 0,
expectedContainerCount: 2,
expectedContainerCount: 1,
expectedMetadataPaths: map[string]string{
"root": expectedPath(""),
"folder": expectedPath("/folder"),
"subfolder": expectedPath("/folder/subfolder"),
"folder": expectedPath(folder),
"subfolder": expectedPath(folderSub),
},
expectedExcludes: map[string]struct{}{},
},
@ -409,16 +411,15 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder,
expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.MovedState, "/folder", "/a-folder"),
"folder": expectedStatePath(data.MovedState, folder, "/a-folder"),
},
expectedItemCount: 1,
expectedFileCount: 0,
expectedContainerCount: 2,
expectedContainerCount: 1,
expectedMetadataPaths: map[string]string{
"root": expectedPath(""),
"folder": expectedPath("/folder"),
"subfolder": expectedPath("/folder/subfolder"),
"folder": expectedPath(folder),
"subfolder": expectedPath(folderSub),
},
expectedExcludes: map[string]struct{}{},
},
@ -430,20 +431,19 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
driveItem("folder", "folder", testBaseDrivePath, "root", false, true, false),
},
inputFolderMap: map[string]string{
"folder": expectedPath("/folder"),
"folder": expectedPath(folder),
},
scope: anyFolder,
expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.NotMovedState, "/folder"),
"folder": expectedStatePath(data.NotMovedState, folder),
},
expectedItemCount: 2,
expectedFileCount: 1,
expectedContainerCount: 2,
expectedContainerCount: 1,
expectedMetadataPaths: map[string]string{
"root": expectedPath(""),
"folder": expectedPath("/folder"),
"folder": expectedPath(folder),
},
expectedExcludes: getDelList("file"),
},
@ -459,12 +459,11 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
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
expectedItemCount: 2,
expectedFileCount: 1,
expectedContainerCount: 2,
expectedContainerCount: 1,
expectedMetadataPaths: map[string]string{
"root": expectedPath(""),
"folder": expectedPath("/folder2"),
@ -482,15 +481,14 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder,
expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.NewState, "/folder"),
"folder": expectedStatePath(data.NewState, folder),
},
expectedItemCount: 2,
expectedFileCount: 1,
expectedContainerCount: 2,
expectedContainerCount: 1,
expectedMetadataPaths: map[string]string{
"root": expectedPath(""),
"folder": expectedPath("/folder"),
"folder": expectedPath(folder),
},
expectedExcludes: getDelList("file"),
},
@ -508,16 +506,15 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder,
expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.MovedState, "/folder", "/a-folder"),
"folder": expectedStatePath(data.MovedState, folder, "/a-folder"),
"subfolder": expectedStatePath(data.MovedState, "/subfolder", "/a-folder/subfolder"),
},
expectedItemCount: 2,
expectedFileCount: 0,
expectedContainerCount: 3,
expectedContainerCount: 2,
expectedMetadataPaths: map[string]string{
"root": expectedPath(""),
"folder": expectedPath("/folder"),
"folder": expectedPath(folder),
"subfolder": expectedPath("/subfolder"),
},
expectedExcludes: map[string]struct{}{},
@ -536,16 +533,15 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder,
expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.MovedState, "/folder", "/a-folder"),
"folder": expectedStatePath(data.MovedState, folder, "/a-folder"),
"subfolder": expectedStatePath(data.MovedState, "/subfolder", "/a-folder/subfolder"),
},
expectedItemCount: 2,
expectedFileCount: 0,
expectedContainerCount: 3,
expectedContainerCount: 2,
expectedMetadataPaths: map[string]string{
"root": expectedPath(""),
"folder": expectedPath("/folder"),
"folder": expectedPath(folder),
"subfolder": expectedPath("/subfolder"),
},
expectedExcludes: map[string]struct{}{},
@ -556,6 +552,9 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
driveRootItem("root"),
driveItem("folder2", "folder2", testBaseDrivePath, "root", false, true, false),
driveItem("itemInFolder2", "itemInFolder2", testBaseDrivePath+"/folder2", "folder2", true, false, false),
// Need to see the parent folder first (expected since that's what Graph
// consistently returns).
driveItem("folder", "a-folder", testBaseDrivePath, "root", false, true, false),
driveItem("subfolder", "subfolder", testBaseDrivePath+"/a-folder", "folder", false, true, false),
driveItem(
"itemInSubfolder",
@ -575,14 +574,13 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder,
expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.MovedState, "/folder", "/a-folder"),
"folder": expectedStatePath(data.MovedState, folder, "/a-folder"),
"folder2": expectedStatePath(data.NewState, "/folder2"),
"subfolder": expectedStatePath(data.MovedState, "/folder/subfolder", "/a-folder/subfolder"),
"subfolder": expectedStatePath(data.MovedState, folderSub, "/a-folder/subfolder"),
},
expectedItemCount: 5,
expectedFileCount: 2,
expectedContainerCount: 4,
expectedContainerCount: 3,
expectedMetadataPaths: map[string]string{
"root": expectedPath(""),
"folder": expectedPath("/folder"),
@ -606,12 +604,11 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder,
expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.MovedState, "/folder2", "/a-folder"),
},
expectedItemCount: 3,
expectedItemCount: 2,
expectedFileCount: 1,
expectedContainerCount: 2,
expectedContainerCount: 1,
expectedMetadataPaths: map[string]string{
"root": expectedPath(""),
"folder": expectedPath("/folder2"),
@ -680,13 +677,12 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
scope: anyFolder,
expect: assert.NoError,
expectedCollectionIDs: map[string]statePath{
"root": expectedStatePath(data.NotMovedState, ""),
"folder": expectedStatePath(data.DeletedState, folder),
"subfolder": expectedStatePath(data.MovedState, "/subfolder", "/folder/subfolder"),
"subfolder": expectedStatePath(data.MovedState, "/subfolder", folderSub),
},
expectedItemCount: 1,
expectedFileCount: 0,
expectedContainerCount: 2,
expectedContainerCount: 1,
expectedMetadataPaths: map[string]string{
"root": expectedPath(""),
"subfolder": expectedPath("/subfolder"),
@ -752,7 +748,11 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
assert.Equal(t, tt.expectedContainerCount, c.NumContainers, "container count")
for id, sp := range tt.expectedCollectionIDs {
assert.Containsf(t, c.CollectionMap, id, "contains collection with id %s", id)
if !assert.Containsf(t, c.CollectionMap, id, "missing collection with id %s", id) {
// Skip collections we don't find so we don't get an NPE.
continue
}
assert.Equalf(t, sp.state, c.CollectionMap[id].State(), "state for collection %s", id)
assert.Equalf(t, sp.curPath, c.CollectionMap[id].FullPath(), "current path for collection %s", id)
assert.Equalf(t, sp.prevPath, c.CollectionMap[id].PreviousPath(), "prev path for collection %s", id)
@ -1299,8 +1299,7 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
driveID1: {},
},
expectedCollections: map[string]map[data.CollectionState][]string{
folderPath1: {data.NewState: {"file"}},
rootFolderPath1: {data.NotMovedState: {"folder"}},
folderPath1: {data.NewState: {"folder", "file"}},
},
expectedDeltaURLs: map[string]string{
driveID1: delta,
@ -1334,8 +1333,7 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
driveID1: {},
},
expectedCollections: map[string]map[data.CollectionState][]string{
folderPath1: {data.NewState: {"file"}},
rootFolderPath1: {data.NotMovedState: {"folder"}},
folderPath1: {data.NewState: {"folder", "file"}},
},
expectedDeltaURLs: map[string]string{
driveID1: delta,
@ -1369,7 +1367,8 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
driveID1: {},
},
expectedCollections: map[string]map[data.CollectionState][]string{
rootFolderPath1: {data.NotMovedState: {"folder", "file"}},
rootFolderPath1: {data.NotMovedState: {"file"}},
folderPath1: {data.NewState: {"folder"}},
},
expectedDeltaURLs: map[string]string{
driveID1: delta,
@ -1402,8 +1401,7 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
driveID1: {},
},
expectedCollections: map[string]map[data.CollectionState][]string{
folderPath1: {data.NewState: {"file"}},
rootFolderPath1: {data.NotMovedState: {"folder"}},
folderPath1: {data.NewState: {"folder", "file"}},
},
expectedDeltaURLs: map[string]string{},
expectedFolderPaths: map[string]map[string]string{},
@ -1437,8 +1435,7 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
driveID1: {},
},
expectedCollections: map[string]map[data.CollectionState][]string{
folderPath1: {data.NewState: {"file", "file2"}},
rootFolderPath1: {data.NotMovedState: {"folder"}},
folderPath1: {data.NewState: {"folder", "file", "file2"}},
},
expectedDeltaURLs: map[string]string{
driveID1: delta,
@ -1485,10 +1482,8 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
driveID2: {},
},
expectedCollections: map[string]map[data.CollectionState][]string{
folderPath1: {data.NewState: {"file"}},
folderPath2: {data.NewState: {"file2"}},
rootFolderPath1: {data.NotMovedState: {"folder"}},
rootFolderPath2: {data.NotMovedState: {"folder2"}},
folderPath1: {data.NewState: {"folder", "file"}},
folderPath2: {data.NewState: {"folder2", "file2"}},
},
expectedDeltaURLs: map[string]string{
driveID1: delta,
@ -1584,8 +1579,8 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
},
errCheck: assert.NoError,
expectedCollections: map[string]map[data.CollectionState][]string{
expectedPath1(""): {data.NotMovedState: {"file", "folder"}},
expectedPath1("/folder"): {data.NewState: {"file2"}},
expectedPath1(""): {data.NotMovedState: {"file"}},
expectedPath1("/folder"): {data.NewState: {"folder", "file2"}},
},
expectedDeltaURLs: map[string]string{
driveID1: delta,
@ -1626,8 +1621,8 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
driveID1: {},
},
expectedCollections: map[string]map[data.CollectionState][]string{
expectedPath1(""): {data.NotMovedState: {"file", "folder"}},
expectedPath1("/folder"): {data.NewState: {"file2"}},
expectedPath1(""): {data.NotMovedState: {"file"}},
expectedPath1("/folder"): {data.NewState: {"folder", "file2"}},
},
expectedDeltaURLs: map[string]string{
driveID1: delta,
@ -1667,9 +1662,8 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
},
},
expectedCollections: map[string]map[data.CollectionState][]string{
expectedPath1(""): {data.NotMovedState: {"folder2"}},
expectedPath1("/folder"): {data.DeletedState: {}},
expectedPath1("/folder2"): {data.NewState: {"file"}},
expectedPath1("/folder2"): {data.NewState: {"folder2", "file"}},
},
expectedDeltaURLs: map[string]string{
driveID1: delta,
@ -1709,8 +1703,7 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
},
},
expectedCollections: map[string]map[data.CollectionState][]string{
expectedPath1(""): {data.NotMovedState: {"folder2"}},
expectedPath1("/folder"): {data.NewState: {"file"}},
expectedPath1("/folder"): {data.NewState: {"folder2", "file"}},
},
expectedDeltaURLs: map[string]string{
driveID1: delta,
@ -1842,9 +1835,9 @@ func (suite *OneDriveCollectionsSuite) TestGet() {
t,
test.expectedCollections[folderPath][baseCol.State()],
itemIDs,
"items in collection %s",
folderPath,
)
"state: %d, path: %s",
baseCol.State(),
folderPath)
assert.Equal(t, test.doNotMergeItems, baseCol.DoNotMergeItems(), "DoNotMergeItems")
}

View File

@ -4,11 +4,14 @@ import (
"context"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/pkg/path"
msdrive "github.com/microsoftgraph/msgraph-sdk-go/drive"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/version"
"github.com/alcionai/corso/src/pkg/path"
)
func getParentPermissions(
@ -33,16 +36,23 @@ func getParentPermissions(
}
func getParentAndCollectionPermissions(
ctx context.Context,
drivePath *path.DrivePath,
collectionPath path.Path,
dc data.RestoreCollection,
permissions map[string][]UserPermission,
backupVersion int,
restorePerms bool,
) ([]UserPermission, []UserPermission, error) {
if !restorePerms {
if !restorePerms || backupVersion < version.OneDrive1DataAndMetaFiles {
return nil, nil, nil
}
var parentPerms []UserPermission
var (
err error
parentPerms []UserPermission
colPerms []UserPermission
collectionPath = dc.FullPath()
)
// Only get parent permissions if we're not restoring the root.
if len(drivePath.Folders) > 0 {
@ -57,11 +67,24 @@ func getParentAndCollectionPermissions(
}
}
// TODO(ashmrtn): For versions after this pull the permissions from the
// current collection with Fetch().
colPerms, err := getParentPermissions(collectionPath, permissions)
if err != nil {
return nil, nil, clues.Wrap(err, "getting collection permissions")
if backupVersion < version.OneDrive4DirIncludesPermissions {
colPerms, err = getParentPermissions(collectionPath, permissions)
if err != nil {
return nil, nil, clues.Wrap(err, "getting collection permissions")
}
} else if len(drivePath.Folders) > 0 {
// Root folder doesn't have a metadata file associated with it.
folders := collectionPath.Folders()
meta, err := fetchAndReadMetadata(
ctx,
dc,
folders[len(folders)-1]+DirMetaFileSuffix)
if err != nil {
return nil, nil, clues.Wrap(err, "collection permissions")
}
colPerms = meta.Permissions
}
return parentPerms, colPerms, nil

View File

@ -170,9 +170,11 @@ func RestoreCollection(
logger.Ctx(ctx).Info("restoring onedrive collection")
parentPerms, colPerms, err := getParentAndCollectionPermissions(
ctx,
drivePath,
dc.FullPath(),
dc,
parentPermissions,
backupVersion,
restorePerms)
if err != nil {
return metrics, folderPerms, permissionIDMappings, clues.Wrap(err, "getting permissions").WithClues(ctx)
@ -278,7 +280,10 @@ func RestoreCollection(
// RestoreOp, so we still need to handle them in some way.
continue
} else if strings.HasSuffix(name, DirMetaFileSuffix) {
if !restorePerms {
// Only the version.OneDrive1DataAndMetaFiles needed to deserialize the
// permission for child folders here. Later versions can request
// permissions inline when processing the collection.
if !restorePerms || backupVersion >= version.OneDrive4DirIncludesPermissions {
continue
}

View File

@ -2,7 +2,7 @@ package version
import "math"
const Backup = 3
const Backup = 4
// Various labels to refer to important version changes.
// Labels don't need 1:1 service:version representation. Add a new
@ -21,12 +21,9 @@ const (
// specifies if the file is a meta file or a data file.
OneDrive3IsMetaMarker = 3
// OneDrive4IncludesPermissions includes permissions in the backup.
// Note that this is larger than the current backup version. That's
// because it isn't implemented yet. But we have tests based on this,
// so maybe we just keep bumping the verson ahead of the backup until
// it gets implemented.
OneDriveXIncludesPermissions = Backup + 1
// OneDrive4IncludesPermissions includes permissions for folders in the same
// collection as the folder itself.
OneDrive4DirIncludesPermissions = 4
// OneDriveXNameInMeta points to the backup format version where we begin
// storing files in kopia with their item ID instead of their OneDrive file