diff --git a/proprietary/GoogleWorkspace/RestoreProvider/RestoreProvider.Drive.cs b/proprietary/GoogleWorkspace/RestoreProvider/RestoreProvider.Drive.cs index ec3e86548..7400b7be7 100644 --- a/proprietary/GoogleWorkspace/RestoreProvider/RestoreProvider.Drive.cs +++ b/proprietary/GoogleWorkspace/RestoreProvider/RestoreProvider.Drive.cs @@ -34,11 +34,7 @@ partial class RestoreProvider string? driveId = null; string? userId = null; - if (target.Type == SourceItemType.Drive) - { - driveId = target.Metadata.GetValueOrDefault("gsuite:Id"); - } - else if (target.Type == SourceItemType.DriveFolder) + if (target.Type == SourceItemType.DriveFolder) { driveId = target.Metadata.GetValueOrDefault("gsuite:DriveId"); _cachedDefaultFolderId = target.Metadata.GetValueOrDefault("gsuite:Id"); diff --git a/proprietary/GoogleWorkspace/RestoreProvider/RestoreProvider.cs b/proprietary/GoogleWorkspace/RestoreProvider/RestoreProvider.cs index 2875b5be7..8990c27f2 100644 --- a/proprietary/GoogleWorkspace/RestoreProvider/RestoreProvider.cs +++ b/proprietary/GoogleWorkspace/RestoreProvider/RestoreProvider.cs @@ -517,7 +517,6 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule case SourceItemType.UserTasks: case SourceItemType.UserKeep: case SourceItemType.UserChat: - case SourceItemType.Drive: case SourceItemType.Calendar: toRemove.Add(kv.Key); break; diff --git a/proprietary/GoogleWorkspace/SourceItems/ChatSourceEntry.cs b/proprietary/GoogleWorkspace/SourceItems/ChatSourceEntry.cs deleted file mode 100644 index 484a835fc..000000000 --- a/proprietary/GoogleWorkspace/SourceItems/ChatSourceEntry.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2026 Duplicati Inc. All rights reserved. - -using Duplicati.Library.Common.IO; -using Duplicati.Library.Interface; -using Google.Apis.Drive.v3; -using Google.Apis.HangoutsChat.v1; -using System.Runtime.CompilerServices; - -namespace Duplicati.Proprietary.GoogleWorkspace.SourceItems; - -internal class ChatSourceEntry(string parentPath, HangoutsChatService chatService, DriveService driveService) - : MetaEntryBase(Util.AppendDirSeparator(SystemIO.IO_OS.PathCombine(parentPath, "Chat")), null, null) -{ - public override async IAsyncEnumerable Enumerate([EnumeratorCancellation] CancellationToken cancellationToken) - { - // Create a folder for each space type to organize the structure - var spaceTypes = new[] { "SPACE", "GROUP_CHAT", "DIRECT_MESSAGE" }; - - foreach (var spaceType in spaceTypes) - { - if (cancellationToken.IsCancellationRequested) yield break; - yield return new ChatSpaceTypeSourceEntry(this.Path, spaceType, chatService, driveService); - } - } - - public override Task> GetMinorMetadata(CancellationToken cancellationToken) - { - return Task.FromResult(new Dictionary - { - { "gsuite:v", "1" }, - { "gsuite:Type", SourceItemType.UserChat.ToString() }, - { "gsuite:Name", "Chat" }, - { "gsuite:Id", "Chat" } - } - .Where(kv => !string.IsNullOrEmpty(kv.Value)) - .ToDictionary(kv => kv.Key, kv => kv.Value)); - } -} diff --git a/proprietary/GoogleWorkspace/SourceItems/ContactsFolderSourceEntry.cs b/proprietary/GoogleWorkspace/SourceItems/ContactsFolderSourceEntry.cs index 1b0ed0d4f..02808e404 100644 --- a/proprietary/GoogleWorkspace/SourceItems/ContactsFolderSourceEntry.cs +++ b/proprietary/GoogleWorkspace/SourceItems/ContactsFolderSourceEntry.cs @@ -8,7 +8,7 @@ using System.Runtime.CompilerServices; namespace Duplicati.Proprietary.GoogleWorkspace.SourceItems; internal class ContactsFolderSourceEntry(string parentPath, PeopleServiceService peopleService) - : MetaEntryBase(Util.AppendDirSeparator(SystemIO.IO_OS.PathCombine(parentPath, "Contacts")), null, null) + : MetaEntryBase(Util.AppendDirSeparator(SystemIO.IO_OS.PathCombine(parentPath, "Entries")), null, null) { public override async IAsyncEnumerable Enumerate([EnumeratorCancellation] CancellationToken cancellationToken) { @@ -40,8 +40,7 @@ internal class ContactsFolderSourceEntry(string parentPath, PeopleServiceService { { "gsuite:v", "1" }, { "gsuite:Type", SourceItemType.UserContacts.ToString() }, - { "gsuite:Name", "Contacts" }, - { "gsuite:Id", "Contacts" } + { "gsuite:Name", "Entries" } } .Where(kv => !string.IsNullOrEmpty(kv.Value)) .ToDictionary(kv => kv.Key, kv => kv.Value)); diff --git a/proprietary/GoogleWorkspace/SourceItems/DriveSourceEntry.cs b/proprietary/GoogleWorkspace/SourceItems/DriveSourceEntry.cs deleted file mode 100644 index be58669b8..000000000 --- a/proprietary/GoogleWorkspace/SourceItems/DriveSourceEntry.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2026 Duplicati Inc. All rights reserved. - -using Duplicati.Library.Common.IO; -using Duplicati.Library.Interface; -using Google.Apis.Drive.v3; -using System.Runtime.CompilerServices; - -namespace Duplicati.Proprietary.GoogleWorkspace.SourceItems; - -internal class DriveSourceEntry(SourceProvider provider, string parentPath, string userId, DriveService driveService) - : MetaEntryBase(Util.AppendDirSeparator(SystemIO.IO_OS.PathCombine(parentPath, "Drive")), null, null) -{ - public override async IAsyncEnumerable Enumerate([EnumeratorCancellation] CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) yield break; - yield return new DriveFolderSourceEntry(this.Path, userId, "My Drive", "root", driveService); - - if (cancellationToken.IsCancellationRequested) yield break; - yield return new SharedDrivesSourceEntry(provider, this.Path, userId, driveService); - } - - public override Task> GetMinorMetadata(CancellationToken cancellationToken) - { - return Task.FromResult(new Dictionary - { - { "gsuite:v", "1" }, - { "gsuite:Type", SourceItemType.Drive.ToString() }, - { "gsuite:Name", "Drive" }, - { "gsuite:Id", "Drive" } - } - .Where(kv => !string.IsNullOrEmpty(kv.Value)) - .ToDictionary(kv => kv.Key, kv => kv.Value)); - } -} diff --git a/proprietary/GoogleWorkspace/SourceItems/KeepSourceEntry.cs b/proprietary/GoogleWorkspace/SourceItems/KeepSourceEntry.cs deleted file mode 100644 index b8118e297..000000000 --- a/proprietary/GoogleWorkspace/SourceItems/KeepSourceEntry.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2026 Duplicati Inc. All rights reserved. - -using Duplicati.Library.Common.IO; -using Duplicati.Library.Interface; -using Google.Apis.Keep.v1; -using System.Runtime.CompilerServices; - -namespace Duplicati.Proprietary.GoogleWorkspace.SourceItems; - -internal class KeepSourceEntry(string parentPath, KeepService keepService) - : MetaEntryBase(Util.AppendDirSeparator(SystemIO.IO_OS.PathCombine(parentPath, "Keep")), null, null) -{ - public override async IAsyncEnumerable Enumerate([EnumeratorCancellation] CancellationToken cancellationToken) - { - var request = keepService.Notes.List(); - - string? nextPageToken = null; - do - { - if (cancellationToken.IsCancellationRequested) yield break; - request.PageToken = nextPageToken; - var notes = await request.ExecuteAsync(cancellationToken); - - if (notes.Notes != null) - { - foreach (var note in notes.Notes) - { - if (cancellationToken.IsCancellationRequested) yield break; - yield return new KeepNoteSourceEntry(this.Path, note, keepService); - } - } - nextPageToken = notes.NextPageToken; - } while (!string.IsNullOrEmpty(nextPageToken)); - } - - public override Task> GetMinorMetadata(CancellationToken cancellationToken) - { - return Task.FromResult(new Dictionary - { - { "gsuite:v", "1" }, - { "gsuite:Type", SourceItemType.UserKeep.ToString() }, - { "gsuite:Name", "Keep" }, - { "gsuite:Id", "Keep" } - } - .Where(kv => !string.IsNullOrEmpty(kv.Value)) - .ToDictionary(kv => kv.Key, kv => kv.Value)); - } -} diff --git a/proprietary/GoogleWorkspace/SourceItems/MetaRootSourceEntry.cs b/proprietary/GoogleWorkspace/SourceItems/MetaRootSourceEntry.cs index afc43837b..74025528b 100644 --- a/proprietary/GoogleWorkspace/SourceItems/MetaRootSourceEntry.cs +++ b/proprietary/GoogleWorkspace/SourceItems/MetaRootSourceEntry.cs @@ -2,7 +2,6 @@ using Duplicati.Library.Common.IO; using Duplicati.Library.Interface; -using Google.Apis.Drive.v3; using System.Runtime.CompilerServices; namespace Duplicati.Proprietary.GoogleWorkspace.SourceItems; @@ -77,11 +76,50 @@ internal class MetaRootSourceEntry(SourceProvider provider, string parentPath, s else if (type == SourceItemType.MetaRootSharedDrives) { var driveService = provider.ApiHelper.GetDriveService(); - yield return new SharedDrivesSourceEntry(provider, this.Path, null, driveService); + await foreach (var n in SharedDrivesSourceEntry.EnumerateSharedDrives(provider, this.Path, null, driveService, cancellationToken)) + yield return n; } else if (type == SourceItemType.MetaRootSites) { - yield return new SitesSourceEntry(provider, this.Path); + var service = provider.ApiHelper.GetDriveService(); + var request = service.Files.List(); + request.Q = $"mimeType='{GoogleMimeTypes.Site}' and trashed=false"; + request.SupportsAllDrives = true; + request.IncludeItemsFromAllDrives = true; + // Request all drives so we do not get user drives only + request.Corpora = "allDrives"; + + string? nextPageToken = null; + do + { + if (cancellationToken.IsCancellationRequested) yield break; + request.PageToken = nextPageToken; + + Google.Apis.Drive.v3.Data.FileList? files = null; + try + { + files = await request.ExecuteAsync(cancellationToken); + } + catch + { + // Fallback or ignore + yield break; + } + + if (files != null && files.Files != null) + { + foreach (var file in files.Files) + { + if (cancellationToken.IsCancellationRequested) yield break; + + if (!provider.LicenseApprovedForEntry(Path, GoogleRootType.Sites, file.Id)) + yield break; + + yield return new SiteSourceEntry(this.Path, file); + } + } + nextPageToken = files?.NextPageToken; + } while (!string.IsNullOrEmpty(nextPageToken)); } else if (type == SourceItemType.MetaRootOrganizationalUnits) { diff --git a/proprietary/GoogleWorkspace/SourceItems/SharedDrivesSourceEntry.cs b/proprietary/GoogleWorkspace/SourceItems/SharedDrivesSourceEntry.cs index f338840c0..e5abfc9b2 100644 --- a/proprietary/GoogleWorkspace/SourceItems/SharedDrivesSourceEntry.cs +++ b/proprietary/GoogleWorkspace/SourceItems/SharedDrivesSourceEntry.cs @@ -10,7 +10,10 @@ namespace Duplicati.Proprietary.GoogleWorkspace.SourceItems; internal class SharedDrivesSourceEntry(SourceProvider provider, string parentPath, string? userId, DriveService driveService) : MetaEntryBase(Util.AppendDirSeparator(SystemIO.IO_OS.PathCombine(parentPath, "Shared Drives")), null, null) { - public override async IAsyncEnumerable Enumerate([EnumeratorCancellation] CancellationToken cancellationToken) + public override IAsyncEnumerable Enumerate(CancellationToken cancellationToken) + => EnumerateSharedDrives(provider, this.Path, userId, driveService, cancellationToken); + + public static async IAsyncEnumerable EnumerateSharedDrives(SourceProvider provider, string path, string? userId, DriveService driveService, [EnumeratorCancellation] CancellationToken cancellationToken) { var request = driveService.Drives.List(); @@ -27,10 +30,10 @@ internal class SharedDrivesSourceEntry(SourceProvider provider, string parentPat { if (cancellationToken.IsCancellationRequested) yield break; - if (!provider.LicenseApprovedForEntry(Path, GoogleRootType.SharedDrives, drive.Id)) + if (!provider.LicenseApprovedForEntry(path, GoogleRootType.SharedDrives, drive.Id)) yield break; - yield return new SharedDriveSourceEntry(this.Path, userId!, drive, driveService); + yield return new SharedDriveSourceEntry(path, userId!, drive, driveService); } } nextPageToken = drives.NextPageToken; diff --git a/proprietary/GoogleWorkspace/SourceItems/SitesSourceEntry.cs b/proprietary/GoogleWorkspace/SourceItems/SitesSourceEntry.cs deleted file mode 100644 index b99c71643..000000000 --- a/proprietary/GoogleWorkspace/SourceItems/SitesSourceEntry.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2026 Duplicati Inc. All rights reserved. - -using Duplicati.Library.Common.IO; -using Duplicati.Library.Interface; -using System.Runtime.CompilerServices; - -namespace Duplicati.Proprietary.GoogleWorkspace.SourceItems; - -internal class SitesSourceEntry(SourceProvider provider, string parentPath) - : MetaEntryBase(Util.AppendDirSeparator(SystemIO.IO_OS.PathCombine(parentPath, "Sites")), null, null) -{ - public override async IAsyncEnumerable Enumerate([EnumeratorCancellation] CancellationToken cancellationToken) - { - var service = provider.ApiHelper.GetDriveService(); - var request = service.Files.List(); - request.Q = $"mimeType='{GoogleMimeTypes.Site}' and trashed=false"; - request.SupportsAllDrives = true; - request.IncludeItemsFromAllDrives = true; - // Request all drives so we do not get user drives only - request.Corpora = "allDrives"; - - string? nextPageToken = null; - do - { - if (cancellationToken.IsCancellationRequested) yield break; - request.PageToken = nextPageToken; - - Google.Apis.Drive.v3.Data.FileList? files = null; - try - { - files = await request.ExecuteAsync(cancellationToken); - } - catch - { - // Fallback or ignore - yield break; - } - - if (files != null && files.Files != null) - { - foreach (var file in files.Files) - { - if (cancellationToken.IsCancellationRequested) yield break; - - if (!provider.LicenseApprovedForEntry(Path, GoogleRootType.Sites, file.Id)) - yield break; - - yield return new SiteSourceEntry(this.Path, file); - } - } - nextPageToken = files?.NextPageToken; - } while (!string.IsNullOrEmpty(nextPageToken)); - } - - public override Task> GetMinorMetadata(CancellationToken cancellationToken) - { - return Task.FromResult(new Dictionary - { - { "gsuite:v", "1" }, - { "gsuite:Type", SourceItemType.MetaRootSites.ToString() }, - { "gsuite:Name", "Sites" }, - { "gsuite:Id", "Sites" } - } - .Where(kv => !string.IsNullOrEmpty(kv.Value)) - .ToDictionary(kv => kv.Key, kv => kv.Value)); - } -} diff --git a/proprietary/GoogleWorkspace/SourceItems/SourceItemType.cs b/proprietary/GoogleWorkspace/SourceItems/SourceItemType.cs index 80c4921bc..c2d282f11 100644 --- a/proprietary/GoogleWorkspace/SourceItems/SourceItemType.cs +++ b/proprietary/GoogleWorkspace/SourceItems/SourceItemType.cs @@ -20,7 +20,6 @@ public enum SourceItemType GmailMessage, GmailAttachment, GmailSettings, - Drive, DriveFile, DriveFolder, DrivePermission, diff --git a/proprietary/GoogleWorkspace/SourceItems/UserTypeSourceEntry.cs b/proprietary/GoogleWorkspace/SourceItems/UserTypeSourceEntry.cs index df6dd1911..63313e508 100644 --- a/proprietary/GoogleWorkspace/SourceItems/UserTypeSourceEntry.cs +++ b/proprietary/GoogleWorkspace/SourceItems/UserTypeSourceEntry.cs @@ -105,7 +105,10 @@ internal class UserTypeSourceEntry(SourceProvider provider, string parentPath, s var driveService = provider.ApiHelper.GetDriveService(userId); if (cancellationToken.IsCancellationRequested) yield break; - yield return new DriveSourceEntry(provider, this.Path, userId, driveService); + yield return new DriveFolderSourceEntry(this.Path, userId, "My Drive", "root", driveService); + + if (cancellationToken.IsCancellationRequested) yield break; + yield return new SharedDrivesSourceEntry(provider, this.Path, userId, driveService); } private async IAsyncEnumerable EnumerateTasks([EnumeratorCancellation] CancellationToken cancellationToken) @@ -134,10 +137,29 @@ internal class UserTypeSourceEntry(SourceProvider provider, string parentPath, s private async IAsyncEnumerable EnumerateKeep([EnumeratorCancellation] CancellationToken cancellationToken) { - var service = provider.ApiHelper.GetKeepService(userId); - if (cancellationToken.IsCancellationRequested) yield break; - yield return new KeepSourceEntry(this.Path, service); + + var service = provider.ApiHelper.GetKeepService(userId); + var request = service.Notes.List(); + + string? nextPageToken = null; + do + { + if (cancellationToken.IsCancellationRequested) yield break; + request.PageToken = nextPageToken; + var notes = await request.ExecuteAsync(cancellationToken); + + if (notes.Notes != null) + { + foreach (var note in notes.Notes) + { + if (cancellationToken.IsCancellationRequested) yield break; + yield return new KeepNoteSourceEntry(this.Path, note, service); + } + } + nextPageToken = notes.NextPageToken; + } while (!string.IsNullOrEmpty(nextPageToken)); + } private async IAsyncEnumerable EnumerateChat([EnumeratorCancellation] CancellationToken cancellationToken) @@ -145,8 +167,11 @@ internal class UserTypeSourceEntry(SourceProvider provider, string parentPath, s var service = provider.ApiHelper.GetChatService(userId); var driveService = provider.ApiHelper.GetDriveService(userId); - if (cancellationToken.IsCancellationRequested) yield break; - yield return new ChatSourceEntry(this.Path, service, driveService); + foreach (var spaceType in new[] { "SPACE", "GROUP_CHAT", "DIRECT_MESSAGE" }) + { + if (cancellationToken.IsCancellationRequested) yield break; + yield return new ChatSpaceTypeSourceEntry(this.Path, spaceType, service, driveService); + } } public override Task> GetMinorMetadata(CancellationToken cancellationToken) diff --git a/proprietary/GoogleWorkspace/WebModule/WebModule.cs b/proprietary/GoogleWorkspace/WebModule/WebModule.cs index 9b5c5ff1e..0a3cb8f30 100644 --- a/proprietary/GoogleWorkspace/WebModule/WebModule.cs +++ b/proprietary/GoogleWorkspace/WebModule/WebModule.cs @@ -37,7 +37,6 @@ public class WebModule : IWebModule SourceItemType.UserKeep.ToString(), SourceItemType.UserChat.ToString(), SourceItemType.GmailLabel.ToString(), - SourceItemType.Drive.ToString(), SourceItemType.DriveFolder.ToString(), SourceItemType.Calendar.ToString(), SourceItemType.TaskList.ToString(),