From 25ac368ff034c4068dbc93f45930fdebcf5c8b61 Mon Sep 17 00:00:00 2001 From: Jackabomb <48334975+Jackabomb@users.noreply.github.com> Date: Thu, 13 Apr 2023 13:38:47 -0700 Subject: [PATCH 1/4] Added shortcut handling for Google Drive -GetChildrenByParentItem now resolves (aka "passes-through") shortcuts. -That is, it knows how to identify shortcuts and returns the object they point to rather than the zero-byte, fairly useless shortcut itself. -Shortcuts appear as the type (file vs. folder) of the underlying target and can be navigated and chosen in the open from cloud dialog. -Added shortcut dereferencing to GetFileByPath. This makes shortcuts work with opening and saving. Note that copy and delete might not do what you expect. They will operate on the final target, not on the shortcut itself. In particular, if you try to delete a shortcut to a folder, KeeAnywhere will instead try to delete the underlying real folder! This will fail for folders not owned by you (i.e. shared with you, with a shortcut in My Drive), but it may succeed for your own shortcut-ed (i.e. symlinked) folders! Be warned! This behavior should probably be considered a defect and fixed. --- .../GoogleDrive/GoogleDriveHelper.cs | 10 ++++++++++ .../GoogleDrive/GoogleDriveStorageProvider.cs | 19 +++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveHelper.cs b/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveHelper.cs index 908e8a4..8f5a7b3 100644 --- a/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveHelper.cs +++ b/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveHelper.cs @@ -102,6 +102,16 @@ public static async Task GetFileByPath(this DriveService api, string path) file = result.Files.FirstOrDefault(); if (file == null) return null; + if (file.MimeType == "application/vnd.google-apps.shortcut") + { + if (file.ShortcutDetails == null) + { + var fileQuery = api.Files.Get(file.Id); + fileQuery.Fields = "*"; + file = await fileQuery.ExecuteAsync(); + } + file = await api.Files.Get(file.ShortcutDetails.TargetId).ExecuteAsync(); + } } return file; diff --git a/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveStorageProvider.cs b/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveStorageProvider.cs index 97d06fa..a78684c 100644 --- a/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveStorageProvider.cs +++ b/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveStorageProvider.cs @@ -129,16 +129,27 @@ public async Task> GetChildrenByParentItem(Stor var api = await GetApi(); var query = api.Files.List(); query.Q = string.Format("'{0}' in parents and trashed = false", parent.Id); + query.Fields = "*"; var items = await query.ExecuteAsync(); var newItems = items.Files.Select(_ => new StorageProviderItem() { - Id = _.Id, + Id = + _.MimeType == "application/vnd.google-apps.shortcut" + ? + _.ShortcutDetails != null + ? _.ShortcutDetails.TargetId + : "1" + : _.Id, Name = _.Name, Type = - _.MimeType == "application/vnd.google-apps.folder" - ? StorageProviderItemType.Folder - : StorageProviderItemType.File, + _.MimeType == "application/vnd.google-apps.shortcut" + ? _.ShortcutDetails.TargetMimeType == "application/vnd.google-apps.folder" + ? StorageProviderItemType.Folder + : StorageProviderItemType.File + : _.MimeType == "application/vnd.google-apps.folder" + ? StorageProviderItemType.Folder + : StorageProviderItemType.File, LastModifiedDateTime = _.ModifiedTime, ParentReferenceId = parent.Id, }); From d4db28e7530a4452ea465731a75748617f3c5ecd Mon Sep 17 00:00:00 2001 From: Jackabomb <48334975+Jackabomb@users.noreply.github.com> Date: Mon, 17 Apr 2023 06:59:57 -0700 Subject: [PATCH 2/4] Fixed delete, so that it acts on the shortcut itself instead of the underlying file/folder. Added a resolveFinalShortcut parameter to GetFileByPath so that we can select whether or not the last element in a path should be resolved or not. If this parameter is set to true, shortcuts are always resolved, and the underlying file is always returned. If this parameter is set to false, shortcuts in the path are resolved, but not the last element. --- .../GoogleDrive/GoogleDriveHelper.cs | 30 +++++++++++-------- .../GoogleDrive/GoogleDriveStorageProvider.cs | 14 ++++----- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveHelper.cs b/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveHelper.cs index 8f5a7b3..70c7eab 100644 --- a/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveHelper.cs +++ b/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveHelper.cs @@ -86,13 +86,14 @@ public static async Task GetClient(TokenResponse token) return client; } - public static async Task GetFileByPath(this DriveService api, string path) + public static async Task GetFileByPath(this DriveService api, string path, bool resolveFinalShortcut) { var parts = path.Split('/'); File file = null; - - foreach (var part in parts) + var partsLength = parts.Count(); + for (int i = 0; i < partsLength; i++) { + var part = parts[i]; var query = api.Files.List(); query.Q = string.Format("name = '{0}' and '{1}' in parents and trashed = false", part, //query.Q = string.Format("title = '{0}'", part, @@ -102,16 +103,19 @@ public static async Task GetFileByPath(this DriveService api, string path) file = result.Files.FirstOrDefault(); if (file == null) return null; - if (file.MimeType == "application/vnd.google-apps.shortcut") - { - if (file.ShortcutDetails == null) - { - var fileQuery = api.Files.Get(file.Id); - fileQuery.Fields = "*"; - file = await fileQuery.ExecuteAsync(); - } - file = await api.Files.Get(file.ShortcutDetails.TargetId).ExecuteAsync(); - } + if (resolveFinalShortcut || i != partsLength-1) + { + if (file.MimeType == "application/vnd.google-apps.shortcut") + { + if (file.ShortcutDetails == null) + { + var fileQuery = api.Files.Get(file.Id); + fileQuery.Fields = "*"; + file = await fileQuery.ExecuteAsync(); + } + file = await api.Files.Get(file.ShortcutDetails.TargetId).ExecuteAsync(); + } + } } return file; diff --git a/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveStorageProvider.cs b/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveStorageProvider.cs index a78684c..eab47a7 100644 --- a/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveStorageProvider.cs +++ b/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveStorageProvider.cs @@ -26,7 +26,7 @@ public async Task Load(string path) { var api = await GetApi(); - var file = await api.GetFileByPath(path); + var file = await api.GetFileByPath(path, true); if (file == null) return null; @@ -48,7 +48,7 @@ public async Task Save(Stream stream, string path) IUploadProgress progress; - var file = await api.GetFileByPath(path); + var file = await api.GetFileByPath(path, true); if (file != null) { progress = await api.Files.Update(null, file.Id, stream, "application/octet-stream").UploadAsync(); @@ -66,7 +66,7 @@ public async Task Save(Stream stream, string path) if (!string.IsNullOrEmpty(folderName)) { - var folder = await api.GetFileByPath(folderName); + var folder = await api.GetFileByPath(folderName, true); if (folder == null) throw new InvalidOperationException(string.Format("Folder does not exist: {0}", folderName)); @@ -84,12 +84,12 @@ public async Task Copy(string sourcePath, string destPath) { var api = await GetApi(); - var sourceFile = await api.GetFileByPath(sourcePath); + var sourceFile = await api.GetFileByPath(sourcePath, true); if (sourceFile == null) throw new FileNotFoundException("Google Drive: File not found.", sourcePath); var destFolder = CloudPath.GetDirectoryName(destPath); - var parentFolder = await api.GetFileByPath(destFolder); + var parentFolder = await api.GetFileByPath(destFolder, true); if (parentFolder == null) throw new FileNotFoundException("Google Drive: File not found.", destFolder); @@ -107,7 +107,7 @@ public async Task Delete(string path) { var api = await GetApi(); - var file = await api.GetFileByPath(path); + var file = await api.GetFileByPath(path, false); if (file == null) throw new FileNotFoundException("Goolge Drive: File not found.", path); @@ -178,7 +178,7 @@ public async Task> GetChildrenByParentItem(Stor public async Task> GetChildrenByParentPath(string path) { var api = await GetApi(); - var item = await api.GetFileByPath(path); + var item = await api.GetFileByPath(path, true); if (item == null) throw new FileNotFoundException("Goolge Drive: File not found.", path); From 92366d02d6b47648edd817c4cc323a5eeedd3ea5 Mon Sep 17 00:00:00 2001 From: Jackabomb <48334975+Jackabomb@users.noreply.github.com> Date: Wed, 19 Apr 2023 09:16:13 -0700 Subject: [PATCH 3/4] Change fields away from *, to only query the fields we need. --- .../GoogleDrive/GoogleDriveHelper.cs | 2 +- .../GoogleDrive/GoogleDriveStorageProvider.cs | 84 +++++++++++-------- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveHelper.cs b/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveHelper.cs index 70c7eab..ebceefa 100644 --- a/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveHelper.cs +++ b/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveHelper.cs @@ -110,7 +110,7 @@ public static async Task GetFileByPath(this DriveService api, string path, if (file.ShortcutDetails == null) { var fileQuery = api.Files.Get(file.Id); - fileQuery.Fields = "*"; + fileQuery.Fields = "shortcutDetails"; file = await fileQuery.ExecuteAsync(); } file = await api.Files.Get(file.ShortcutDetails.TargetId).ExecuteAsync(); diff --git a/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveStorageProvider.cs b/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveStorageProvider.cs index eab47a7..f7926dc 100644 --- a/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveStorageProvider.cs +++ b/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveStorageProvider.cs @@ -129,50 +129,30 @@ public async Task> GetChildrenByParentItem(Stor var api = await GetApi(); var query = api.Files.List(); query.Q = string.Format("'{0}' in parents and trashed = false", parent.Id); - query.Fields = "*"; + query.Fields = "files(id, name, mimeType, modifiedTime, shortcutDetails, parents)"; var items = await query.ExecuteAsync(); - var newItems = items.Files.Select(_ => new StorageProviderItem() - { - Id = - _.MimeType == "application/vnd.google-apps.shortcut" - ? - _.ShortcutDetails != null - ? _.ShortcutDetails.TargetId - : "1" - : _.Id, - Name = _.Name, - Type = - _.MimeType == "application/vnd.google-apps.shortcut" - ? _.ShortcutDetails.TargetMimeType == "application/vnd.google-apps.folder" - ? StorageProviderItemType.Folder - : StorageProviderItemType.File - : _.MimeType == "application/vnd.google-apps.folder" - ? StorageProviderItemType.Folder - : StorageProviderItemType.File, - LastModifiedDateTime = _.ModifiedTime, - ParentReferenceId = parent.Id, - }); + var newItems = items.Files.Select(async _ => + { + var result = await MakeStorageProviderItem(_, api); + result.ParentReferenceId = parent.Id; + return result; + }); + while (items.NextPageToken != null) { query.PageToken = items.NextPageToken; items = await query.ExecuteAsync(); - newItems = newItems.Concat(items.Files.Select(_ => new StorageProviderItem() - { - Id = _.Id, - Name = _.Name, - Type = - _.MimeType == "application/vnd.google-apps.folder" - ? StorageProviderItemType.Folder - : StorageProviderItemType.File, - LastModifiedDateTime = _.ModifiedTime, - ParentReferenceId = parent.Id, - })); + newItems = newItems.Concat(items.Files.Select(async _ => + { + var result = await MakeStorageProviderItem(_, api); + result.ParentReferenceId = parent.Id; return result; + })); } - return newItems.ToArray(); + return await Task.WhenAll(newItems.ToArray()); } public async Task> GetChildrenByParentPath(string path) @@ -200,5 +180,41 @@ protected async Task GetApi() return _api; } + + protected async Task MakeStorageProviderItem(File _, DriveService api) + { + var isShortcut = false; + if (_.MimeType == "application/vnd.google-apps.shortcut") + { + isShortcut = true; + if (_.ShortcutDetails==null) + { + var fileQuery = api.Files.Get(_.Id); + fileQuery.Fields = "shortcutDetails"; + + _ = await fileQuery.ExecuteAsync(); + } + } + var result = new StorageProviderItem() + { + Id = + isShortcut + ? _.ShortcutDetails.TargetId + : _.Id, + Name = _.Name, + Type = + isShortcut + ? _.ShortcutDetails.TargetMimeType == "application/vnd.google-apps.folder" + ? StorageProviderItemType.Folder + : StorageProviderItemType.File + : _.MimeType == "application/vnd.google-apps.folder" + ? StorageProviderItemType.Folder + : StorageProviderItemType.File, + LastModifiedDateTime = _.ModifiedTime, + ParentReferenceId = _.Parents.FirstOrDefault(), + }; + + return result; + } } } From d7f7f7d7826c2cd16ee56241bc1639305f2f7b1d Mon Sep 17 00:00:00 2001 From: Jackabomb <48334975+Jackabomb@users.noreply.github.com> Date: Sat, 22 Apr 2023 03:11:27 -0700 Subject: [PATCH 4/4] Bugfix: Not all items in drive were being shown (only 100). Added nextPageToken to the fields being requested. I forgot that if I don't put it in the request fields, it will *always* be set to null. Downside of using the Fields parameter is, you have to put everything in there manually, or you won't have it. --- .../GoogleDrive/GoogleDriveStorageProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveStorageProvider.cs b/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveStorageProvider.cs index f7926dc..9b1a08c 100644 --- a/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveStorageProvider.cs +++ b/KeeAnywhere/StorageProviders/GoogleDrive/GoogleDriveStorageProvider.cs @@ -129,7 +129,7 @@ public async Task> GetChildrenByParentItem(Stor var api = await GetApi(); var query = api.Files.List(); query.Q = string.Format("'{0}' in parents and trashed = false", parent.Id); - query.Fields = "files(id, name, mimeType, modifiedTime, shortcutDetails, parents)"; + query.Fields = "nextPageToken, files(id, name, mimeType, shortcutDetails, modifiedTime, parents)"; //The shortcutDetails field isn't returned in queries by default. Unless we request it, it's always null. The downside is, now we have to spell out every field we *do* want. Forgetting something we need will mean it's always set to null in the returned query, File object, etc. and things will break. This already happened once when I forgot I needed to explicitly request nextPageToken. var items = await query.ExecuteAsync(); var newItems = items.Files.Select(async _ => @@ -160,7 +160,7 @@ public async Task> GetChildrenByParentPath(stri var api = await GetApi(); var item = await api.GetFileByPath(path, true); if (item == null) - throw new FileNotFoundException("Goolge Drive: File not found.", path); + throw new FileNotFoundException("Google Drive: File not found.", path); return await GetChildrenByParentItem(new StorageProviderItem {Id = item.Id}); }