cache created folders when restoring drive items (#3220)
drive items aren't currently getting cached after creation. Instead, we do a lookup on the folder name every time we walk the folder hierarchy toward the next child to restore. Normally this would work fine, but apparently if the folder is named `folder`, then graph api returns nothing, causing the process to fail when repeatedly creating the folder. --- #### Does this PR need a docs update or release note? - [x] ✅ Yes, it's included #### Type of change - [x] 🐛 Bugfix #### Test Plan - [x] 💪 Manual - [x] ⚡ Unit test - [x] 💚 E2E
This commit is contained in:
parent
c28673b2f0
commit
f2f010b9f6
@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Graph API requests that return an ECONNRESET error are now retried.
|
- Graph API requests that return an ECONNRESET error are now retried.
|
||||||
- Fixed edge case in incremental backups where moving a subfolder, deleting and recreating the subfolder's original parent folder, and moving the subfolder back to where it started would skip backing up unchanged items in the subfolder.
|
- Fixed edge case in incremental backups where moving a subfolder, deleting and recreating the subfolder's original parent folder, and moving the subfolder back to where it started would skip backing up unchanged items in the subfolder.
|
||||||
- SharePoint now correctly displays site urls on `backup list`, instead of the site id.
|
- SharePoint now correctly displays site urls on `backup list`, instead of the site id.
|
||||||
|
- Drives with a directory containing a folder named 'folder' will now restore without error.
|
||||||
|
|
||||||
### Known Issues
|
### Known Issues
|
||||||
- Restoring a OneDrive or SharePoint file with the same name as a file with that name as its M365 ID may restore both items.
|
- Restoring a OneDrive or SharePoint file with the same name as a file with that name as its M365 ID may restore both items.
|
||||||
|
|||||||
@ -106,9 +106,11 @@ func onedriveMetadata(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
fileName = "test-file.txt"
|
fileName = "test-file.txt"
|
||||||
folderAName = "folder-a"
|
folderAName = "folder-a"
|
||||||
folderBName = "b"
|
folderBName = "b"
|
||||||
|
folderNamedFolder = "folder"
|
||||||
|
rootFolder = "root:"
|
||||||
|
|
||||||
fileAData = []byte(strings.Repeat("a", 33))
|
fileAData = []byte(strings.Repeat("a", 33))
|
||||||
fileBData = []byte(strings.Repeat("b", 65))
|
fileBData = []byte(strings.Repeat("b", 65))
|
||||||
@ -254,7 +256,7 @@ func (c *onedriveCollection) withPermissions(perm permData) *onedriveCollection
|
|||||||
metaName = ""
|
metaName = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if name == "root:" {
|
if name == rootFolder {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -544,6 +546,11 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsInheritanceR
|
|||||||
testPermissionsInheritanceRestoreAndBackup(suite, version.Backup)
|
testPermissionsInheritanceRestoreAndBackup(suite, version.Backup)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *GraphConnectorOneDriveIntegrationSuite) TestRestoreFolderNamedFolderRegression() {
|
||||||
|
// No reason why it couldn't work with previous versions, but this is when it got introduced.
|
||||||
|
testRestoreFolderNamedFolderRegression(suite, version.All8MigrateUserPNToID)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// OneDrive regression
|
// OneDrive regression
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -600,11 +607,15 @@ func (suite *GraphConnectorOneDriveNightlySuite) TestPermissionsBackupAndNoResto
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *GraphConnectorOneDriveNightlySuite) TestPermissionsInheritanceRestoreAndBackup() {
|
func (suite *GraphConnectorOneDriveNightlySuite) TestPermissionsInheritanceRestoreAndBackup() {
|
||||||
// No reason why it couldn't work with previous versions, but this is when it
|
// No reason why it couldn't work with previous versions, but this is when it got introduced.
|
||||||
// got introduced.
|
|
||||||
testPermissionsInheritanceRestoreAndBackup(suite, version.OneDrive4DirIncludesPermissions)
|
testPermissionsInheritanceRestoreAndBackup(suite, version.OneDrive4DirIncludesPermissions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *GraphConnectorOneDriveNightlySuite) TestRestoreFolderNamedFolderRegression() {
|
||||||
|
// No reason why it couldn't work with previous versions, but this is when it got introduced.
|
||||||
|
testRestoreFolderNamedFolderRegression(suite, version.All8MigrateUserPNToID)
|
||||||
|
}
|
||||||
|
|
||||||
func testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(
|
func testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(
|
||||||
suite oneDriveSuite,
|
suite oneDriveSuite,
|
||||||
startVersion int,
|
startVersion int,
|
||||||
@ -618,31 +629,30 @@ func testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(
|
|||||||
ctx,
|
ctx,
|
||||||
suite.BackupService(),
|
suite.BackupService(),
|
||||||
suite.Service(),
|
suite.Service(),
|
||||||
suite.BackupResourceOwner(),
|
suite.BackupResourceOwner())
|
||||||
)
|
|
||||||
|
|
||||||
rootPath := []string{
|
rootPath := []string{
|
||||||
"drives",
|
"drives",
|
||||||
driveID,
|
driveID,
|
||||||
"root:",
|
rootFolder,
|
||||||
}
|
}
|
||||||
folderAPath := []string{
|
folderAPath := []string{
|
||||||
"drives",
|
"drives",
|
||||||
driveID,
|
driveID,
|
||||||
"root:",
|
rootFolder,
|
||||||
folderAName,
|
folderAName,
|
||||||
}
|
}
|
||||||
subfolderBPath := []string{
|
subfolderBPath := []string{
|
||||||
"drives",
|
"drives",
|
||||||
driveID,
|
driveID,
|
||||||
"root:",
|
rootFolder,
|
||||||
folderAName,
|
folderAName,
|
||||||
folderBName,
|
folderBName,
|
||||||
}
|
}
|
||||||
subfolderAPath := []string{
|
subfolderAPath := []string{
|
||||||
"drives",
|
"drives",
|
||||||
driveID,
|
driveID,
|
||||||
"root:",
|
rootFolder,
|
||||||
folderAName,
|
folderAName,
|
||||||
folderBName,
|
folderBName,
|
||||||
folderAName,
|
folderAName,
|
||||||
@ -650,7 +660,7 @@ func testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(
|
|||||||
folderBPath := []string{
|
folderBPath := []string{
|
||||||
"drives",
|
"drives",
|
||||||
driveID,
|
driveID,
|
||||||
"root:",
|
rootFolder,
|
||||||
folderBName,
|
folderBName,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -744,8 +754,7 @@ func testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(
|
|||||||
control.Options{
|
control.Options{
|
||||||
RestorePermissions: true,
|
RestorePermissions: true,
|
||||||
ToggleFeatures: control.Toggles{},
|
ToggleFeatures: control.Toggles{},
|
||||||
},
|
})
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -762,8 +771,7 @@ func testPermissionsRestoreAndBackup(suite oneDriveSuite, startVersion int) {
|
|||||||
ctx,
|
ctx,
|
||||||
suite.BackupService(),
|
suite.BackupService(),
|
||||||
suite.Service(),
|
suite.Service(),
|
||||||
suite.BackupResourceOwner(),
|
suite.BackupResourceOwner())
|
||||||
)
|
|
||||||
|
|
||||||
fileName2 := "test-file2.txt"
|
fileName2 := "test-file2.txt"
|
||||||
folderCName := "folder-c"
|
folderCName := "folder-c"
|
||||||
@ -771,32 +779,32 @@ func testPermissionsRestoreAndBackup(suite oneDriveSuite, startVersion int) {
|
|||||||
rootPath := []string{
|
rootPath := []string{
|
||||||
"drives",
|
"drives",
|
||||||
driveID,
|
driveID,
|
||||||
"root:",
|
rootFolder,
|
||||||
}
|
}
|
||||||
folderAPath := []string{
|
folderAPath := []string{
|
||||||
"drives",
|
"drives",
|
||||||
driveID,
|
driveID,
|
||||||
"root:",
|
rootFolder,
|
||||||
folderAName,
|
folderAName,
|
||||||
}
|
}
|
||||||
folderBPath := []string{
|
folderBPath := []string{
|
||||||
"drives",
|
"drives",
|
||||||
driveID,
|
driveID,
|
||||||
"root:",
|
rootFolder,
|
||||||
folderBName,
|
folderBName,
|
||||||
}
|
}
|
||||||
// For skipped test
|
// For skipped test
|
||||||
// subfolderAPath := []string{
|
// subfolderAPath := []string{
|
||||||
// "drives",
|
// "drives",
|
||||||
// driveID,
|
// driveID,
|
||||||
// "root:",
|
// rootFolder,
|
||||||
// folderBName,
|
// folderBName,
|
||||||
// folderAName,
|
// folderAName,
|
||||||
// }
|
// }
|
||||||
folderCPath := []string{
|
folderCPath := []string{
|
||||||
"drives",
|
"drives",
|
||||||
driveID,
|
driveID,
|
||||||
"root:",
|
rootFolder,
|
||||||
folderCName,
|
folderCName,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -958,8 +966,7 @@ func testPermissionsRestoreAndBackup(suite oneDriveSuite, startVersion int) {
|
|||||||
control.Options{
|
control.Options{
|
||||||
RestorePermissions: true,
|
RestorePermissions: true,
|
||||||
ToggleFeatures: control.Toggles{},
|
ToggleFeatures: control.Toggles{},
|
||||||
},
|
})
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -976,15 +983,14 @@ func testPermissionsBackupAndNoRestore(suite oneDriveSuite, startVersion int) {
|
|||||||
ctx,
|
ctx,
|
||||||
suite.BackupService(),
|
suite.BackupService(),
|
||||||
suite.Service(),
|
suite.Service(),
|
||||||
suite.BackupResourceOwner(),
|
suite.BackupResourceOwner())
|
||||||
)
|
|
||||||
|
|
||||||
inputCols := []onedriveColInfo{
|
inputCols := []onedriveColInfo{
|
||||||
{
|
{
|
||||||
pathElements: []string{
|
pathElements: []string{
|
||||||
"drives",
|
"drives",
|
||||||
driveID,
|
driveID,
|
||||||
"root:",
|
rootFolder,
|
||||||
},
|
},
|
||||||
files: []itemData{
|
files: []itemData{
|
||||||
{
|
{
|
||||||
@ -1005,7 +1011,7 @@ func testPermissionsBackupAndNoRestore(suite oneDriveSuite, startVersion int) {
|
|||||||
pathElements: []string{
|
pathElements: []string{
|
||||||
"drives",
|
"drives",
|
||||||
driveID,
|
driveID,
|
||||||
"root:",
|
rootFolder,
|
||||||
},
|
},
|
||||||
files: []itemData{
|
files: []itemData{
|
||||||
{
|
{
|
||||||
@ -1041,8 +1047,7 @@ func testPermissionsBackupAndNoRestore(suite oneDriveSuite, startVersion int) {
|
|||||||
control.Options{
|
control.Options{
|
||||||
RestorePermissions: false,
|
RestorePermissions: false,
|
||||||
ToggleFeatures: control.Toggles{},
|
ToggleFeatures: control.Toggles{},
|
||||||
},
|
})
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1062,8 +1067,7 @@ func testPermissionsInheritanceRestoreAndBackup(suite oneDriveSuite, startVersio
|
|||||||
ctx,
|
ctx,
|
||||||
suite.BackupService(),
|
suite.BackupService(),
|
||||||
suite.Service(),
|
suite.Service(),
|
||||||
suite.BackupResourceOwner(),
|
suite.BackupResourceOwner())
|
||||||
)
|
|
||||||
|
|
||||||
folderAName := "custom"
|
folderAName := "custom"
|
||||||
folderBName := "inherited"
|
folderBName := "inherited"
|
||||||
@ -1072,32 +1076,32 @@ func testPermissionsInheritanceRestoreAndBackup(suite oneDriveSuite, startVersio
|
|||||||
rootPath := []string{
|
rootPath := []string{
|
||||||
"drives",
|
"drives",
|
||||||
driveID,
|
driveID,
|
||||||
"root:",
|
rootFolder,
|
||||||
}
|
}
|
||||||
folderAPath := []string{
|
folderAPath := []string{
|
||||||
"drives",
|
"drives",
|
||||||
driveID,
|
driveID,
|
||||||
"root:",
|
rootFolder,
|
||||||
folderAName,
|
folderAName,
|
||||||
}
|
}
|
||||||
subfolderAAPath := []string{
|
subfolderAAPath := []string{
|
||||||
"drives",
|
"drives",
|
||||||
driveID,
|
driveID,
|
||||||
"root:",
|
rootFolder,
|
||||||
folderAName,
|
folderAName,
|
||||||
folderAName,
|
folderAName,
|
||||||
}
|
}
|
||||||
subfolderABPath := []string{
|
subfolderABPath := []string{
|
||||||
"drives",
|
"drives",
|
||||||
driveID,
|
driveID,
|
||||||
"root:",
|
rootFolder,
|
||||||
folderAName,
|
folderAName,
|
||||||
folderBName,
|
folderBName,
|
||||||
}
|
}
|
||||||
subfolderACPath := []string{
|
subfolderACPath := []string{
|
||||||
"drives",
|
"drives",
|
||||||
driveID,
|
driveID,
|
||||||
"root:",
|
rootFolder,
|
||||||
folderAName,
|
folderAName,
|
||||||
folderCName,
|
folderCName,
|
||||||
}
|
}
|
||||||
@ -1214,6 +1218,117 @@ func testPermissionsInheritanceRestoreAndBackup(suite oneDriveSuite, startVersio
|
|||||||
}
|
}
|
||||||
|
|
||||||
runRestoreBackupTestVersions(
|
runRestoreBackupTestVersions(
|
||||||
|
t,
|
||||||
|
suite.Account(),
|
||||||
|
testData,
|
||||||
|
suite.Tenant(),
|
||||||
|
[]string{suite.BackupResourceOwner()},
|
||||||
|
control.Options{
|
||||||
|
RestorePermissions: true,
|
||||||
|
ToggleFeatures: control.Toggles{},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRestoreFolderNamedFolderRegression(
|
||||||
|
suite oneDriveSuite,
|
||||||
|
startVersion int,
|
||||||
|
) {
|
||||||
|
ctx, flush := tester.NewContext()
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
// Get the default drive ID for the test user.
|
||||||
|
driveID := mustGetDefaultDriveID(
|
||||||
|
suite.T(),
|
||||||
|
ctx,
|
||||||
|
suite.BackupService(),
|
||||||
|
suite.Service(),
|
||||||
|
suite.BackupResourceOwner())
|
||||||
|
|
||||||
|
rootPath := []string{
|
||||||
|
"drives",
|
||||||
|
driveID,
|
||||||
|
rootFolder,
|
||||||
|
}
|
||||||
|
folderFolderPath := []string{
|
||||||
|
"drives",
|
||||||
|
driveID,
|
||||||
|
rootFolder,
|
||||||
|
folderNamedFolder,
|
||||||
|
}
|
||||||
|
subfolderPath := []string{
|
||||||
|
"drives",
|
||||||
|
driveID,
|
||||||
|
rootFolder,
|
||||||
|
folderNamedFolder,
|
||||||
|
folderBName,
|
||||||
|
}
|
||||||
|
|
||||||
|
cols := []onedriveColInfo{
|
||||||
|
{
|
||||||
|
pathElements: rootPath,
|
||||||
|
files: []itemData{
|
||||||
|
{
|
||||||
|
name: fileName,
|
||||||
|
data: fileAData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
folders: []itemData{
|
||||||
|
{
|
||||||
|
name: folderNamedFolder,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: folderBName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pathElements: folderFolderPath,
|
||||||
|
files: []itemData{
|
||||||
|
{
|
||||||
|
name: fileName,
|
||||||
|
data: fileBData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
folders: []itemData{
|
||||||
|
{
|
||||||
|
name: folderBName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pathElements: subfolderPath,
|
||||||
|
files: []itemData{
|
||||||
|
{
|
||||||
|
name: fileName,
|
||||||
|
data: fileCData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
folders: []itemData{
|
||||||
|
{
|
||||||
|
name: folderNamedFolder,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := testDataForInfo(suite.T(), suite.BackupService(), cols, version.Backup)
|
||||||
|
|
||||||
|
for vn := startVersion; vn <= version.Backup; vn++ {
|
||||||
|
suite.Run(fmt.Sprintf("Version%d", vn), func() {
|
||||||
|
t := suite.T()
|
||||||
|
input := testDataForInfo(t, suite.BackupService(), cols, vn)
|
||||||
|
|
||||||
|
testData := restoreBackupInfoMultiVersion{
|
||||||
|
service: suite.BackupService(),
|
||||||
|
resource: suite.Resource(),
|
||||||
|
backupVersion: vn,
|
||||||
|
collectionsPrevious: input,
|
||||||
|
collectionsLatest: expected,
|
||||||
|
}
|
||||||
|
|
||||||
|
runRestoreTestWithVerion(
|
||||||
t,
|
t,
|
||||||
suite.Account(),
|
suite.Account(),
|
||||||
testData,
|
testData,
|
||||||
|
|||||||
@ -606,6 +606,43 @@ func runRestoreBackupTest(
|
|||||||
test.collections)
|
test.collections)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runRestoreTest restores with data using the test's backup version
|
||||||
|
func runRestoreTestWithVerion(
|
||||||
|
t *testing.T,
|
||||||
|
acct account.Account,
|
||||||
|
test restoreBackupInfoMultiVersion,
|
||||||
|
tenant string,
|
||||||
|
resourceOwners []string,
|
||||||
|
opts control.Options,
|
||||||
|
) {
|
||||||
|
ctx, flush := tester.NewContext()
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
config := configInfo{
|
||||||
|
acct: acct,
|
||||||
|
opts: opts,
|
||||||
|
resource: test.resource,
|
||||||
|
service: test.service,
|
||||||
|
tenant: tenant,
|
||||||
|
resourceOwners: resourceOwners,
|
||||||
|
dest: tester.DefaultTestRestoreDestination(),
|
||||||
|
}
|
||||||
|
|
||||||
|
totalItems, _, collections, _ := getCollectionsAndExpected(
|
||||||
|
t,
|
||||||
|
config,
|
||||||
|
test.collectionsPrevious,
|
||||||
|
test.backupVersion)
|
||||||
|
|
||||||
|
runRestore(
|
||||||
|
t,
|
||||||
|
ctx,
|
||||||
|
config,
|
||||||
|
test.backupVersion,
|
||||||
|
collections,
|
||||||
|
totalItems)
|
||||||
|
}
|
||||||
|
|
||||||
// runRestoreBackupTestVersions restores with data from an older
|
// runRestoreBackupTestVersions restores with data from an older
|
||||||
// version of the backup and check the restored data against the
|
// version of the backup and check the restored data against the
|
||||||
// something that would be in the form of a newer backup.
|
// something that would be in the form of a newer backup.
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
abstractions "github.com/microsoft/kiota-abstractions-go"
|
abstractions "github.com/microsoft/kiota-abstractions-go"
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/drive"
|
||||||
"github.com/microsoftgraph/msgraph-sdk-go/drives"
|
"github.com/microsoftgraph/msgraph-sdk-go/drives"
|
||||||
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
"github.com/microsoftgraph/msgraph-sdk-go/sites"
|
"github.com/microsoftgraph/msgraph-sdk-go/sites"
|
||||||
@ -323,3 +324,51 @@ func GetDriveByID(
|
|||||||
|
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetDriveRoot(
|
||||||
|
ctx context.Context,
|
||||||
|
srv graph.Servicer,
|
||||||
|
driveID string,
|
||||||
|
) (models.DriveItemable, error) {
|
||||||
|
root, err := srv.Client().DrivesById(driveID).Root().Get(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, graph.Wrap(ctx, err, "getting drive root")
|
||||||
|
}
|
||||||
|
|
||||||
|
return root, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemByPathRawURLFmt = "https://graph.microsoft.com/v1.0/drives/%s/items/%s:/%s"
|
||||||
|
|
||||||
|
var ErrFolderNotFound = clues.New("folder not found")
|
||||||
|
|
||||||
|
// GetFolderByName will lookup the specified folder by name within the parentFolderID folder.
|
||||||
|
func GetFolderByName(
|
||||||
|
ctx context.Context,
|
||||||
|
service graph.Servicer,
|
||||||
|
driveID, parentFolderID, folder string,
|
||||||
|
) (models.DriveItemable, error) {
|
||||||
|
// The `Children().Get()` API doesn't yet support $filter, so using that to find a folder
|
||||||
|
// will be sub-optimal.
|
||||||
|
// Instead, we leverage OneDrive path-based addressing -
|
||||||
|
// https://learn.microsoft.com/en-us/graph/onedrive-addressing-driveitems#path-based-addressing
|
||||||
|
// - which allows us to lookup an item by its path relative to the parent ID
|
||||||
|
rawURL := fmt.Sprintf(itemByPathRawURLFmt, driveID, parentFolderID, folder)
|
||||||
|
builder := drive.NewItemsDriveItemItemRequestBuilder(rawURL, service.Adapter())
|
||||||
|
|
||||||
|
foundItem, err := builder.Get(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
if graph.IsErrDeletedInFlight(err) {
|
||||||
|
return nil, graph.Stack(ctx, clues.Stack(ErrFolderNotFound, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, graph.Wrap(ctx, err, "getting folder")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the item found is a folder, fail the call if not
|
||||||
|
if foundItem.GetFolder() == nil {
|
||||||
|
return nil, graph.Wrap(ctx, ErrFolderNotFound, "item is not a folder")
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundItem, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -18,8 +18,6 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/logger"
|
"github.com/alcionai/corso/src/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errFolderNotFound = clues.New("folder not found")
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
maxDrivesRetries = 3
|
maxDrivesRetries = 3
|
||||||
|
|
||||||
@ -27,7 +25,6 @@ const (
|
|||||||
// graph response
|
// graph response
|
||||||
nextLinkKey = "@odata.nextLink"
|
nextLinkKey = "@odata.nextLink"
|
||||||
itemChildrenRawURLFmt = "https://graph.microsoft.com/v1.0/drives/%s/items/%s/children"
|
itemChildrenRawURLFmt = "https://graph.microsoft.com/v1.0/drives/%s/items/%s/children"
|
||||||
itemByPathRawURLFmt = "https://graph.microsoft.com/v1.0/drives/%s/items/%s:/%s"
|
|
||||||
itemNotFoundErrorCode = "itemNotFound"
|
itemNotFoundErrorCode = "itemNotFound"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -195,42 +192,6 @@ func collectItems(
|
|||||||
return DeltaUpdate{URL: newDeltaURL, Reset: invalidPrevDelta}, newPaths, excluded, nil
|
return DeltaUpdate{URL: newDeltaURL, Reset: invalidPrevDelta}, newPaths, excluded, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFolder will lookup the specified folder name under `parentFolderID`
|
|
||||||
func getFolder(
|
|
||||||
ctx context.Context,
|
|
||||||
service graph.Servicer,
|
|
||||||
driveID, parentFolderID, folderName string,
|
|
||||||
) (models.DriveItemable, error) {
|
|
||||||
// The `Children().Get()` API doesn't yet support $filter, so using that to find a folder
|
|
||||||
// will be sub-optimal.
|
|
||||||
// Instead, we leverage OneDrive path-based addressing -
|
|
||||||
// https://learn.microsoft.com/en-us/graph/onedrive-addressing-driveitems#path-based-addressing
|
|
||||||
// - which allows us to lookup an item by its path relative to the parent ID
|
|
||||||
rawURL := fmt.Sprintf(itemByPathRawURLFmt, driveID, parentFolderID, folderName)
|
|
||||||
builder := drive.NewItemsDriveItemItemRequestBuilder(rawURL, service.Adapter())
|
|
||||||
|
|
||||||
var (
|
|
||||||
foundItem models.DriveItemable
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
foundItem, err = builder.Get(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
if graph.IsErrDeletedInFlight(err) {
|
|
||||||
return nil, graph.Stack(ctx, clues.Stack(errFolderNotFound, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, graph.Wrap(ctx, err, "getting folder")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the item found is a folder, fail the call if not
|
|
||||||
if foundItem.GetFolder() == nil {
|
|
||||||
return nil, graph.Stack(ctx, errFolderNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
return foundItem, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new item in the specified folder
|
// Create a new item in the specified folder
|
||||||
func CreateItem(
|
func CreateItem(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/logger"
|
"github.com/alcionai/corso/src/pkg/logger"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
"github.com/alcionai/corso/src/pkg/selectors"
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -282,7 +283,7 @@ type OneDriveSuite struct {
|
|||||||
userID string
|
userID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOneDriveDriveSuite(t *testing.T) {
|
func TestOneDriveSuite(t *testing.T) {
|
||||||
suite.Run(t, &OneDriveSuite{
|
suite.Run(t, &OneDriveSuite{
|
||||||
Suite: tester.NewIntegrationSuite(
|
Suite: tester.NewIntegrationSuite(
|
||||||
t,
|
t,
|
||||||
@ -329,15 +330,20 @@ func (suite *OneDriveSuite) TestCreateGetDeleteFolder() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
folderID, err := CreateRestoreFolders(ctx, gs, driveID, folderElements)
|
rootFolder, err := api.GetDriveRoot(ctx, gs, driveID)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
restoreFolders := path.Builder{}.Append(folderElements...)
|
||||||
|
|
||||||
|
folderID, err := CreateRestoreFolders(ctx, gs, driveID, ptr.Val(rootFolder.GetId()), restoreFolders, NewFolderCache())
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
folderIDs = append(folderIDs, folderID)
|
folderIDs = append(folderIDs, folderID)
|
||||||
|
|
||||||
folderName2 := "Corso_Folder_Test_" + common.FormatNow(common.SimpleTimeTesting)
|
folderName2 := "Corso_Folder_Test_" + common.FormatNow(common.SimpleTimeTesting)
|
||||||
folderElements = append(folderElements, folderName2)
|
restoreFolders = restoreFolders.Append(folderName2)
|
||||||
|
|
||||||
folderID, err = CreateRestoreFolders(ctx, gs, driveID, folderElements)
|
folderID, err = CreateRestoreFolders(ctx, gs, driveID, ptr.Val(rootFolder.GetId()), restoreFolders, NewFolderCache())
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
folderIDs = append(folderIDs, folderID)
|
folderIDs = append(folderIDs, folderID)
|
||||||
@ -390,8 +396,8 @@ func (fm testFolderMatcher) IsAny() bool {
|
|||||||
return fm.scope.IsAny(selectors.OneDriveFolder)
|
return fm.scope.IsAny(selectors.OneDriveFolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fm testFolderMatcher) Matches(path string) bool {
|
func (fm testFolderMatcher) Matches(p string) bool {
|
||||||
return fm.scope.Matches(selectors.OneDriveFolder, path)
|
return fm.scope.Matches(selectors.OneDriveFolder, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *OneDriveSuite) TestOneDriveNewCollections() {
|
func (suite *OneDriveSuite) TestOneDriveNewCollections() {
|
||||||
|
|||||||
28
src/internal/connector/onedrive/folder_cache.go
Normal file
28
src/internal/connector/onedrive/folder_cache.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package onedrive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: refactor to comply with graph/cache_container
|
||||||
|
|
||||||
|
type folderCache struct {
|
||||||
|
cache map[string]models.DriveItemable
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFolderCache() *folderCache {
|
||||||
|
return &folderCache{
|
||||||
|
cache: map[string]models.DriveItemable{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *folderCache) get(loc *path.Builder) (models.DriveItemable, bool) {
|
||||||
|
mdi, ok := c.cache[loc.String()]
|
||||||
|
return mdi, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *folderCache) set(loc *path.Builder, mdi models.DriveItemable) {
|
||||||
|
c.cache[loc.String()] = mdi
|
||||||
|
}
|
||||||
@ -155,7 +155,7 @@ func (suite *ItemIntegrationSuite) TestItemWriter() {
|
|||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
// Test Requirement 2: "Test Folder" should exist
|
// Test Requirement 2: "Test Folder" should exist
|
||||||
folder, err := getFolder(ctx, srv, test.driveID, ptr.Val(root.GetId()), "Test Folder")
|
folder, err := api.GetFolderByName(ctx, srv, test.driveID, ptr.Val(root.GetId()), "Test Folder")
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
newFolderName := "testfolder_" + common.FormatNow(common.SimpleTimeTesting)
|
newFolderName := "testfolder_" + common.FormatNow(common.SimpleTimeTesting)
|
||||||
@ -184,8 +184,8 @@ func (suite *ItemIntegrationSuite) TestItemWriter() {
|
|||||||
|
|
||||||
// HACK: Leveraging this to test getFolder behavior for a file. `getFolder()` on the
|
// HACK: Leveraging this to test getFolder behavior for a file. `getFolder()` on the
|
||||||
// newly created item should fail because it's a file not a folder
|
// newly created item should fail because it's a file not a folder
|
||||||
_, err = getFolder(ctx, srv, test.driveID, ptr.Val(newFolder.GetId()), newItemName)
|
_, err = api.GetFolderByName(ctx, srv, test.driveID, ptr.Val(newFolder.GetId()), newItemName)
|
||||||
require.ErrorIs(t, err, errFolderNotFound, clues.ToCore(err))
|
require.ErrorIs(t, err, api.ErrFolderNotFound, clues.ToCore(err))
|
||||||
|
|
||||||
// Initialize a 100KB mockDataProvider
|
// Initialize a 100KB mockDataProvider
|
||||||
td, writeSize := mockDataReader(int64(100 * 1024))
|
td, writeSize := mockDataReader(int64(100 * 1024))
|
||||||
@ -237,11 +237,11 @@ func (suite *ItemIntegrationSuite) TestDriveGetFolder() {
|
|||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
// Lookup a folder that doesn't exist
|
// Lookup a folder that doesn't exist
|
||||||
_, err = getFolder(ctx, srv, test.driveID, ptr.Val(root.GetId()), "FolderDoesNotExist")
|
_, err = api.GetFolderByName(ctx, srv, test.driveID, ptr.Val(root.GetId()), "FolderDoesNotExist")
|
||||||
require.ErrorIs(t, err, errFolderNotFound, clues.ToCore(err))
|
require.ErrorIs(t, err, api.ErrFolderNotFound, clues.ToCore(err))
|
||||||
|
|
||||||
// Lookup a folder that does exist
|
// Lookup a folder that does exist
|
||||||
_, err = getFolder(ctx, srv, test.driveID, ptr.Val(root.GetId()), "")
|
_, err = api.GetFolderByName(ctx, srv, test.driveID, ptr.Val(root.GetId()), "")
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -85,50 +85,6 @@ func getCollectionMetadata(
|
|||||||
return meta, nil
|
return meta, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createRestoreFoldersWithPermissions creates the restore folder hierarchy in
|
|
||||||
// the specified drive and returns the folder ID of the last folder entry in the
|
|
||||||
// hierarchy. Permissions are only applied to the last folder in the hierarchy.
|
|
||||||
// Passing nil for the permissions results in just creating the folder(s).
|
|
||||||
func createRestoreFoldersWithPermissions(
|
|
||||||
ctx context.Context,
|
|
||||||
creds account.M365Config,
|
|
||||||
service graph.Servicer,
|
|
||||||
drivePath *path.DrivePath,
|
|
||||||
restoreFolders []string,
|
|
||||||
folderPath path.Path,
|
|
||||||
folderMetadata Metadata,
|
|
||||||
folderMetas map[string]Metadata,
|
|
||||||
permissionIDMappings map[string]string,
|
|
||||||
restorePerms bool,
|
|
||||||
) (string, error) {
|
|
||||||
id, err := CreateRestoreFolders(ctx, service, drivePath.DriveID, restoreFolders)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(drivePath.Folders) == 0 {
|
|
||||||
// No permissions for root folder
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !restorePerms {
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = RestorePermissions(
|
|
||||||
ctx,
|
|
||||||
creds,
|
|
||||||
service,
|
|
||||||
drivePath.DriveID,
|
|
||||||
id,
|
|
||||||
folderPath,
|
|
||||||
folderMetadata,
|
|
||||||
folderMetas,
|
|
||||||
permissionIDMappings)
|
|
||||||
|
|
||||||
return id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// isSamePermission checks equality of two UserPermission objects
|
// isSamePermission checks equality of two UserPermission objects
|
||||||
func isSamePermission(p1, p2 UserPermission) bool {
|
func isSamePermission(p1, p2 UserPermission) bool {
|
||||||
// EntityID can be empty for older backups and Email can be empty
|
// EntityID can be empty for older backups and Email can be empty
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
"github.com/alcionai/corso/src/internal/connector/graph"
|
"github.com/alcionai/corso/src/internal/connector/graph"
|
||||||
|
"github.com/alcionai/corso/src/internal/connector/onedrive/api"
|
||||||
"github.com/alcionai/corso/src/internal/connector/onedrive/metadata"
|
"github.com/alcionai/corso/src/internal/connector/onedrive/metadata"
|
||||||
"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"
|
||||||
@ -52,6 +53,8 @@ func RestoreCollections(
|
|||||||
// permissionIDMappings is used to map between old and new id
|
// permissionIDMappings is used to map between old and new id
|
||||||
// of permissions as we restore them
|
// of permissions as we restore them
|
||||||
permissionIDMappings = map[string]string{}
|
permissionIDMappings = map[string]string{}
|
||||||
|
fc = NewFolderCache()
|
||||||
|
rootIDCache = map[string]string{}
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx = clues.Add(
|
ctx = clues.Add(
|
||||||
@ -90,6 +93,8 @@ func RestoreCollections(
|
|||||||
dc,
|
dc,
|
||||||
folderMetas,
|
folderMetas,
|
||||||
permissionIDMappings,
|
permissionIDMappings,
|
||||||
|
fc,
|
||||||
|
rootIDCache,
|
||||||
OneDriveSource,
|
OneDriveSource,
|
||||||
dest.ContainerName,
|
dest.ContainerName,
|
||||||
deets,
|
deets,
|
||||||
@ -129,6 +134,8 @@ func RestoreCollection(
|
|||||||
dc data.RestoreCollection,
|
dc data.RestoreCollection,
|
||||||
folderMetas map[string]Metadata,
|
folderMetas map[string]Metadata,
|
||||||
permissionIDMappings map[string]string,
|
permissionIDMappings map[string]string,
|
||||||
|
fc *folderCache,
|
||||||
|
rootIDCache map[string]string, // map of drive id -> root folder ID
|
||||||
source driveSource,
|
source driveSource,
|
||||||
restoreContainerName string,
|
restoreContainerName string,
|
||||||
deets *details.Builder,
|
deets *details.Builder,
|
||||||
@ -150,12 +157,24 @@ func RestoreCollection(
|
|||||||
return metrics, clues.Wrap(err, "creating drive path").WithClues(ctx)
|
return metrics, clues.Wrap(err, "creating drive path").WithClues(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rootIDCache == nil {
|
||||||
|
rootIDCache = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := rootIDCache[drivePath.DriveID]; !ok {
|
||||||
|
root, err := api.GetDriveRoot(ctx, service, drivePath.DriveID)
|
||||||
|
if err != nil {
|
||||||
|
return metrics, clues.Wrap(err, "getting drive root id")
|
||||||
|
}
|
||||||
|
|
||||||
|
rootIDCache[drivePath.DriveID] = ptr.Val(root.GetId())
|
||||||
|
}
|
||||||
|
|
||||||
// Assemble folder hierarchy we're going to restore into (we recreate the folder hierarchy
|
// Assemble folder hierarchy we're going to restore into (we recreate the folder hierarchy
|
||||||
// from the backup under this the restore folder instead of root)
|
// from the backup under this the restore folder instead of root)
|
||||||
// i.e. Restore into `<drive>/root:/<restoreContainerName>/<original folder path>`
|
// i.e. Restore into `<restoreContainerName>/<original folder path>`
|
||||||
|
// the drive into which this folder gets restored is tracked separately in drivePath.
|
||||||
restoreFolderElements := []string{restoreContainerName}
|
restoreFolderElements := path.Builder{}.Append(restoreContainerName).Append(drivePath.Folders...)
|
||||||
restoreFolderElements = append(restoreFolderElements, drivePath.Folders...)
|
|
||||||
|
|
||||||
ctx = clues.Add(
|
ctx = clues.Add(
|
||||||
ctx,
|
ctx,
|
||||||
@ -183,10 +202,12 @@ func RestoreCollection(
|
|||||||
creds,
|
creds,
|
||||||
service,
|
service,
|
||||||
drivePath,
|
drivePath,
|
||||||
|
rootIDCache[drivePath.DriveID],
|
||||||
restoreFolderElements,
|
restoreFolderElements,
|
||||||
dc.FullPath(),
|
dc.FullPath(),
|
||||||
colMeta,
|
colMeta,
|
||||||
folderMetas,
|
folderMetas,
|
||||||
|
fc,
|
||||||
permissionIDMappings,
|
permissionIDMappings,
|
||||||
restorePerms)
|
restorePerms)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -541,43 +562,112 @@ func restoreV6File(
|
|||||||
return itemInfo, nil
|
return itemInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createRestoreFoldersWithPermissions creates the restore folder hierarchy in
|
||||||
|
// the specified drive and returns the folder ID of the last folder entry in the
|
||||||
|
// hierarchy. Permissions are only applied to the last folder in the hierarchy.
|
||||||
|
// Passing nil for the permissions results in just creating the folder(s).
|
||||||
|
// folderCache is mutated, as a side effect of populating the items.
|
||||||
|
func createRestoreFoldersWithPermissions(
|
||||||
|
ctx context.Context,
|
||||||
|
creds account.M365Config,
|
||||||
|
service graph.Servicer,
|
||||||
|
drivePath *path.DrivePath,
|
||||||
|
driveRootID string,
|
||||||
|
restoreFolders *path.Builder,
|
||||||
|
folderPath path.Path,
|
||||||
|
folderMetadata Metadata,
|
||||||
|
folderMetas map[string]Metadata,
|
||||||
|
fc *folderCache,
|
||||||
|
permissionIDMappings map[string]string,
|
||||||
|
restorePerms bool,
|
||||||
|
) (string, error) {
|
||||||
|
id, err := CreateRestoreFolders(
|
||||||
|
ctx,
|
||||||
|
service,
|
||||||
|
drivePath.DriveID,
|
||||||
|
driveRootID,
|
||||||
|
restoreFolders,
|
||||||
|
fc)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(drivePath.Folders) == 0 {
|
||||||
|
// No permissions for root folder
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !restorePerms {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = RestorePermissions(
|
||||||
|
ctx,
|
||||||
|
creds,
|
||||||
|
service,
|
||||||
|
drivePath.DriveID,
|
||||||
|
id,
|
||||||
|
folderPath,
|
||||||
|
folderMetadata,
|
||||||
|
folderMetas,
|
||||||
|
permissionIDMappings)
|
||||||
|
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
// CreateRestoreFolders creates the restore folder hierarchy in the specified
|
// CreateRestoreFolders creates the restore folder hierarchy in the specified
|
||||||
// drive and returns the folder ID of the last folder entry in the hierarchy.
|
// drive and returns the folder ID of the last folder entry in the hierarchy.
|
||||||
|
// folderCache is mutated, as a side effect of populating the items.
|
||||||
func CreateRestoreFolders(
|
func CreateRestoreFolders(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
service graph.Servicer,
|
service graph.Servicer,
|
||||||
driveID string,
|
driveID, driveRootID string,
|
||||||
restoreFolders []string,
|
restoreFolders *path.Builder,
|
||||||
|
fc *folderCache,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
driveRoot, err := service.Client().DrivesById(driveID).Root().Get(ctx, nil)
|
var (
|
||||||
if err != nil {
|
location = &path.Builder{}
|
||||||
return "", graph.Wrap(ctx, err, "getting drive root")
|
parentFolderID = driveRootID
|
||||||
}
|
folders = restoreFolders.Elements()
|
||||||
|
)
|
||||||
|
|
||||||
parentFolderID := ptr.Val(driveRoot.GetId())
|
for _, folder := range folders {
|
||||||
ctx = clues.Add(ctx, "drive_root_id", parentFolderID)
|
location = location.Append(folder)
|
||||||
|
ictx := clues.Add(
|
||||||
|
ctx,
|
||||||
|
"creating_restore_folder", folder,
|
||||||
|
"restore_folder_location", location,
|
||||||
|
"parent_of_restore_folder", parentFolderID)
|
||||||
|
|
||||||
logger.Ctx(ctx).Debug("found drive root")
|
if fl, ok := fc.get(location); ok {
|
||||||
|
parentFolderID = ptr.Val(fl.GetId())
|
||||||
for _, folder := range restoreFolders {
|
// folder was already created, move on to the child
|
||||||
folderItem, err := getFolder(ctx, service, driveID, parentFolderID, folder)
|
|
||||||
if err == nil {
|
|
||||||
parentFolderID = ptr.Val(folderItem.GetId())
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !errors.Is(err, errFolderNotFound) {
|
folderItem, err := api.GetFolderByName(ictx, service, driveID, parentFolderID, folder)
|
||||||
return "", clues.Wrap(err, "folder not found").With("folder_id", folder).WithClues(ctx)
|
if err != nil && !errors.Is(err, api.ErrFolderNotFound) {
|
||||||
|
return "", clues.Wrap(err, "getting folder by display name").WithClues(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// folder found, moving to next child
|
||||||
|
if err == nil {
|
||||||
|
parentFolderID = ptr.Val(folderItem.GetId())
|
||||||
|
fc.set(location, folderItem)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the folder if not found
|
||||||
folderItem, err = CreateItem(ctx, service, driveID, parentFolderID, newItem(folder, true))
|
folderItem, err = CreateItem(ctx, service, driveID, parentFolderID, newItem(folder, true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", clues.Wrap(err, "creating folder")
|
return "", clues.Wrap(err, "creating folder")
|
||||||
}
|
}
|
||||||
|
|
||||||
parentFolderID = ptr.Val(folderItem.GetId())
|
parentFolderID = ptr.Val(folderItem.GetId())
|
||||||
|
fc.set(location, folderItem)
|
||||||
|
|
||||||
logger.Ctx(ctx).Debugw("resolved restore destination", "dest_id", parentFolderID)
|
logger.Ctx(ctx).Debug("resolved restore destination")
|
||||||
}
|
}
|
||||||
|
|
||||||
return parentFolderID, nil
|
return parentFolderID, nil
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import (
|
|||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/common"
|
"github.com/alcionai/corso/src/internal/common"
|
||||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
"github.com/alcionai/corso/src/internal/connector/onedrive"
|
|
||||||
"github.com/alcionai/corso/src/internal/connector/sharepoint/api"
|
"github.com/alcionai/corso/src/internal/connector/sharepoint/api"
|
||||||
spMock "github.com/alcionai/corso/src/internal/connector/sharepoint/mock"
|
spMock "github.com/alcionai/corso/src/internal/connector/sharepoint/mock"
|
||||||
"github.com/alcionai/corso/src/internal/connector/support"
|
"github.com/alcionai/corso/src/internal/connector/support"
|
||||||
@ -233,30 +232,3 @@ func (suite *SharePointCollectionSuite) TestListCollection_Restore() {
|
|||||||
assert.NoError(t, err, clues.ToCore(err))
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRestoreLocation temporary test for greater restore operation
|
|
||||||
// TODO delete after full functionality tested in GraphConnector
|
|
||||||
func (suite *SharePointCollectionSuite) TestRestoreLocation() {
|
|
||||||
ctx, flush := tester.NewContext()
|
|
||||||
defer flush()
|
|
||||||
|
|
||||||
t := suite.T()
|
|
||||||
|
|
||||||
service := createTestService(t, suite.creds)
|
|
||||||
rootFolder := "General_" + common.FormatNow(common.SimpleTimeTesting)
|
|
||||||
folderID, err := createRestoreFolders(ctx, service, suite.siteID, []string{rootFolder})
|
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
|
||||||
t.Log("FolderID: " + folderID)
|
|
||||||
|
|
||||||
_, err = createRestoreFolders(ctx, service, suite.siteID, []string{rootFolder, "Tsao"})
|
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
|
||||||
|
|
||||||
// CleanUp
|
|
||||||
siteDrive, err := service.Client().SitesById(suite.siteID).Drive().Get(ctx, nil)
|
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
|
||||||
|
|
||||||
driveID := ptr.Val(siteDrive.GetId())
|
|
||||||
|
|
||||||
err = onedrive.DeleteItem(ctx, service, driveID, folderID)
|
|
||||||
assert.NoError(t, err, clues.ToCore(err))
|
|
||||||
}
|
|
||||||
|
|||||||
@ -63,6 +63,7 @@ func RestoreCollections(
|
|||||||
"category", category,
|
"category", category,
|
||||||
"destination", clues.Hide(dest.ContainerName),
|
"destination", clues.Hide(dest.ContainerName),
|
||||||
"resource_owner", clues.Hide(dc.FullPath().ResourceOwner()))
|
"resource_owner", clues.Hide(dc.FullPath().ResourceOwner()))
|
||||||
|
driveFolderCache = onedrive.NewFolderCache()
|
||||||
)
|
)
|
||||||
|
|
||||||
switch dc.FullPath().Category() {
|
switch dc.FullPath().Category() {
|
||||||
@ -75,11 +76,14 @@ func RestoreCollections(
|
|||||||
dc,
|
dc,
|
||||||
map[string]onedrive.Metadata{}, // Currently permission data is not stored for sharepoint
|
map[string]onedrive.Metadata{}, // Currently permission data is not stored for sharepoint
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
|
driveFolderCache,
|
||||||
|
nil,
|
||||||
onedrive.SharePointSource,
|
onedrive.SharePointSource,
|
||||||
dest.ContainerName,
|
dest.ContainerName,
|
||||||
deets,
|
deets,
|
||||||
false,
|
false,
|
||||||
errs)
|
errs)
|
||||||
|
|
||||||
case path.ListsCategory:
|
case path.ListsCategory:
|
||||||
metrics, err = RestoreListCollection(
|
metrics, err = RestoreListCollection(
|
||||||
ictx,
|
ictx,
|
||||||
@ -88,6 +92,7 @@ func RestoreCollections(
|
|||||||
dest.ContainerName,
|
dest.ContainerName,
|
||||||
deets,
|
deets,
|
||||||
errs)
|
errs)
|
||||||
|
|
||||||
case path.PagesCategory:
|
case path.PagesCategory:
|
||||||
metrics, err = RestorePageCollection(
|
metrics, err = RestorePageCollection(
|
||||||
ictx,
|
ictx,
|
||||||
@ -96,6 +101,7 @@ func RestoreCollections(
|
|||||||
dest.ContainerName,
|
dest.ContainerName,
|
||||||
deets,
|
deets,
|
||||||
errs)
|
errs)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, clues.Wrap(clues.New(category.String()), "category not supported").With("category", category)
|
return nil, clues.Wrap(clues.New(category.String()), "category not supported").With("category", category)
|
||||||
}
|
}
|
||||||
@ -117,23 +123,6 @@ func RestoreCollections(
|
|||||||
return status, err
|
return status, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// createRestoreFolders creates the restore folder hierarchy in the specified drive and returns the folder ID
|
|
||||||
// of the last folder entry given in the hierarchy
|
|
||||||
func createRestoreFolders(
|
|
||||||
ctx context.Context,
|
|
||||||
service graph.Servicer,
|
|
||||||
siteID string,
|
|
||||||
restoreFolders []string,
|
|
||||||
) (string, error) {
|
|
||||||
// Get Main Drive for Site, Documents
|
|
||||||
mainDrive, err := service.Client().SitesById(siteID).Drive().Get(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", graph.Wrap(ctx, err, "getting site drive root")
|
|
||||||
}
|
|
||||||
|
|
||||||
return onedrive.CreateRestoreFolders(ctx, service, ptr.Val(mainDrive.GetId()), restoreFolders)
|
|
||||||
}
|
|
||||||
|
|
||||||
// restoreListItem utility function restores a List to the siteID.
|
// restoreListItem utility function restores a List to the siteID.
|
||||||
// The name is changed to to Corso_Restore_{timeStame}_name
|
// The name is changed to to Corso_Restore_{timeStame}_name
|
||||||
// API Reference: https://learn.microsoft.com/en-us/graph/api/list-create?view=graph-rest-1.0&tabs=http
|
// API Reference: https://learn.microsoft.com/en-us/graph/api/list-create?view=graph-rest-1.0&tabs=http
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user