Files
duplicati/proprietary/GoogleWorkspace/RestoreProvider/RestoreProvider.Drive.cs
T

771 lines
33 KiB
C#

// Copyright (c) 2026 Duplicati Inc. All rights reserved.
using System.Text.Json;
using Duplicati.Library.Common.IO;
using Duplicati.Library.Logging;
using Duplicati.Proprietary.GoogleWorkspace.SourceItems;
using Google.Apis.Drive.v3.Data;
using Google.Apis.Upload;
namespace Duplicati.Proprietary.GoogleWorkspace;
partial class RestoreProvider
{
private DriveRestoreHelper? _driveRestoreHelper = null;
internal DriveRestoreHelper DriveRestore => _driveRestoreHelper ??= new DriveRestoreHelper(this);
internal class DriveRestoreHelper(RestoreProvider Provider)
{
private string? _cachedDefaultFolderId = null;
private bool _defaultFolderIdChecked = false;
public async Task<string?> GetDefaultFolder(CancellationToken cancel)
{
if (_defaultFolderIdChecked)
return _cachedDefaultFolderId;
_defaultFolderIdChecked = true;
var target = Provider.RestoreTarget;
if (target == null)
return null;
if (target.Type == SourceItemType.DriveFolder)
return _cachedDefaultFolderId = target.Metadata.GetValueOrDefault("gsuite:Id");
string? userId;
if (target.Type == SourceItemType.User)
{
userId = target.Metadata.GetValueOrDefault("gsuite:Id");
}
else if (target.Type == SourceItemType.UserDrive)
{
userId = target.Path.TrimStart(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar).Skip(1).FirstOrDefault();
}
else
{
return _cachedDefaultFolderId = null;
}
// Create "Restored" folder
try
{
var driveService = Provider._apiHelper.GetDriveService(userId);
// Check if folder already exists
var listRequest = driveService.Files.List();
listRequest.Q = $"mimeType='{GoogleMimeTypes.Folder}' and name='Restored' and 'root' in parents and trashed=false";
listRequest.Spaces = "drive";
var existingFolders = await listRequest.ExecuteAsync(cancel);
if (existingFolders.Files?.Count > 0)
{
_cachedDefaultFolderId = existingFolders.Files[0].Id;
}
else
{
// Create new folder
var folderMetadata = new Google.Apis.Drive.v3.Data.File
{
Name = "Restored",
MimeType = GoogleMimeTypes.Folder,
Parents = new List<string> { "root" }
};
var createdFolder = await driveService.Files.Create(folderMetadata).ExecuteAsync(cancel);
_cachedDefaultFolderId = createdFolder.Id;
}
}
catch (Exception ex)
{
Log.WriteWarningMessage(LOGTAG, "CreateRestoredFolderFailed", ex, $"Failed to create 'Restored' folder for user {userId}");
}
return _cachedDefaultFolderId;
}
public async Task<string?> CreateFolder(string? userId, string parentFolderId, string name, CancellationToken cancel)
{
var driveService = Provider._apiHelper.GetDriveService(userId);
// Check if folder already exists
var listRequest = driveService.Files.List();
listRequest.Q = $"mimeType='{GoogleMimeTypes.Folder}' and name='{name.Replace("'", "\\'")}' and '{parentFolderId}' in parents and trashed=false";
listRequest.Spaces = "drive";
listRequest.SupportsAllDrives = true;
listRequest.IncludeItemsFromAllDrives = true;
var existingFolders = await listRequest.ExecuteAsync(cancel);
if (existingFolders.Files?.Count > 0)
{
if (Provider._ignoreExisting)
{
Log.WriteInformationMessage(LOGTAG, "CreateFolderSkipExisting", $"Folder {name} already exists, skipping.");
return existingFolders.Files[0].Id;
}
// Return existing folder ID
return existingFolders.Files[0].Id;
}
// Create new folder
var folderMetadata = new Google.Apis.Drive.v3.Data.File
{
Name = name,
MimeType = GoogleMimeTypes.Folder,
Parents = new List<string> { parentFolderId }
};
var createRequest = driveService.Files.Create(folderMetadata);
createRequest.SupportsAllDrives = true;
var createdFolder = await createRequest.ExecuteAsync(cancel);
return createdFolder.Id;
}
public async Task<string?> UploadFile(string? userId, string parentFolderId, string name, Stream contentStream, string? mimeType, CancellationToken cancel)
{
var driveService = Provider._apiHelper.GetDriveService(userId);
// Check if file already exists
if (!Provider._ignoreExisting)
{
var listRequest = driveService.Files.List();
listRequest.Q = $"name='{name.Replace("'", "\\'")}' and '{parentFolderId}' in parents and trashed=false";
listRequest.Spaces = "drive";
listRequest.Fields = "nextPageToken, files(id, name, size)";
listRequest.SupportsAllDrives = true;
listRequest.IncludeItemsFromAllDrives = true;
var existingFiles = await listRequest.ExecuteAsync(cancel);
var existingFile = existingFiles.Files?.FirstOrDefault(x => x.Size == contentStream.Length);
if (existingFile != null)
{
Log.WriteInformationMessage(LOGTAG, "UploadFileSkipDuplicate", $"File {name} already exists with same size, skipping.");
return existingFile.Id;
}
}
// Upload file
var fileMetadata = new Google.Apis.Drive.v3.Data.File
{
Name = name,
Parents = new List<string> { parentFolderId }
};
// For Google Workspace files, we need to handle them differently
if (mimeType != null && GoogleMimeTypes.IsGoogleDoc(mimeType))
{
// Create the file
var createRequest = driveService.Files.Create(fileMetadata);
createRequest.SupportsAllDrives = true;
var createdFile = await createRequest.ExecuteAsync(cancel);
return createdFile.Id;
}
else
{
// Upload binary content
var uploadRequest = driveService.Files.Create(fileMetadata, contentStream, mimeType ?? "application/octet-stream");
uploadRequest.SupportsAllDrives = true;
var uploadResult = await uploadRequest.UploadAsync(cancel);
if (uploadResult.Status == UploadStatus.Failed)
{
throw new Exception($"Failed to upload file: {uploadResult.Exception?.Message}");
}
return uploadRequest.ResponseBody?.Id;
}
}
public async Task<string?> UploadFileRevision(string? userId, string fileId, Stream contentStream, string? mimeType, CancellationToken cancel)
{
var driveService = Provider._apiHelper.GetDriveService(userId);
// For Google Workspace files, revisions cannot be uploaded directly
if (mimeType != null && GoogleMimeTypes.IsGoogleDoc(mimeType))
{
// Google Workspace files don't support direct revision uploads
// Revisions are created automatically when the file is edited
Log.WriteInformationMessage(LOGTAG, "UploadFileRevisionSkipGoogleDoc", "Skipping revision upload for Google Workspace file.");
return fileId;
}
// Upload new revision for binary files
var uploadRequest = driveService.Files.Update(new Google.Apis.Drive.v3.Data.File(), fileId, contentStream, mimeType ?? "application/octet-stream");
uploadRequest.SupportsAllDrives = true;
var uploadResult = await uploadRequest.UploadAsync(cancel);
if (uploadResult.Status == UploadStatus.Failed)
{
throw new Exception($"Failed to upload file revision: {uploadResult.Exception?.Message}");
}
return fileId;
}
public async Task<string?> CreateGoogleWorkspaceFile(string? userId, string parentFolderId, string name, string? mimeType, CancellationToken cancel)
{
var driveService = Provider._apiHelper.GetDriveService(userId);
var fileMetadata = new Google.Apis.Drive.v3.Data.File
{
Name = name,
MimeType = mimeType ?? GoogleMimeTypes.Document,
Parents = new List<string> { parentFolderId }
};
var createRequest = driveService.Files.Create(fileMetadata);
createRequest.SupportsAllDrives = true;
var createdFile = await createRequest.ExecuteAsync(cancel);
return createdFile.Id;
}
public async Task<string?> ImportGoogleWorkspaceFile(string? userId, string parentFolderId, string name, Stream contentStream, string? targetMimeType, CancellationToken cancel)
{
var driveService = Provider._apiHelper.GetDriveService(userId);
var fileMetadata = new Google.Apis.Drive.v3.Data.File
{
Name = name,
Parents = new List<string> { parentFolderId }
};
// Upload with convert=true to import the file as a Google Workspace file
var uploadRequest = driveService.Files.Create(fileMetadata, contentStream, targetMimeType ?? "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
uploadRequest.SupportsAllDrives = true;
var uploadResult = await uploadRequest.UploadAsync(cancel);
if (uploadResult.Status == UploadStatus.Failed)
{
throw new Exception($"Failed to import file: {uploadResult.Exception?.Message}");
}
return uploadRequest.ResponseBody?.Id;
}
public async Task UpdateFileMetadata(string? userId, string fileId, Stream metadataStream, CancellationToken cancel)
{
var driveService = Provider._apiHelper.GetDriveService(userId);
try
{
var metadata = await JsonSerializer.DeserializeAsync<Google.Apis.Drive.v3.Data.File>(metadataStream, cancellationToken: cancel);
if (metadata == null) return;
// Only update fields that can be modified
var updateMetadata = new Google.Apis.Drive.v3.Data.File
{
Name = metadata.Name,
Description = metadata.Description,
ModifiedTimeRaw = metadata.ModifiedTimeRaw,
// Note: CreatedTime cannot be changed after file creation
};
var updateRequest = driveService.Files.Update(updateMetadata, fileId);
updateRequest.SupportsAllDrives = true;
await updateRequest.ExecuteAsync(cancel);
}
catch (Exception ex)
{
Log.WriteWarningMessage(LOGTAG, "UpdateFileMetadataFailed", ex, $"Failed to update metadata for file {fileId}");
}
}
public async Task RestoreComments(string? userId, string fileId, Stream commentsStream, CancellationToken cancel)
{
var driveService = Provider._apiHelper.GetDriveService(userId);
try
{
var comments = await JsonSerializer.DeserializeAsync<Google.Apis.Drive.v3.Data.CommentList>(commentsStream, cancellationToken: cancel);
if (comments?.Comments == null) return;
foreach (var comment in comments.Comments)
{
try
{
var newComment = new Google.Apis.Drive.v3.Data.Comment
{
Content = comment.Content,
QuotedFileContent = comment.QuotedFileContent,
Anchor = comment.Anchor
};
await driveService.Comments.Create(newComment, fileId).ExecuteAsync(cancel);
}
catch (Exception ex)
{
Log.WriteWarningMessage(LOGTAG, "RestoreCommentFailed", ex, $"Failed to restore comment for file {fileId}");
}
}
}
catch (Exception ex)
{
Log.WriteWarningMessage(LOGTAG, "RestoreCommentsFailed", ex, $"Failed to restore comments for file {fileId}");
}
}
public async Task RestorePermissions(string? userId, string fileId, Stream permissionsStream, bool isSharedDrive, CancellationToken cancel)
{
var driveService = Provider._apiHelper.GetDriveService(userId);
PermissionList? permissionList;
try
{
permissionList = await JsonSerializer.DeserializeAsync<PermissionList>(permissionsStream, GoogleApiJsonDeserializer.Options, cancellationToken: cancel);
}
catch (Exception ex)
{
Log.WriteWarningMessage(LOGTAG, "RestorePermissionsFailed", ex, $"Failed to deserialize permissions for file {fileId}");
return;
}
if (permissionList?.Permissions == null || permissionList.Permissions.Count == 0) return;
foreach (var permission in permissionList.Permissions)
{
try
{
// Skip owner permissions (can't change ownership easily)
if (permission.Role == "owner") continue;
// Skip permissions with invalid email addresses (e.g., internal Google Chat/Spaces identifiers)
if (!string.IsNullOrEmpty(permission.EmailAddress) && !IsValidEmailAddress(permission.EmailAddress))
continue;
// The "fileOrganizer" role is only valid for shared drives
// Skip this role when restoring to a non-shared drive (e.g., user's My Drive)
if ((permission.Role == "fileOrganizer" || permission.Role == "organizer") && !isSharedDrive)
{
Log.WriteInformationMessage(LOGTAG, "RestorePermissionSkipOrganizer", $"Skipping fileOrganizer role for file {fileId} - not a shared drive");
continue;
}
var newPermission = new Permission
{
Role = permission.Role,
Type = permission.Type,
EmailAddress = permission.EmailAddress,
Domain = permission.Domain,
AllowFileDiscovery = permission.AllowFileDiscovery
};
var createPermissionRequest = driveService.Permissions.Create(newPermission, fileId);
createPermissionRequest.SupportsAllDrives = true;
await createPermissionRequest.ExecuteAsync(cancel);
}
catch (Exception ex)
{
Log.WriteWarningMessage(LOGTAG, "RestorePermissionFailed", ex, $"Failed to restore permission for file {fileId}");
}
}
}
private static bool IsValidEmailAddress(string email)
{
// Basic email validation - must contain @ and not contain path-like prefixes
// This filters out internal Google identifiers like "/namespaced-roster/..."
return email.Contains('@') && !email.StartsWith('/');
}
}
private async Task RestoreDriveFolders(CancellationToken cancel)
{
if (RestoreTarget == null)
throw new InvalidOperationException("Restore target entry is not set");
var folders = GetMetadataByType(SourceItemType.DriveFolder);
if (folders.Count == 0)
return;
var defaultFolderId = await DriveRestore.GetDefaultFolder(cancel);
string? userId = null;
if (RestoreTarget.Type == SourceItemType.User)
{
userId = RestoreTarget.Metadata.GetValueOrDefault("gsuite:Id");
}
else if (RestoreTarget.Type == SourceItemType.UserDrive)
{
userId = RestoreTarget.Path.TrimStart(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar).Skip(1).FirstOrDefault();
}
else if (RestoreTarget.Type == SourceItemType.DriveFolder)
{
userId = RestoreTarget.Metadata.GetValueOrDefault("gsuite:UserId");
}
// For shared drive folders, userId may be null - we can still restore using the service account
// or admin credentials without user impersonation
if (string.IsNullOrWhiteSpace(userId) && RestoreTarget.Type != SourceItemType.DriveFolder)
{
Log.WriteWarningMessage(LOGTAG, "RestoreDriveFoldersMissingUser", null, $"Could not determine target user for Drive folder restore ({RestoreTarget.Type}).");
return;
}
// Sort folders by path length to ensure parents are created before children
var sortedFolders = folders.OrderBy(k => k.Key.Split(Path.DirectorySeparatorChar).Length).ToList();
foreach (var folder in sortedFolders)
{
if (cancel.IsCancellationRequested)
break;
try
{
var originalPath = folder.Key;
var metadata = folder.Value;
var displayName = metadata.GetValueOrDefault("gsuite:Name") ?? Path.GetFileName(originalPath.TrimEnd(Path.DirectorySeparatorChar));
_metadata.TryRemove(originalPath, out _);
if (string.IsNullOrWhiteSpace(displayName))
{
Log.WriteWarningMessage(LOGTAG, "RestoreDriveFoldersMissingName", null, $"Missing display name for folder {originalPath}, skipping.");
continue;
}
// Determine parent folder ID
var parentPath = Util.AppendDirSeparator(Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)) ?? "");
var parentId = defaultFolderId ?? "root";
if (parentPath != null && _restoredDriveFolderMap.TryGetValue(parentPath, out var mappedParentId))
{
parentId = mappedParentId;
}
var newFolderId = await DriveRestore.CreateFolder(userId, parentId, displayName, cancel);
if (newFolderId != null)
{
_restoredDriveFolderMap[originalPath] = newFolderId;
}
}
catch (Exception ex)
{
Log.WriteErrorMessage(LOGTAG, "RestoreDriveFoldersFailed", ex, $"Failed to restore folder {folder.Key}");
}
}
}
private async Task RestoreDriveFiles(CancellationToken cancel)
{
if (RestoreTarget == null)
throw new InvalidOperationException("Restore target entry is not set");
var files = GetMetadataByType(SourceItemType.DriveFile);
if (files.Count == 0)
return;
var defaultFolderId = await DriveRestore.GetDefaultFolder(cancel);
string? userId = null;
if (RestoreTarget.Type == SourceItemType.User)
{
userId = RestoreTarget.Metadata.GetValueOrDefault("gsuite:Id");
}
else if (RestoreTarget.Type == SourceItemType.UserDrive)
{
userId = RestoreTarget.Path.TrimStart(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar).Skip(1).FirstOrDefault();
}
else if (RestoreTarget.Type == SourceItemType.DriveFolder)
{
userId = RestoreTarget.Metadata.GetValueOrDefault("gsuite:UserId");
}
// For shared drive folders, userId may be null - we can still restore using the service account
// or admin credentials without user impersonation
if (string.IsNullOrWhiteSpace(userId) && RestoreTarget.Type != SourceItemType.DriveFolder)
{
Log.WriteWarningMessage(LOGTAG, "RestoreDriveFilesMissingUser", null, $"Could not determine target user for Drive file restore ({RestoreTarget.Type}).");
return;
}
foreach (var file in files)
{
if (cancel.IsCancellationRequested)
break;
try
{
var originalPath = file.Key;
var metadata = file.Value;
var displayName = metadata.GetValueOrDefault("gsuite:Name") ?? Path.GetFileName(originalPath);
var mimeType = metadata.GetValueOrDefault("gsuite:MimeType");
// Determine parent folder ID
var parentPath = Util.AppendDirSeparator(Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)) ?? "");
var parentId = defaultFolderId ?? "root";
if (parentPath != null && _restoredDriveFolderMap.TryGetValue(parentPath, out var mappedParentId))
{
parentId = mappedParentId;
}
// Get the content file path (stored at originalPath + "/content")
var contentPath = SystemIO.IO_OS.PathCombine(originalPath, "content");
var contentEntry = _temporaryFiles.GetValueOrDefault(contentPath);
string? fileId = null;
if (contentEntry != null)
{
// For Google Workspace files, the content is an exported format that needs to be imported
if (GoogleMimeTypes.IsGoogleDoc(mimeType))
{
// Get the export MIME type for the Google Workspace file
var exportMimeType = GoogleMimeTypes.GetExportMimeType(mimeType!);
using (var contentStream = SystemIO.IO_OS.FileOpenRead(contentEntry))
{
fileId = await DriveRestore.ImportGoogleWorkspaceFile(userId, parentId, displayName, contentStream, exportMimeType, cancel);
}
}
else
{
// For regular binary files, upload directly
using (var contentStream = SystemIO.IO_OS.FileOpenRead(contentEntry))
{
fileId = await DriveRestore.UploadFile(userId, parentId, displayName, contentStream, mimeType, cancel);
}
}
_metadata.TryRemove(contentPath, out _);
_temporaryFiles.TryRemove(contentPath, out var contentFile);
contentFile?.Dispose();
}
else
{
// No content available, create empty Google Workspace file if applicable
if (mimeType != null && GoogleMimeTypes.IsGoogleDoc(mimeType))
{
fileId = await DriveRestore.CreateGoogleWorkspaceFile(userId, parentId, displayName, mimeType, cancel);
}
else
{
Log.WriteWarningMessage(LOGTAG, "RestoreDriveFilesMissingContent", null, $"Missing content for file {originalPath}, skipping.");
continue;
}
}
if (fileId != null)
{
_restoredDriveFileMap[originalPath] = fileId;
// Now restore any revisions for this file
await RestoreDriveFileRevisions(userId, fileId, originalPath, mimeType, cancel);
// Restore metadata (modified time, description, etc.)
await RestoreDriveFileMetadata(userId, fileId, originalPath, cancel);
// Restore comments
await RestoreDriveFileComments(userId, fileId, originalPath, cancel);
}
_metadata.TryRemove(originalPath, out _);
}
catch (Exception ex)
{
Log.WriteErrorMessage(LOGTAG, "RestoreDriveFilesFailed", ex, $"Failed to restore file {file.Key}");
}
}
}
private async Task RestoreDriveFileRevisions(string? userId, string fileId, string originalFilePath, string? mimeType, CancellationToken cancel)
{
// Find all revisions for this file
// Revisions are stored at originalFilePath + "/Revisions/" + revisionId
var revisionPrefix = SystemIO.IO_OS.PathCombine(originalFilePath, "Revisions");
var revisionEntries = _temporaryFiles
.Where(kv => kv.Key.StartsWith(revisionPrefix + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
.ToList();
if (revisionEntries.Count == 0)
return;
// Sort revisions by path to maintain chronological order
var sortedRevisions = revisionEntries.OrderBy(r => r.Key).ToList();
foreach (var revisionEntry in sortedRevisions)
{
if (cancel.IsCancellationRequested)
break;
try
{
using (var contentStream = SystemIO.IO_OS.FileOpenRead(revisionEntry.Value))
{
await DriveRestore.UploadFileRevision(userId, fileId, contentStream, mimeType, cancel);
}
_temporaryFiles.TryRemove(revisionEntry.Key, out var tempFile);
tempFile?.Dispose();
// Also remove from metadata if present
_metadata.TryRemove(revisionEntry.Key, out _);
}
catch (Exception ex)
{
Log.WriteWarningMessage(LOGTAG, "RestoreDriveFileRevisionFailed", ex, $"Failed to restore revision {revisionEntry.Key} for file {fileId}");
}
}
// Clean up the Revisions folder metadata entry if it exists
var revisionsFolderPath = Util.AppendDirSeparator(revisionPrefix);
_metadata.TryRemove(revisionsFolderPath, out _);
}
private async Task RestoreDriveFileMetadata(string? userId, string fileId, string originalFilePath, CancellationToken cancel)
{
var metadataPath = SystemIO.IO_OS.PathCombine(originalFilePath, "metadata.json");
var metadataEntry = _temporaryFiles.GetValueOrDefault(metadataPath);
if (metadataEntry == null)
return;
try
{
using (var contentStream = SystemIO.IO_OS.FileOpenRead(metadataEntry))
{
await DriveRestore.UpdateFileMetadata(userId, fileId, contentStream, cancel);
}
_temporaryFiles.TryRemove(metadataPath, out var tempFile);
tempFile?.Dispose();
// Also remove from metadata if present
_metadata.TryRemove(metadataPath, out _);
}
catch (Exception ex)
{
Log.WriteWarningMessage(LOGTAG, "RestoreDriveFileMetadataFailed", ex, $"Failed to restore metadata for file {fileId}");
}
}
private async Task RestoreDriveFileComments(string? userId, string fileId, string originalFilePath, CancellationToken cancel)
{
var commentsPath = SystemIO.IO_OS.PathCombine(originalFilePath, "comments.json");
var commentsEntry = _temporaryFiles.GetValueOrDefault(commentsPath);
if (commentsEntry == null)
return;
try
{
using (var contentStream = SystemIO.IO_OS.FileOpenRead(commentsEntry))
{
await DriveRestore.RestoreComments(userId, fileId, contentStream, cancel);
}
_temporaryFiles.TryRemove(commentsPath, out var tempFile);
tempFile?.Dispose();
// Also remove from metadata if present
_metadata.TryRemove(commentsPath, out _);
}
catch (Exception ex)
{
Log.WriteWarningMessage(LOGTAG, "RestoreDriveFileCommentsFailed", ex, $"Failed to restore comments for file {fileId}");
}
}
private async Task RestoreDrivePermissions(CancellationToken cancel)
{
if (RestoreTarget == null)
throw new InvalidOperationException("Restore target entry is not set");
var permissions = GetMetadataByType(SourceItemType.DrivePermission);
if (permissions.Count == 0)
return;
string? userId = null;
if (RestoreTarget.Type == SourceItemType.User)
{
userId = RestoreTarget.Metadata.GetValueOrDefault("gsuite:Id");
}
else if (RestoreTarget.Type == SourceItemType.UserDrive)
{
userId = RestoreTarget.Path.TrimStart(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar).Skip(1).FirstOrDefault();
}
else if (RestoreTarget.Type == SourceItemType.DriveFolder)
{
userId = RestoreTarget.Metadata.GetValueOrDefault("gsuite:UserId");
}
// For shared drive folders, userId may be null - we can still restore using the service account
// or admin credentials without user impersonation
if (string.IsNullOrWhiteSpace(userId) && RestoreTarget.Type != SourceItemType.DriveFolder)
{
Log.WriteWarningMessage(LOGTAG, "RestoreDrivePermissionsMissingUser", null, $"Could not determine target user for Drive permission restore ({RestoreTarget.Type}).");
return;
}
// Determine if the restore target is a shared drive
// The "fileOrganizer" role is only valid for shared drives
bool isSharedDrive = RestoreTarget.Type == SourceItemType.SharedDrives || RestoreTarget.Type == SourceItemType.DriveFolder;
foreach (var permission in permissions)
{
if (cancel.IsCancellationRequested)
break;
try
{
var originalPath = permission.Key;
var contentEntry = _temporaryFiles.GetValueOrDefault(originalPath);
if (contentEntry == null)
{
Log.WriteWarningMessage(LOGTAG, "RestoreDrivePermissionsMissingContent", null, $"Missing content for permission {originalPath}, skipping.");
continue;
}
// Get the file ID from parent path
var parentPath = Util.AppendDirSeparator(Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)) ?? "");
string? fileId = null;
if (parentPath != null)
{
// Check if it's a folder
if (_restoredDriveFolderMap.TryGetValue(parentPath, out var folderId))
{
fileId = folderId;
}
// Check if it's a file (using the restored file map)
else if (_restoredDriveFileMap.TryGetValue(parentPath, out var restoredFileId))
{
fileId = restoredFileId;
}
else
{
// It might be a file - check metadata (fallback for older backups)
var fileMetadata = _metadata.FirstOrDefault(m => m.Key == parentPath.TrimEnd(Path.DirectorySeparatorChar));
if (!string.IsNullOrEmpty(fileMetadata.Key))
{
fileId = fileMetadata.Value.GetValueOrDefault("gsuite:Id");
}
}
}
if (string.IsNullOrWhiteSpace(fileId))
{
Log.WriteWarningMessage(LOGTAG, "RestoreDrivePermissionsMissingFileId", null, $"Could not find file ID for permission {originalPath}, skipping.");
continue;
}
using (var contentStream = SystemIO.IO_OS.FileOpenRead(contentEntry))
{
await DriveRestore.RestorePermissions(userId, fileId, contentStream, isSharedDrive, cancel);
}
_metadata.TryRemove(originalPath, out _);
_temporaryFiles.TryRemove(originalPath, out var contentFile);
contentFile?.Dispose();
}
catch (Exception ex)
{
Log.WriteErrorMessage(LOGTAG, "RestoreDrivePermissionsFailed", ex, $"Failed to restore permission {permission.Key}");
}
}
}
}