From 4d569c51e4600829816bb49eb0fd0b385be85c94 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Wed, 3 Jun 2026 17:27:34 +0200 Subject: [PATCH] Added read-only testing to backends This PR adds a flag to the test method so we can choose to test for read-only access (needed for restore) or read-write (needed for backups). Additionally, this PR fixes a logic bug in that the BackendSourceProvider would not actually test the connection, but just check the placeholder element returned (which is always returned). With this PR the backend's ListAsync command is tested fully. Also renamed the methods on ISourceProvider to have the `...Async` prefix. This replaces #6939 --- .../CommandLine/BackendTester/Program.cs | 2 +- .../CommandLine/SourceTool/ListEnumerator.cs | 2 +- .../Backend/AliyunOSS/AliyunOSSBackend.cs | 4 +- .../Backend/AzureBlob/AzureBlobBackend.cs | 4 +- Duplicati/Library/Backend/Backblaze/B2.cs | 4 +- Duplicati/Library/Backend/Box/BoxBackend.cs | 4 +- .../Backend/DrimeCloud/DrimeBackend.cs | 4 +- Duplicati/Library/Backend/Dropbox/Dropbox.cs | 4 +- .../Backend/Duplicati/DuplicatiBackend.cs | 4 +- Duplicati/Library/Backend/FTP/FTPBackend.cs | 5 ++- Duplicati/Library/Backend/File/FileBackend.cs | 4 +- .../Library/Backend/Filejump/Filejump.cs | 4 +- .../Library/Backend/Filen/FilenBackend.cs | 4 +- .../GoogleServices/GoogleCloudStorage.cs | 4 +- .../Backend/GoogleServices/GoogleDrive.cs | 4 +- .../Backend/Idrivee2/Idrivee2Backend.cs | 4 +- .../Library/Backend/Jottacloud/Jottacloud.cs | 4 +- Duplicati/Library/Backend/Mega/MegaBackend.cs | 4 +- .../MovistarCloud/MovistarCloudBackend.cs | 4 +- .../Backend/OneDrive/MicrosoftGraphBackend.cs | 4 +- .../Backend/OpenStack/OpenStackStorage.cs | 4 +- Duplicati/Library/Backend/Rclone/Rclone.cs | 4 +- Duplicati/Library/Backend/S3/S3Backend.cs | 4 +- Duplicati/Library/Backend/SMB/SMBBackend.cs | 6 +-- .../Library/Backend/SSHv2/SSHv2Backend.cs | 4 +- .../Backend/SharePoint/SharePointBackend.cs | 4 +- .../Library/Backend/Storj/StorjBackend.cs | 29 +++++++----- .../Library/Backend/TahoeLAFS/TahoeBackend.cs | 4 +- .../Library/Backend/TencentCOS/COSBackend.cs | 5 ++- Duplicati/Library/Backend/WEBDAV/WEBDAV.cs | 4 +- .../Library/Backend/pCloud/pCloudBackend.cs | 5 ++- .../DynamicLoader/SourceProviderLoader.cs | 2 +- Duplicati/Library/Interface/IBackend.cs | 41 ++++++++--------- .../Library/Interface/ISourceProvider.cs | 8 ++-- .../Backup/FileEnumerationProcess.cs | 4 +- .../Main/Operation/TestFilterHandler.cs | 4 +- .../Builtin/BackendSourceProvider.cs | 10 ++--- .../SourceProvider/Builtin/Combiner.cs | 18 ++++---- .../SourceProvider/Builtin/LocalFileSource.cs | 8 ++-- .../Library/Utility/BackendExtensions.cs | 44 ++++++++++++++++++- Duplicati/UnitTest/ArchiveAttributeTests.cs | 4 +- .../UnitTest/DeterministicErrorBackend.cs | 4 +- .../UnitTest/DiskImage/DiskImageTests.cs | 4 +- Duplicati/UnitTest/Issue5845.cs | 4 +- Duplicati/UnitTest/Issue5862.cs | 4 +- Duplicati/UnitTest/RandomErrorBackend.cs | 4 +- Duplicati/UnitTest/SizeOmittingBackend.cs | 4 +- Duplicati/UnitTest/SoftDeleteTests.cs | 4 +- .../Dto/V2/DestinationTestRequestDto.cs | 5 +++ .../Endpoints/V1/RemoteOperation.cs | 14 +++--- .../Endpoints/V2/DestinationVerify.cs | 17 +++---- proprietary/DiskImage/SourceProvider.cs | 8 ++-- proprietary/DiskImage/WebModule.cs | 4 +- .../RestoreProvider/RestoreProvider.cs | 12 ++--- .../SourceProvider/SourceProvider.cs | 8 ++-- .../GoogleWorkspace/WebModule/WebModule.cs | 4 +- .../RestoreProvider/RestoreProvider.Email.cs | 2 +- .../RestoreProvider/RestoreProvider.cs | 12 ++--- .../SourceProvider/SourceProvider.cs | 8 ++-- proprietary/Office365/WebModule/WebModule.cs | 4 +- 60 files changed, 238 insertions(+), 181 deletions(-) diff --git a/Duplicati/CommandLine/BackendTester/Program.cs b/Duplicati/CommandLine/BackendTester/Program.cs index c0a19766f..0bb0f5c07 100644 --- a/Duplicati/CommandLine/BackendTester/Program.cs +++ b/Duplicati/CommandLine/BackendTester/Program.cs @@ -176,7 +176,7 @@ namespace Duplicati.CommandLine.BackendTester IEnumerable curlist = null; try { - Retry(() => backend.TestAsync(CancellationToken.None), retries).Await(); + Retry(() => backend.TestAsync(true, CancellationToken.None), retries).Await(); curlist = Retry(() => backend.ListAsync(CancellationToken.None).ToBlockingEnumerable().ToList(), retries); } catch (FolderMissingException) diff --git a/Duplicati/CommandLine/SourceTool/ListEnumerator.cs b/Duplicati/CommandLine/SourceTool/ListEnumerator.cs index b267cf5eb..e186f6bbb 100644 --- a/Duplicati/CommandLine/SourceTool/ListEnumerator.cs +++ b/Duplicati/CommandLine/SourceTool/ListEnumerator.cs @@ -38,7 +38,7 @@ public static partial class Common public static async Task Visit(ISourceProvider source, int maxdepth, Func> visitor, CancellationToken token) { var visit = new Stack<(ISourceProviderEntry Entry, int Level)>(); - await foreach (var item in source.Enumerate(token)) + await foreach (var item in source.EnumerateAsync(token)) visit.Push((item, 0)); while (visit.Count() != 0) diff --git a/Duplicati/Library/Backend/AliyunOSS/AliyunOSSBackend.cs b/Duplicati/Library/Backend/AliyunOSS/AliyunOSSBackend.cs index 3d8b59b77..ef5492de2 100644 --- a/Duplicati/Library/Backend/AliyunOSS/AliyunOSSBackend.cs +++ b/Duplicati/Library/Backend/AliyunOSS/AliyunOSSBackend.cs @@ -175,8 +175,8 @@ namespace Duplicati.Library.Backend.AliyunOSS } } - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); public Task CreateFolderAsync(CancellationToken cancelToken) // No need to create folders diff --git a/Duplicati/Library/Backend/AzureBlob/AzureBlobBackend.cs b/Duplicati/Library/Backend/AzureBlob/AzureBlobBackend.cs index 51e175886..4c8dde038 100644 --- a/Duplicati/Library/Backend/AzureBlob/AzureBlobBackend.cs +++ b/Duplicati/Library/Backend/AzureBlob/AzureBlobBackend.cs @@ -256,8 +256,8 @@ namespace Duplicati.Library.Backend.AzureBlob public Task GetDNSNamesAsync(CancellationToken cancelToken) => Task.FromResult(_azureBlob.DnsNames); - public Task TestAsync(CancellationToken cancellationToken) - => this.TestReadWritePermissionsAsync(cancellationToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancellationToken) + => this.TestBackendAsync(alsoWrite, cancellationToken); public Task CreateFolderAsync(CancellationToken cancellationToken) { diff --git a/Duplicati/Library/Backend/Backblaze/B2.cs b/Duplicati/Library/Backend/Backblaze/B2.cs index 8122c14bf..3cf715c35 100644 --- a/Duplicati/Library/Backend/Backblaze/B2.cs +++ b/Duplicati/Library/Backend/Backblaze/B2.cs @@ -657,8 +657,8 @@ public class B2 : IStreamingBackend, ILockingBackend, IRenameEnabledBackend /// Performs test with backend (internally it uses the List() command, which by definition means the backend is working) /// /// Cancellation Token - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); /// /// Create remote folder diff --git a/Duplicati/Library/Backend/Box/BoxBackend.cs b/Duplicati/Library/Backend/Box/BoxBackend.cs index 4b0507efe..188e4c057 100644 --- a/Duplicati/Library/Backend/Box/BoxBackend.cs +++ b/Duplicati/Library/Backend/Box/BoxBackend.cs @@ -452,8 +452,8 @@ namespace Duplicati.Library.Backend.Box } /// - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); /// public Task CreateFolderAsync(CancellationToken cancellationToken) diff --git a/Duplicati/Library/Backend/DrimeCloud/DrimeBackend.cs b/Duplicati/Library/Backend/DrimeCloud/DrimeBackend.cs index db8ea493a..7ea33427f 100644 --- a/Duplicati/Library/Backend/DrimeCloud/DrimeBackend.cs +++ b/Duplicati/Library/Backend/DrimeCloud/DrimeBackend.cs @@ -505,8 +505,8 @@ public class DrimeBackend : IBackend, IStreamingBackend //, IRenameEnabledBacken } /// - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); /// public Task GetDNSNamesAsync(CancellationToken cancelToken) diff --git a/Duplicati/Library/Backend/Dropbox/Dropbox.cs b/Duplicati/Library/Backend/Dropbox/Dropbox.cs index 5332b0235..df11e5fee 100644 --- a/Duplicati/Library/Backend/Dropbox/Dropbox.cs +++ b/Duplicati/Library/Backend/Dropbox/Dropbox.cs @@ -214,8 +214,8 @@ namespace Duplicati.Library.Backend public Task GetDNSNamesAsync(CancellationToken cancelToken) => Task.FromResult(WebApi.Dropbox.Hosts()); - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); public async Task CreateFolderAsync(CancellationToken cancelToken) { diff --git a/Duplicati/Library/Backend/Duplicati/DuplicatiBackend.cs b/Duplicati/Library/Backend/Duplicati/DuplicatiBackend.cs index 36b1229e2..fcbf9b083 100644 --- a/Duplicati/Library/Backend/Duplicati/DuplicatiBackend.cs +++ b/Duplicati/Library/Backend/Duplicati/DuplicatiBackend.cs @@ -478,8 +478,8 @@ public class DuplicatiBackend : IBackend, IStreamingBackend, IQuotaEnabledBacken ]; /// - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); /// /// Response from getting credentials diff --git a/Duplicati/Library/Backend/FTP/FTPBackend.cs b/Duplicati/Library/Backend/FTP/FTPBackend.cs index c7fe6cb08..ed5336b6b 100644 --- a/Duplicati/Library/Backend/FTP/FTPBackend.cs +++ b/Duplicati/Library/Backend/FTP/FTPBackend.cs @@ -515,7 +515,7 @@ namespace Duplicati.Library.Backend public bool SupportsStreaming => true; /// - public async Task TestAsync(CancellationToken cancellationToken) + public async Task TestAsync(bool alsoWrite, CancellationToken cancellationToken) { // Start with a simple list and pureFTP detection try @@ -556,7 +556,8 @@ namespace Duplicati.Library.Backend } // Test the read/write permissions in folder - await this.TestReadWritePermissionsAsync(cancellationToken).ConfigureAwait(false); + if (alsoWrite) + await this.TestReadWritePermissionsAsync(cancellationToken).ConfigureAwait(false); } /// diff --git a/Duplicati/Library/Backend/File/FileBackend.cs b/Duplicati/Library/Backend/File/FileBackend.cs index 65765fe34..23fadb976 100644 --- a/Duplicati/Library/Backend/File/FileBackend.cs +++ b/Duplicati/Library/Backend/File/FileBackend.cs @@ -410,8 +410,8 @@ namespace Duplicati.Library.Backend public string Description => Strings.FileBackend.Description; /// - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); /// public async Task CreateFolderAsync(CancellationToken cancelToken) diff --git a/Duplicati/Library/Backend/Filejump/Filejump.cs b/Duplicati/Library/Backend/Filejump/Filejump.cs index 12b83b34d..bcbc2924c 100644 --- a/Duplicati/Library/Backend/Filejump/Filejump.cs +++ b/Duplicati/Library/Backend/Filejump/Filejump.cs @@ -517,8 +517,8 @@ namespace Duplicati.Library.Backend } /// - public Task TestAsync(CancellationToken token) - => this.TestReadWritePermissionsAsync(token); + public Task TestAsync(bool alsoWrite, CancellationToken token) + => this.TestBackendAsync(alsoWrite, token); /// public Task GetDNSNamesAsync(CancellationToken token) diff --git a/Duplicati/Library/Backend/Filen/FilenBackend.cs b/Duplicati/Library/Backend/Filen/FilenBackend.cs index 3f953a754..5fbfc9c5e 100644 --- a/Duplicati/Library/Backend/Filen/FilenBackend.cs +++ b/Duplicati/Library/Backend/Filen/FilenBackend.cs @@ -243,8 +243,8 @@ public class FilenBackend : IStreamingBackend, IRenameEnabledBackend ); /// - public Task TestAsync(CancellationToken cancellationToken) - => this.TestReadWritePermissionsAsync(cancellationToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancellationToken) + => this.TestBackendAsync(alsoWrite, cancellationToken); /// public async Task CreateFolderAsync(CancellationToken cancellationToken) diff --git a/Duplicati/Library/Backend/GoogleServices/GoogleCloudStorage.cs b/Duplicati/Library/Backend/GoogleServices/GoogleCloudStorage.cs index 6c50bb276..17eff9109 100644 --- a/Duplicati/Library/Backend/GoogleServices/GoogleCloudStorage.cs +++ b/Duplicati/Library/Backend/GoogleServices/GoogleCloudStorage.cs @@ -298,8 +298,8 @@ namespace Duplicati.Library.Backend.GoogleCloudStorage } } - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); public async Task CreateFolderAsync(CancellationToken cancelToken) { diff --git a/Duplicati/Library/Backend/GoogleServices/GoogleDrive.cs b/Duplicati/Library/Backend/GoogleServices/GoogleDrive.cs index 1d1ba4323..d3bebd62b 100644 --- a/Duplicati/Library/Backend/GoogleServices/GoogleDrive.cs +++ b/Duplicati/Library/Backend/GoogleServices/GoogleDrive.cs @@ -403,8 +403,8 @@ namespace Duplicati.Library.Backend.GoogleDrive } } - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); public async Task CreateFolderAsync(CancellationToken cancelToken) { diff --git a/Duplicati/Library/Backend/Idrivee2/Idrivee2Backend.cs b/Duplicati/Library/Backend/Idrivee2/Idrivee2Backend.cs index 10564d1fb..427553524 100644 --- a/Duplicati/Library/Backend/Idrivee2/Idrivee2Backend.cs +++ b/Duplicati/Library/Backend/Idrivee2/Idrivee2Backend.cs @@ -215,8 +215,8 @@ namespace Duplicati.Library.Backend public string Description => Strings.Idrivee2Backend.Description; /// - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); /// public bool SupportsStreaming => true; diff --git a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs index c406dc830..e607d3d19 100644 --- a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs +++ b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs @@ -362,8 +362,8 @@ public class Jottacloud : IStreamingBackend, IRenameEnabledBackend public bool SupportsStreaming => true; /// - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); /// public async Task CreateFolderAsync(CancellationToken cancelToken) diff --git a/Duplicati/Library/Backend/Mega/MegaBackend.cs b/Duplicati/Library/Backend/Mega/MegaBackend.cs index beb93d4e4..4043f74d2 100644 --- a/Duplicati/Library/Backend/Mega/MegaBackend.cs +++ b/Duplicati/Library/Backend/Mega/MegaBackend.cs @@ -251,8 +251,8 @@ namespace Duplicati.Library.Backend.Mega } /// - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); /// public Task CreateFolderAsync(CancellationToken cancelToken) diff --git a/Duplicati/Library/Backend/MovistarCloud/MovistarCloudBackend.cs b/Duplicati/Library/Backend/MovistarCloud/MovistarCloudBackend.cs index c776ed6cf..b9fff3281 100644 --- a/Duplicati/Library/Backend/MovistarCloud/MovistarCloudBackend.cs +++ b/Duplicati/Library/Backend/MovistarCloud/MovistarCloudBackend.cs @@ -385,7 +385,7 @@ public sealed class MovistarCloudBackend : IBackend } /// - public async Task TestAsync(CancellationToken cancellationToken) + public async Task TestAsync(bool alsoWrite, CancellationToken cancellationToken) { var client = await GetClientAsync(cancellationToken).ConfigureAwait(false); @@ -421,6 +421,8 @@ public sealed class MovistarCloudBackend : IBackend { await RunDiagnosticsAsync(cancellationToken).ConfigureAwait(false); } + + await this.TestBackendAsync(alsoWrite, cancellationToken).ConfigureAwait(false); } /// diff --git a/Duplicati/Library/Backend/OneDrive/MicrosoftGraphBackend.cs b/Duplicati/Library/Backend/OneDrive/MicrosoftGraphBackend.cs index 22fda1614..c0e660f38 100644 --- a/Duplicati/Library/Backend/OneDrive/MicrosoftGraphBackend.cs +++ b/Duplicati/Library/Backend/OneDrive/MicrosoftGraphBackend.cs @@ -650,7 +650,7 @@ namespace Duplicati.Library.Backend } } - public async Task TestAsync(CancellationToken cancelToken) + public async Task TestAsync(bool alsoWrite, CancellationToken cancelToken) { try { @@ -664,7 +664,7 @@ namespace Duplicati.Library.Backend throw new FolderMissingException(ex); } - await this.TestReadWritePermissionsAsync(cancelToken).ConfigureAwait(false); + await this.TestBackendAsync(alsoWrite, cancelToken).ConfigureAwait(false); } public void Dispose() diff --git a/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs b/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs index b8633dad4..5c49b3ead 100644 --- a/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs +++ b/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs @@ -446,8 +446,8 @@ public class OpenStackStorage : IStreamingBackend, IRenameEnabledBackend } /// - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); /// public async Task CreateFolderAsync(CancellationToken cancelToken) diff --git a/Duplicati/Library/Backend/Rclone/Rclone.cs b/Duplicati/Library/Backend/Rclone/Rclone.cs index 69385ad66..7581d384b 100644 --- a/Duplicati/Library/Backend/Rclone/Rclone.cs +++ b/Duplicati/Library/Backend/Rclone/Rclone.cs @@ -297,8 +297,8 @@ namespace Duplicati.Library.Backend public Task GetDNSNamesAsync(CancellationToken cancelToken) => Task.FromResult(new[] { remote_repo }); - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); public Task CreateFolderAsync(CancellationToken cancelToken) { diff --git a/Duplicati/Library/Backend/S3/S3Backend.cs b/Duplicati/Library/Backend/S3/S3Backend.cs index 7729b53b3..b815c0c95 100644 --- a/Duplicati/Library/Backend/S3/S3Backend.cs +++ b/Duplicati/Library/Backend/S3/S3Backend.cs @@ -438,8 +438,8 @@ namespace Duplicati.Library.Backend public string Description => Strings.S3Backend.Description_v2; /// - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); /// public Task CreateFolderAsync(CancellationToken cancelToken) diff --git a/Duplicati/Library/Backend/SMB/SMBBackend.cs b/Duplicati/Library/Backend/SMB/SMBBackend.cs index 4dcb6a4fb..40f62bfcb 100644 --- a/Duplicati/Library/Backend/SMB/SMBBackend.cs +++ b/Duplicati/Library/Backend/SMB/SMBBackend.cs @@ -284,8 +284,8 @@ public class SMBBackend : IStreamingBackend, IFolderEnabledBackend, IRenameEnabl /// /// The cancellation token (not used) /// Thrown when configured path does not exist - public Task TestAsync(CancellationToken cancellationToken) - => this.TestReadWritePermissionsAsync(cancellationToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancellationToken) + => this.TestBackendAsync(alsoWrite, cancellationToken); /// /// Creates the configured remote folder path if it doesn't exist @@ -350,7 +350,7 @@ public class SMBBackend : IStreamingBackend, IFolderEnabledBackend, IRenameEnabl var con = await GetConnectionAsync(cancellationToken).ConfigureAwait(false); await con.RenameAsync(oldname, newname, cancellationToken).ConfigureAwait(false); } - + /// public async Task GetEntryAsync(string path, CancellationToken cancellationToken) { diff --git a/Duplicati/Library/Backend/SSHv2/SSHv2Backend.cs b/Duplicati/Library/Backend/SSHv2/SSHv2Backend.cs index a81c42bc2..c936de444 100644 --- a/Duplicati/Library/Backend/SSHv2/SSHv2Backend.cs +++ b/Duplicati/Library/Backend/SSHv2/SSHv2Backend.cs @@ -123,8 +123,8 @@ namespace Duplicati.Library.Backend #region IBackend Members - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); public async Task CreateFolderAsync(CancellationToken cancelToken) { diff --git a/Duplicati/Library/Backend/SharePoint/SharePointBackend.cs b/Duplicati/Library/Backend/SharePoint/SharePointBackend.cs index 067304b3a..829b30402 100644 --- a/Duplicati/Library/Backend/SharePoint/SharePointBackend.cs +++ b/Duplicati/Library/Backend/SharePoint/SharePointBackend.cs @@ -427,11 +427,11 @@ namespace Duplicati.Library.Backend #region [Public backend methods] - public async Task TestAsync(CancellationToken cancelToken) + public async Task TestAsync(bool alsoWrite, CancellationToken cancelToken) { var ctx = await GetSpClientContextAsync(true, cancelToken).ConfigureAwait(false); await TestContextForWebAsync(ctx, true, m_timeouts.ShortTimeout, cancelToken).ConfigureAwait(false); - await this.TestReadWritePermissionsAsync(cancelToken).ConfigureAwait(false); + await this.TestBackendAsync(alsoWrite, cancelToken).ConfigureAwait(false); } /// diff --git a/Duplicati/Library/Backend/Storj/StorjBackend.cs b/Duplicati/Library/Backend/Storj/StorjBackend.cs index 1864d32d2..0236dcece 100644 --- a/Duplicati/Library/Backend/Storj/StorjBackend.cs +++ b/Duplicati/Library/Backend/Storj/StorjBackend.cs @@ -262,8 +262,8 @@ namespace Duplicati.Library.Backend.Storj throw new Exception(upload.ErrorMessage); } - public Task TestAsync(CancellationToken cancelToken) - => Utility.Utility.WithTimeout(_timeouts.ShortTimeout, cancelToken, ct => TestImplAsync(ct)); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => Utility.Utility.WithTimeout(_timeouts.ShortTimeout, cancelToken, ct => TestImplAsync(alsoWrite, ct)); /// /// Test the connection by: @@ -271,22 +271,31 @@ namespace Duplicati.Library.Backend.Storj /// - uploading 256 random bytes to a test-file /// - downloading the file back and expecting 256 bytes /// + /// If true, the test will also write a file to the backend + /// The cancellation token /// true, if the test was successfull or and exception - private async Task TestImplAsync(CancellationToken cancelToken) + private async Task TestImplAsync(bool alsoWrite, CancellationToken cancelToken) { var testFileName = GetBasePath() + "duplicati_test.dat"; var bucket = await GetBucketAsync(cancelToken).ConfigureAwait(false); - var upload = await _objectService.UploadObjectAsync(bucket, testFileName, new UploadOptions(), GetRandomBytes(256), false).ConfigureAwait(false); - await upload.StartUploadAsync().ConfigureAwait(false); + if (alsoWrite) + { + var upload = await _objectService.UploadObjectAsync(bucket, testFileName, new UploadOptions(), GetRandomBytes(256), false).ConfigureAwait(false); + await upload.StartUploadAsync().ConfigureAwait(false); - var download = await _objectService.DownloadObjectAsync(bucket, testFileName, new DownloadOptions(), false).ConfigureAwait(false); - await download.StartDownloadAsync().ConfigureAwait(false); + var download = await _objectService.DownloadObjectAsync(bucket, testFileName, new DownloadOptions(), false).ConfigureAwait(false); + await download.StartDownloadAsync().ConfigureAwait(false); - await _objectService.DeleteObjectAsync(bucket, testFileName).ConfigureAwait(false); + await _objectService.DeleteObjectAsync(bucket, testFileName).ConfigureAwait(false); - if (download.Failed || download.BytesReceived != 256) - throw new Exception(download.ErrorMessage); + if (download.Failed || download.BytesReceived != 256) + throw new Exception(download.ErrorMessage); + } + else + { + await _objectService.ListObjectsAsync(bucket, new ListObjectsOptions { Prefix = GetBasePath() }).ConfigureAwait(false); + } return true; } diff --git a/Duplicati/Library/Backend/TahoeLAFS/TahoeBackend.cs b/Duplicati/Library/Backend/TahoeLAFS/TahoeBackend.cs index b9c357230..651aa9657 100644 --- a/Duplicati/Library/Backend/TahoeLAFS/TahoeBackend.cs +++ b/Duplicati/Library/Backend/TahoeLAFS/TahoeBackend.cs @@ -77,8 +77,8 @@ public class TahoeBackend : IStreamingBackend, IRenameEnabledBackend } /// - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); /// public async Task CreateFolderAsync(CancellationToken cancelToken) diff --git a/Duplicati/Library/Backend/TencentCOS/COSBackend.cs b/Duplicati/Library/Backend/TencentCOS/COSBackend.cs index 6ed69d341..a7c4d6188 100644 --- a/Duplicati/Library/Backend/TencentCOS/COSBackend.cs +++ b/Duplicati/Library/Backend/TencentCOS/COSBackend.cs @@ -243,7 +243,7 @@ namespace Duplicati.Library.Backend.TencentCOS } } - public async Task TestAsync(CancellationToken cancelToken) + public async Task TestAsync(bool alsoWrite, CancellationToken cancelToken) { var json = JsonConvert.SerializeObject(_cosOptions); try @@ -271,7 +271,8 @@ namespace Duplicati.Library.Backend.TencentCOS throw; } - await this.TestReadWritePermissionsAsync(cancelToken).ConfigureAwait(false); + if (alsoWrite) + await this.TestReadWritePermissionsAsync(cancelToken).ConfigureAwait(false); } public Task CreateFolderAsync(CancellationToken cancelToken) diff --git a/Duplicati/Library/Backend/WEBDAV/WEBDAV.cs b/Duplicati/Library/Backend/WEBDAV/WEBDAV.cs index bb1471960..9ed66b000 100644 --- a/Duplicati/Library/Backend/WEBDAV/WEBDAV.cs +++ b/Duplicati/Library/Backend/WEBDAV/WEBDAV.cs @@ -588,8 +588,8 @@ namespace Duplicati.Library.Backend public Task GetDNSNamesAsync(CancellationToken cancelToken) => Task.FromResult(new[] { m_dnsName }); /// - public Task TestAsync(CancellationToken cancelToken) - => this.TestReadWritePermissionsAsync(cancelToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) + => this.TestBackendAsync(alsoWrite, cancelToken); /// public async Task CreateFolderAsync(CancellationToken cancelToken) diff --git a/Duplicati/Library/Backend/pCloud/pCloudBackend.cs b/Duplicati/Library/Backend/pCloud/pCloudBackend.cs index 7c659754a..c5097605d 100644 --- a/Duplicati/Library/Backend/pCloud/pCloudBackend.cs +++ b/Duplicati/Library/Backend/pCloud/pCloudBackend.cs @@ -426,14 +426,15 @@ public class pCloudBackend : IStreamingBackend, IRenameEnabledBackend /// /// Tests backend connectivity by verifying the configured path exists /// + /// If true, also test write permissions /// The cancellation token (not used) /// Thrown when configured path does not exist - public async Task TestAsync(CancellationToken cancellationToken) + public async Task TestAsync(bool alsoWrite, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(_Path) || _Path.Split(PATH_SEPARATORS, StringSplitOptions.RemoveEmptyEntries).Length == 0) return; - await this.TestReadWritePermissionsAsync(cancellationToken).ConfigureAwait(false); + await this.TestBackendAsync(alsoWrite, cancellationToken).ConfigureAwait(false); } /// diff --git a/Duplicati/Library/DynamicLoader/SourceProviderLoader.cs b/Duplicati/Library/DynamicLoader/SourceProviderLoader.cs index 36d32e620..26382353f 100644 --- a/Duplicati/Library/DynamicLoader/SourceProviderLoader.cs +++ b/Duplicati/Library/DynamicLoader/SourceProviderLoader.cs @@ -220,7 +220,7 @@ namespace Duplicati.Library.DynamicLoader { // In test mode, we do not initialize the provider if (!getForTesting) - await provider.Initialize(cancellationToken).ConfigureAwait(false); + await provider.InitializeAsync(cancellationToken).ConfigureAwait(false); return provider; } catch diff --git a/Duplicati/Library/Interface/IBackend.cs b/Duplicati/Library/Interface/IBackend.cs index 0cef06c57..919dcedc5 100644 --- a/Duplicati/Library/Interface/IBackend.cs +++ b/Duplicati/Library/Interface/IBackend.cs @@ -1,22 +1,22 @@ -// Copyright (C) 2026, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// Copyright (C) 2026, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; @@ -96,8 +96,9 @@ namespace Duplicati.Library.Interface /// If the encountered problem is a missing target "folder", /// this method should throw a . /// + /// If true, the test should also attempt to write a file to the backend. /// Token to cancel the operation. - Task TestAsync(CancellationToken cancellationToken); + Task TestAsync(bool alsoWrite, CancellationToken cancellationToken); /// /// The purpose of this method is to create the underlying "folder". diff --git a/Duplicati/Library/Interface/ISourceProvider.cs b/Duplicati/Library/Interface/ISourceProvider.cs index 90278983a..5e0f6f960 100644 --- a/Duplicati/Library/Interface/ISourceProvider.cs +++ b/Duplicati/Library/Interface/ISourceProvider.cs @@ -43,21 +43,21 @@ public interface ISourceProvider : IDisposable /// /// The cancellation token /// An awaitable task - Task Initialize(CancellationToken cancellationToken); + Task InitializeAsync(CancellationToken cancellationToken); /// /// Tests the provider connection /// /// The cancellation token /// An awaitable task - Task Test(CancellationToken cancellationToken); + Task TestAsync(CancellationToken cancellationToken); /// /// Gets the root entries /// /// The cancellation token /// An enumerable of file entries - IAsyncEnumerable Enumerate(CancellationToken cancellationToken); + IAsyncEnumerable EnumerateAsync(CancellationToken cancellationToken); /// /// Gets a specific entry @@ -66,5 +66,5 @@ public interface ISourceProvider : IDisposable /// True if the path is a folder /// The cancellation token /// The file entry - Task GetEntry(string path, bool isFolder, CancellationToken cancellationToken); + Task GetEntryAsync(string path, bool isFolder, CancellationToken cancellationToken); } diff --git a/Duplicati/Library/Main/Operation/Backup/FileEnumerationProcess.cs b/Duplicati/Library/Main/Operation/Backup/FileEnumerationProcess.cs index ab2436bc2..1a8df6fc2 100644 --- a/Duplicati/Library/Main/Operation/Backup/FileEnumerationProcess.cs +++ b/Duplicati/Library/Main/Operation/Backup/FileEnumerationProcess.cs @@ -113,7 +113,7 @@ namespace Duplicati.Library.Main.Operation.Backup { foreach (var s in list) { - var r = await sourceProvider.GetEntry(s, s.EndsWith(Path.DirectorySeparatorChar), token).ConfigureAwait(false); + var r = await sourceProvider.GetEntryAsync(s, s.EndsWith(Path.DirectorySeparatorChar), token).ConfigureAwait(false); if (r != null) { //TODO: Set r.IsRoot = true for source elements @@ -166,7 +166,7 @@ namespace Duplicati.Library.Main.Operation.Backup } else { - worklist = RecurseEntriesAsync(sourceProvider.Enumerate(token), + worklist = RecurseEntriesAsync(sourceProvider.EnumerateAsync(token), FilterEntry, token ); diff --git a/Duplicati/Library/Main/Operation/TestFilterHandler.cs b/Duplicati/Library/Main/Operation/TestFilterHandler.cs index 92712ebf3..f6f67f06d 100644 --- a/Duplicati/Library/Main/Operation/TestFilterHandler.cs +++ b/Duplicati/Library/Main/Operation/TestFilterHandler.cs @@ -86,7 +86,7 @@ namespace Duplicati.Library.Main.Operation // Go for the symlink target, as we know we follow symlinks if (!string.IsNullOrWhiteSpace(symlinkTarget)) { - var targetEntry = await provider.GetEntry(symlinkTarget, false, stopToken).ConfigureAwait(false); + var targetEntry = await provider.GetEntryAsync(symlinkTarget, false, stopToken).ConfigureAwait(false); fa = FileAttributes.Normal; try { fa = targetEntry!.Attributes; } @@ -94,7 +94,7 @@ namespace Duplicati.Library.Main.Operation // If we guessed wrong and the symlink target is a folder, we need to fetch it with the correct flag if (fa.HasFlag(FileAttributes.Directory)) - targetEntry = await provider.GetEntry(symlinkTarget, true, stopToken).ConfigureAwait(false); + targetEntry = await provider.GetEntryAsync(symlinkTarget, true, stopToken).ConfigureAwait(false); // No such target if (targetEntry == null) diff --git a/Duplicati/Library/SourceProvider/Builtin/BackendSourceProvider.cs b/Duplicati/Library/SourceProvider/Builtin/BackendSourceProvider.cs index 1d0e8b92c..d040da149 100644 --- a/Duplicati/Library/SourceProvider/Builtin/BackendSourceProvider.cs +++ b/Duplicati/Library/SourceProvider/Builtin/BackendSourceProvider.cs @@ -70,7 +70,7 @@ public class BackendSourceProvider(IFolderEnabledBackend backend, string mounted => new BackendSourceFileEntry(this, "", true, true, new DateTime(0), new DateTime(0), 0); /// - public async Task Initialize(CancellationToken cancellationToken) + public async Task InitializeAsync(CancellationToken cancellationToken) { // Only allow a single intiiialization call if (Interlocked.Exchange(ref isInitialized, 1) != 0) @@ -83,15 +83,15 @@ public class BackendSourceProvider(IFolderEnabledBackend backend, string mounted } /// - public Task Test(CancellationToken cancellationToken) - => backend.TestAsync(cancellationToken); + public Task TestAsync(CancellationToken cancellationToken) + => backend.TestAsync(false, cancellationToken); /// - public IAsyncEnumerable Enumerate(CancellationToken cancellationToken) + public IAsyncEnumerable EnumerateAsync(CancellationToken cancellationToken) => new[] { Interlocked.Exchange(ref preparedRoot, null) ?? CreateRoot() }.ToAsyncEnumerable(); /// - public async Task GetEntry(string path, bool isFolder, CancellationToken cancellationToken) + public async Task GetEntryAsync(string path, bool isFolder, CancellationToken cancellationToken) { var entry = await backend.GetEntryAsync(BackendSourceFileEntry.NormalizePathTo(path, '/'), cancellationToken).ConfigureAwait(false); return entry == null ? null : BackendSourceFileEntry.FromFileEntry(this, path, entry); diff --git a/Duplicati/Library/SourceProvider/Builtin/Combiner.cs b/Duplicati/Library/SourceProvider/Builtin/Combiner.cs index a006ac551..9e354ce00 100644 --- a/Duplicati/Library/SourceProvider/Builtin/Combiner.cs +++ b/Duplicati/Library/SourceProvider/Builtin/Combiner.cs @@ -38,19 +38,19 @@ public class Combiner(IEnumerable providers) : ISourceProvider private readonly List providers = providers.SelectMany(x => x is Combiner c ? c.providers.AsEnumerable() : [x]).ToList(); /// - public Task Initialize(CancellationToken cancellationToken) - => Task.WhenAll(providers.Select(x => x.Initialize(cancellationToken))); + public Task InitializeAsync(CancellationToken cancellationToken) + => Task.WhenAll(providers.Select(x => x.InitializeAsync(cancellationToken))); /// - public Task Test(CancellationToken cancellationToken) - => Task.WhenAll(providers.Select(x => x.Test(cancellationToken))); + public Task TestAsync(CancellationToken cancellationToken) + => Task.WhenAll(providers.Select(x => x.TestAsync(cancellationToken))); /// - public async IAsyncEnumerable Enumerate([EnumeratorCancellation] CancellationToken cancellationToken) + public async IAsyncEnumerable EnumerateAsync([EnumeratorCancellation] CancellationToken cancellationToken) { foreach (var provider in providers) { - await foreach (var entry in provider.Enumerate(cancellationToken).ConfigureAwait(false)) + await foreach (var entry in provider.EnumerateAsync(cancellationToken).ConfigureAwait(false)) yield return entry; } } @@ -67,18 +67,18 @@ public class Combiner(IEnumerable providers) : ISourceProvider /// True if the path is a folder /// The cancellation token /// The filesystem entry - public async Task GetEntry(string path, bool isFolder, CancellationToken cancellationToken) + public async Task GetEntryAsync(string path, bool isFolder, CancellationToken cancellationToken) { foreach (var provider in providers) if (!string.IsNullOrWhiteSpace(MountedPath) && provider.MountedPath.StartsWith(path)) { var subpath = provider.MountedPath.Substring(path.Length); - return await provider.GetEntry(subpath, isFolder, cancellationToken).ConfigureAwait(false); + return await provider.GetEntryAsync(subpath, isFolder, cancellationToken).ConfigureAwait(false); } foreach (var provider in providers.Where(x => string.IsNullOrWhiteSpace(x.MountedPath))) { - var res = await provider.GetEntry(path, isFolder, cancellationToken).ConfigureAwait(false); + var res = await provider.GetEntryAsync(path, isFolder, cancellationToken).ConfigureAwait(false); if (res != null) return res; } diff --git a/Duplicati/Library/SourceProvider/Builtin/LocalFileSource.cs b/Duplicati/Library/SourceProvider/Builtin/LocalFileSource.cs index cf1ee6cfa..c45700363 100644 --- a/Duplicati/Library/SourceProvider/Builtin/LocalFileSource.cs +++ b/Duplicati/Library/SourceProvider/Builtin/LocalFileSource.cs @@ -33,10 +33,10 @@ public class LocalFileSource(ISnapshotService snapshotService) : ISourceProvider public string MountedPath => string.Empty; /// - public Task Initialize(CancellationToken cancellationToken) => Task.CompletedTask; + public Task InitializeAsync(CancellationToken cancellationToken) => Task.CompletedTask; /// - public IAsyncEnumerable Enumerate(CancellationToken cancellationToken) + public IAsyncEnumerable EnumerateAsync(CancellationToken cancellationToken) => snapshotService.EnumerateFilesystemEntries().ToAsyncEnumerable(); /// @@ -45,11 +45,11 @@ public class LocalFileSource(ISnapshotService snapshotService) : ISourceProvider public ISnapshotService SnapshotService => snapshotService; /// - public Task GetEntry(string path, bool isFolder, CancellationToken cancellationToken) + public Task GetEntryAsync(string path, bool isFolder, CancellationToken cancellationToken) => Task.FromResult(snapshotService.GetFilesystemEntry(path, isFolder)); /// - public Task Test(CancellationToken cancellationToken) + public Task TestAsync(CancellationToken cancellationToken) => snapshotService.DirectoryExists(MountedPath) ? Task.CompletedTask : throw new Exception($"The path {MountedPath} does not exist"); /// diff --git a/Duplicati/Library/Utility/BackendExtensions.cs b/Duplicati/Library/Utility/BackendExtensions.cs index 333074464..e571c8562 100644 --- a/Duplicati/Library/Utility/BackendExtensions.cs +++ b/Duplicati/Library/Utility/BackendExtensions.cs @@ -46,11 +46,23 @@ public static class BackendExtensions public const string TEST_FILE_CONTENT = "This file is used by Duplicati to test access permissions and can be safely deleted."; /// - /// Tests a backend by invoking the List() method. - /// As long as the iteration can either complete or find at least one file without throwing, the test is successful + /// Test the backend in either read-only or read-write mode. + /// + /// The backend to test + /// If the test is also checking the destination for write permissions + /// Cancellation token + /// An awaitable tasks + public static Task TestBackendAsync(this IBackend backend, bool alsoWrite, CancellationToken cancellationToken) + => alsoWrite + ? TestReadWritePermissionsAsync(backend, cancellationToken) + : TestReadPermissionsAsync(backend, cancellationToken); + + /// + /// Tests a backend by invoking the List() method, and attempting to write a small file on the destination. /// /// Backend to test /// Cancellation token + /// An awaitable tasks public static async Task TestReadWritePermissionsAsync(this IBackend backend, CancellationToken cancellationToken) { // Remove the file if it exists @@ -142,4 +154,32 @@ public static class BackendExtensions throw new TestAfterConnectException(Strings.BackendExtensions.ErrorDeleteFile(TEST_FILE_NAME, e.Message), "TestCleanupError", e); } } + + /// + /// Tests a backend by invoking the List() method. + /// As long as the iteration can return one page, the test is considered successful. + /// + /// Backend to test + /// Cancellation token + /// An awaitable tasks + public static async Task TestReadPermissionsAsync(this IBackend backend, CancellationToken cancellationToken) + { + try + { + await foreach (var entry in backend + .ListAsync(cancellationToken) + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) + { + break; + } + } + catch (Exception e) + // Don't catch FolderMissingException or FileMissingException + // so we can pass those through to the caller + when (e is not FolderMissingException && e is not FileMissingException) + { + throw new UserInformationException(Strings.BackendExtensions.ErrorListContent(e.Message), "TestPreparationError", e); + } + } } diff --git a/Duplicati/UnitTest/ArchiveAttributeTests.cs b/Duplicati/UnitTest/ArchiveAttributeTests.cs index abf853a71..ad68c1ae5 100644 --- a/Duplicati/UnitTest/ArchiveAttributeTests.cs +++ b/Duplicati/UnitTest/ArchiveAttributeTests.cs @@ -81,8 +81,8 @@ namespace Duplicati.UnitTest public Task PutAsync(string remotename, string filename, CancellationToken cancellationToken) => backend.PutAsync(remotename, filename, cancellationToken); - public Task TestAsync(CancellationToken cancellationToken) - => backend.TestAsync(cancellationToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancellationToken) + => backend.TestAsync(alsoWrite, cancellationToken); } [Test] diff --git a/Duplicati/UnitTest/DeterministicErrorBackend.cs b/Duplicati/UnitTest/DeterministicErrorBackend.cs index b2bf97a38..d233d3430 100644 --- a/Duplicati/UnitTest/DeterministicErrorBackend.cs +++ b/Duplicati/UnitTest/DeterministicErrorBackend.cs @@ -111,9 +111,9 @@ namespace Duplicati.UnitTest await m_backend.DeleteAsync(remotename, cancelToken).ConfigureAwait(false); ThrowError(BackendAction.DeleteAfter, remotename); } - public Task TestAsync(CancellationToken cancelToken) + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) { - return m_backend.TestAsync(cancelToken); + return m_backend.TestAsync(alsoWrite, cancelToken); } public Task CreateFolderAsync(CancellationToken cancelToken) { diff --git a/Duplicati/UnitTest/DiskImage/DiskImageTests.cs b/Duplicati/UnitTest/DiskImage/DiskImageTests.cs index b083859dd..7306a42d7 100644 --- a/Duplicati/UnitTest/DiskImage/DiskImageTests.cs +++ b/Duplicati/UnitTest/DiskImage/DiskImageTests.cs @@ -902,11 +902,11 @@ public class DiskImageTests : BasicSetupHelper using var provider = new SourceProvider(sourceUrl, "", new Dictionary()); // Initialize the provider - await provider.Initialize(CancellationToken.None); + await provider.InitializeAsync(CancellationToken.None); // Enumerate entries var diskEntries = new List(); - await foreach (var entry in provider.Enumerate(CancellationToken.None)) + await foreach (var entry in provider.EnumerateAsync(CancellationToken.None)) { diskEntries.Add(entry); await TestContext.Progress.WriteLineAsync($"Found entry: {entry.Path} (IsFolder: {entry.IsFolder}, Size: {entry.Size})"); diff --git a/Duplicati/UnitTest/Issue5845.cs b/Duplicati/UnitTest/Issue5845.cs index 1f37ca1a2..c566eff98 100644 --- a/Duplicati/UnitTest/Issue5845.cs +++ b/Duplicati/UnitTest/Issue5845.cs @@ -79,8 +79,8 @@ namespace Duplicati.UnitTest public Task PutAsync(string remotename, string filename, CancellationToken cancellationToken) => backend.PutAsync(remotename, filename, cancellationToken); - public Task TestAsync(CancellationToken cancellationToken) - => backend.TestAsync(cancellationToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancellationToken) + => backend.TestAsync(alsoWrite, cancellationToken); } [Test] diff --git a/Duplicati/UnitTest/Issue5862.cs b/Duplicati/UnitTest/Issue5862.cs index 3507dae7a..01d3b696b 100644 --- a/Duplicati/UnitTest/Issue5862.cs +++ b/Duplicati/UnitTest/Issue5862.cs @@ -75,8 +75,8 @@ namespace Duplicati.UnitTest public Task PutAsync(string remotename, string filename, CancellationToken cancellationToken) => throw new DeterministicErrorBackend.DeterministicErrorBackendException("Prevent"); - public Task TestAsync(CancellationToken cancellationToken) - => backend.TestAsync(cancellationToken); + public Task TestAsync(bool alsoWrite, CancellationToken cancellationToken) + => backend.TestAsync(alsoWrite, cancellationToken); } [Test] diff --git a/Duplicati/UnitTest/RandomErrorBackend.cs b/Duplicati/UnitTest/RandomErrorBackend.cs index 060143f06..7a04a4849 100644 --- a/Duplicati/UnitTest/RandomErrorBackend.cs +++ b/Duplicati/UnitTest/RandomErrorBackend.cs @@ -94,9 +94,9 @@ namespace Duplicati.UnitTest await m_backend.DeleteAsync(remotename, cancelToken).ConfigureAwait(false); ThrowErrorRandom(); } - public Task TestAsync(CancellationToken cancelToken) + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) { - return m_backend.TestAsync(cancelToken); + return m_backend.TestAsync(alsoWrite, cancelToken); } public Task CreateFolderAsync(CancellationToken cancelToken) { diff --git a/Duplicati/UnitTest/SizeOmittingBackend.cs b/Duplicati/UnitTest/SizeOmittingBackend.cs index f6b437ae0..6a6c02d55 100644 --- a/Duplicati/UnitTest/SizeOmittingBackend.cs +++ b/Duplicati/UnitTest/SizeOmittingBackend.cs @@ -78,9 +78,9 @@ namespace Duplicati.UnitTest { await m_backend.DeleteAsync(remotename, cancelToken).ConfigureAwait(false); } - public Task TestAsync(CancellationToken cancelToken) + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) { - return m_backend.TestAsync(cancelToken); + return m_backend.TestAsync(alsoWrite, cancelToken); } public Task CreateFolderAsync(CancellationToken cancelToken) { diff --git a/Duplicati/UnitTest/SoftDeleteTests.cs b/Duplicati/UnitTest/SoftDeleteTests.cs index 21689a930..211f86169 100644 --- a/Duplicati/UnitTest/SoftDeleteTests.cs +++ b/Duplicati/UnitTest/SoftDeleteTests.cs @@ -506,9 +506,9 @@ namespace Duplicati.UnitTest return m_backend.DeleteAsync(remotename, cancelToken); } - public Task TestAsync(CancellationToken cancelToken) + public Task TestAsync(bool alsoWrite, CancellationToken cancelToken) { - return m_backend.TestAsync(cancelToken); + return m_backend.TestAsync(alsoWrite, cancelToken); } public Task CreateFolderAsync(CancellationToken cancelToken) diff --git a/Duplicati/WebserverCore/Dto/V2/DestinationTestRequestDto.cs b/Duplicati/WebserverCore/Dto/V2/DestinationTestRequestDto.cs index 0bcf1618f..84d2f81b1 100644 --- a/Duplicati/WebserverCore/Dto/V2/DestinationTestRequestDto.cs +++ b/Duplicati/WebserverCore/Dto/V2/DestinationTestRequestDto.cs @@ -67,6 +67,11 @@ public sealed record DestinationTestRequestDto /// public required bool AutoCreate { get; init; } = false; + /// + /// Whether to test read-only access + /// + public bool ReadOnlyTest { get; init; } = false; + /// /// The type of remote destination /// diff --git a/Duplicati/WebserverCore/Endpoints/V1/RemoteOperation.cs b/Duplicati/WebserverCore/Endpoints/V1/RemoteOperation.cs index 23b71c7ef..29faa99d4 100644 --- a/Duplicati/WebserverCore/Endpoints/V1/RemoteOperation.cs +++ b/Duplicati/WebserverCore/Endpoints/V1/RemoteOperation.cs @@ -39,8 +39,8 @@ namespace Duplicati.WebserverCore.Endpoints.V1 => ExecuteDbPath(input.path)) .RequireAuthorization(); - group.MapPost("/remoteoperation/test", ([FromServices] Connection connection, [FromServices] IApplicationSettings applicationSettings, [FromQuery] bool? autocreate, [FromQuery] Dto.V2.RemoteDestinationType? type, [FromBody] RemoteOperationInput input, CancellationToken cancelToken) - => ExecuteTestAsync(connection, applicationSettings, input.path, input.backupId, input.connectionStringId, input.sourcePrefix, autocreate ?? false, type ?? Dto.V2.RemoteDestinationType.Backend, cancelToken)) + group.MapPost("/remoteoperation/test", ([FromServices] Connection connection, [FromServices] IApplicationSettings applicationSettings, [FromQuery] bool? autocreate, [FromQuery] bool? readOnlyTest, [FromQuery] Dto.V2.RemoteDestinationType? type, [FromBody] RemoteOperationInput input, CancellationToken cancelToken) + => ExecuteTestAsync(connection, applicationSettings, input.path, input.backupId, input.connectionStringId, input.sourcePrefix, autocreate ?? false, readOnlyTest ?? false, type ?? Dto.V2.RemoteDestinationType.Backend, cancelToken)) .RequireAuthorization(); group.MapPost("/remoteoperation/create", ([FromServices] Connection connection, [FromServices] IApplicationSettings applicationSettings, [FromBody] RemoteOperationInput input, CancellationToken cancelToken) @@ -54,7 +54,7 @@ namespace Duplicati.WebserverCore.Endpoints.V1 return new Dto.GetDbPathDto(!string.IsNullOrWhiteSpace(path), path); } - private static async Task ExecuteTestAsync(Connection connection, IApplicationSettings applicationSettings, string maskedurl, string? backupId, long? connectionStringId, string? sourcePrefix, bool autoCreate, Dto.V2.RemoteDestinationType type, CancellationToken cancelToken) + private static async Task ExecuteTestAsync(Connection connection, IApplicationSettings applicationSettings, string maskedurl, string? backupId, long? connectionStringId, string? sourcePrefix, bool autoCreate, bool readOnlyTest, Dto.V2.RemoteDestinationType type, CancellationToken cancelToken) { try { @@ -62,7 +62,7 @@ namespace Duplicati.WebserverCore.Endpoints.V1 { using var wrapper = await SharedRemoteOperation.GetSourceProviderForTestingAsync(connection, applicationSettings, maskedurl, backupId, connectionStringId ?? -1, sourcePrefix, cancelToken); using (var s = wrapper.SourceProvider) - await s.Test(cancelToken).ConfigureAwait(false); + await s.TestAsync(cancelToken).ConfigureAwait(false); } else if (type == Dto.V2.RemoteDestinationType.RestoreDestinationProvider) { @@ -75,14 +75,14 @@ namespace Duplicati.WebserverCore.Endpoints.V1 using var wrapper = await SharedRemoteOperation.GetBackendAsync(connection, applicationSettings, maskedurl, backupId, connectionStringId ?? -1, cancelToken); using (var b = wrapper.Backend) { - try { await b.TestAsync(cancelToken).ConfigureAwait(false); } + try { await b.TestAsync(!readOnlyTest, cancelToken).ConfigureAwait(false); } catch (Exception ex) when (SharedRemoteOperation.GetInnerException(ex) is FolderMissingException) { - if (!autoCreate) + if (!autoCreate || readOnlyTest) throw; await b.CreateFolderAsync(cancelToken).ConfigureAwait(false); - await b.TestAsync(cancelToken).ConfigureAwait(false); + await b.TestAsync(!readOnlyTest, cancelToken).ConfigureAwait(false); } } } diff --git a/Duplicati/WebserverCore/Endpoints/V2/DestinationVerify.cs b/Duplicati/WebserverCore/Endpoints/V2/DestinationVerify.cs index f621bfc1b..78494f1a5 100644 --- a/Duplicati/WebserverCore/Endpoints/V2/DestinationVerify.cs +++ b/Duplicati/WebserverCore/Endpoints/V2/DestinationVerify.cs @@ -49,14 +49,11 @@ public class DestinationVerify : IEndpointV2 { using var wrapper = await SharedRemoteOperation.GetSourceProviderForTestingAsync(connection, applicationSettings, input.DestinationUrl, input.BackupId, input.ConnectionStringId ?? -1, input.SourcePrefix, cancelToken); - // We do not call TestAsync here, because we may have read-only access to the source, and test will verify write access - // Instead we just check if we can enumerate the files, which is the main thing we need to verify for a source provider - - // Technically we also count folders as files here, but really we just want to know if there is data to backup - var anyFiles = await wrapper.SourceProvider.Enumerate(cancelToken).AnyAsync(cancelToken); + // Call the specific Test method on the ISourceProvider (read-only test) + await wrapper.SourceProvider.TestAsync(cancelToken); return DestinationTestResponseDto.Create( - anyFiles: anyFiles, + anyFiles: true, anyBackups: false, anyEncryptedFiles: false ); @@ -65,7 +62,7 @@ public class DestinationVerify : IEndpointV2 { using var wrapper = await SharedRemoteOperation.GetRestoreDestinationProviderForTestingAsync(connection, applicationSettings, input.DestinationUrl, input.BackupId, input.ConnectionStringId ?? -1, input.SourcePrefix, cancelToken); - // Here we do call TestAsync, because we need to verify write access + // Call the specific Test method on the IRestoreDestinationProvider (should be a write test) await wrapper.RestoreDestinationProvider.Test(cancelToken); return DestinationTestResponseDto.Create( @@ -80,14 +77,14 @@ public class DestinationVerify : IEndpointV2 using (var b = wrapper.Backend) { - try { await b.TestAsync(cancelToken).ConfigureAwait(false); } + try { await b.TestAsync(!input.ReadOnlyTest, cancelToken).ConfigureAwait(false); } catch (Exception ex) when (SharedRemoteOperation.GetInnerException(ex) is FolderMissingException) { - if (!input.AutoCreate) + if (!input.AutoCreate || input.ReadOnlyTest) throw; await b.CreateFolderAsync(cancelToken).ConfigureAwait(false); - await b.TestAsync(cancelToken).ConfigureAwait(false); + await b.TestAsync(!input.ReadOnlyTest, cancelToken).ConfigureAwait(false); } var anyFiles = false; diff --git a/proprietary/DiskImage/SourceProvider.cs b/proprietary/DiskImage/SourceProvider.cs index 08ff573e2..e6509a34a 100644 --- a/proprietary/DiskImage/SourceProvider.cs +++ b/proprietary/DiskImage/SourceProvider.cs @@ -102,7 +102,7 @@ public sealed class SourceProvider : ISourceProviderModule, IDisposable internal bool TreatFilesystemAsUnknown => _treatFilesystemAsUnknown; /// - public async Task Initialize(CancellationToken cancellationToken) + public async Task InitializeAsync(CancellationToken cancellationToken) { if (string.IsNullOrEmpty(_devicePath)) throw new UserInformationException("Disk device path is not specified.", "DiskDeviceNotSpecified"); @@ -132,7 +132,7 @@ public sealed class SourceProvider : ISourceProviderModule, IDisposable } /// - public Task Test(CancellationToken cancellationToken) + public Task TestAsync(CancellationToken cancellationToken) { if (_disk == null) throw new InvalidOperationException("Provider not initialized."); @@ -140,7 +140,7 @@ public sealed class SourceProvider : ISourceProviderModule, IDisposable } /// - public async IAsyncEnumerable Enumerate([EnumeratorCancellation] CancellationToken cancellationToken) + public async IAsyncEnumerable EnumerateAsync([EnumeratorCancellation] CancellationToken cancellationToken) { if (_disk == null) throw new InvalidOperationException("Provider not initialized."); @@ -150,7 +150,7 @@ public sealed class SourceProvider : ISourceProviderModule, IDisposable } /// - public async Task GetEntry(string path, bool isFolder, CancellationToken cancellationToken) + public async Task GetEntryAsync(string path, bool isFolder, CancellationToken cancellationToken) { if (_disk == null) throw new InvalidOperationException("Provider not initialized."); diff --git a/proprietary/DiskImage/WebModule.cs b/proprietary/DiskImage/WebModule.cs index 423d30734..beca17639 100644 --- a/proprietary/DiskImage/WebModule.cs +++ b/proprietary/DiskImage/WebModule.cs @@ -64,7 +64,7 @@ public class WebModule : IWebModule var subpath = parts.Last(); using var client = new SourceProvider("diskimage://" + physicalDrivePath, "", new Dictionary(options)); - await client.Initialize(cancellationToken); + await client.InitializeAsync(cancellationToken); if (string.IsNullOrWhiteSpace(subpath)) return new Dictionary() @@ -72,7 +72,7 @@ public class WebModule : IWebModule {$"{physicalDrivePath}{Path.DirectorySeparatorChar}root{Path.DirectorySeparatorChar}", "{}"} }; - var targetEntry = await client.GetEntry(subpath, isFolder: true, cancellationToken).ConfigureAwait(false) + var targetEntry = await client.GetEntryAsync(subpath, isFolder: true, cancellationToken).ConfigureAwait(false) ?? throw new DirectoryNotFoundException($"Path not found: {path}"); var result = new Dictionary(); diff --git a/proprietary/GoogleWorkspace/RestoreProvider/RestoreProvider.cs b/proprietary/GoogleWorkspace/RestoreProvider/RestoreProvider.cs index a8efb7674..9af79a987 100644 --- a/proprietary/GoogleWorkspace/RestoreProvider/RestoreProvider.cs +++ b/proprietary/GoogleWorkspace/RestoreProvider/RestoreProvider.cs @@ -145,8 +145,8 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule if (!_hasSetOverwriteOption) throw new UserInformationException(Strings.RestoreTargetMissingOverwriteOption("overwrite", OptionsHelper.GOOGLE_IGNORE_EXISTING_OPTION), "OverwriteOptionNotSet"); - await SourceProvider.Initialize(cancel); - var entry = await SourceProvider.GetEntry(_restorePath, isFolder: true, cancel); + await SourceProvider.InitializeAsync(cancel); + var entry = await SourceProvider.GetEntryAsync(_restorePath, isFolder: true, cancel); if (entry == null) throw new UserInformationException(Strings.RestoreTargetNotFound(_restorePath), "RestoreTargetNotFound"); var metadata = await entry.GetMinorMetadata(cancel); @@ -197,7 +197,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule if (_temporaryFiles.ContainsKey(path)) return true; - var entry = await SourceProvider.GetEntry(path, isFolder: false, cancel).ConfigureAwait(false); + var entry = await SourceProvider.GetEntryAsync(path, isFolder: false, cancel).ConfigureAwait(false); return entry != null; } @@ -218,7 +218,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule if (_temporaryFiles.ContainsKey(path)) return SystemIO.IO_OS.FileOpenRead(_temporaryFiles[path]); - var entry = await SourceProvider.GetEntry(path, isFolder: false, cancel).ConfigureAwait(false); + var entry = await SourceProvider.GetEntryAsync(path, isFolder: false, cancel).ConfigureAwait(false); if (entry != null) return await entry.OpenRead(cancel).ConfigureAwait(false); @@ -233,7 +233,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule if (_temporaryFiles.ContainsKey(path)) return SystemIO.IO_OS.FileOpenReadWrite(_temporaryFiles[path]); - var entry = await SourceProvider.GetEntry(path, isFolder: false, cancel).ConfigureAwait(false); + var entry = await SourceProvider.GetEntryAsync(path, isFolder: false, cancel).ConfigureAwait(false); if (entry == null) { _temporaryFiles.GetOrAdd(path, _ => new TempFile()); @@ -260,7 +260,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule if (_temporaryFiles.ContainsKey(path)) return SystemIO.IO_OS.FileLength(_temporaryFiles[path]); - var entry = await SourceProvider.GetEntry(path, isFolder: false, cancel).ConfigureAwait(false); + var entry = await SourceProvider.GetEntryAsync(path, isFolder: false, cancel).ConfigureAwait(false); if (entry == null) throw new FileNotFoundException($"File not found: {path}"); diff --git a/proprietary/GoogleWorkspace/SourceProvider/SourceProvider.cs b/proprietary/GoogleWorkspace/SourceProvider/SourceProvider.cs index 17890eccf..5210d60cb 100644 --- a/proprietary/GoogleWorkspace/SourceProvider/SourceProvider.cs +++ b/proprietary/GoogleWorkspace/SourceProvider/SourceProvider.cs @@ -114,12 +114,12 @@ public sealed class SourceProvider : ISourceProviderModule, IDisposable { } - public IAsyncEnumerable Enumerate(CancellationToken cancellationToken) + public IAsyncEnumerable EnumerateAsync(CancellationToken cancellationToken) { return new RootSourceEntry(this).Enumerate(cancellationToken); } - public async Task GetEntry(string path, bool isFolder, CancellationToken cancellationToken) + public async Task GetEntryAsync(string path, bool isFolder, CancellationToken cancellationToken) { if (_entryCache.TryGetValue(path, out var cachedEntry)) return cachedEntry; @@ -179,7 +179,7 @@ public sealed class SourceProvider : ISourceProviderModule, IDisposable return resultEntry; } - public Task Initialize(CancellationToken cancellationToken) + public Task InitializeAsync(CancellationToken cancellationToken) { if (!_hasSetMetadataStorageOption) throw new UserInformationException(Strings.MetadataStorageNotEnabled("store-metadata-content-in-database"), "DatabaseMetadataStorageNotEnabled"); @@ -187,7 +187,7 @@ public sealed class SourceProvider : ISourceProviderModule, IDisposable return Task.CompletedTask; } - public Task Test(CancellationToken cancellationToken) + public Task TestAsync(CancellationToken cancellationToken) { _apiHelper.TestConnection(); return Task.CompletedTask; diff --git a/proprietary/GoogleWorkspace/WebModule/WebModule.cs b/proprietary/GoogleWorkspace/WebModule/WebModule.cs index ea60e57de..2cba7fd31 100644 --- a/proprietary/GoogleWorkspace/WebModule/WebModule.cs +++ b/proprietary/GoogleWorkspace/WebModule/WebModule.cs @@ -95,9 +95,9 @@ public class WebModule : IWebModule forwardoptions[key!] = uri.QueryParameters[key]; using var client = new SourceProvider(url, "", forwardoptions, true); - await client.Initialize(cancellationToken); + await client.InitializeAsync(cancellationToken); - var targetEntry = await client.GetEntry((path ?? "").TrimStart('/'), isFolder: true, cancellationToken).ConfigureAwait(false); + var targetEntry = await client.GetEntryAsync((path ?? "").TrimStart('/'), isFolder: true, cancellationToken).ConfigureAwait(false); if (targetEntry == null) throw new DirectoryNotFoundException($"Path not found: {path}"); diff --git a/proprietary/Office365/RestoreProvider/RestoreProvider.Email.cs b/proprietary/Office365/RestoreProvider/RestoreProvider.Email.cs index f62156c6a..78441b40e 100644 --- a/proprietary/Office365/RestoreProvider/RestoreProvider.Email.cs +++ b/proprietary/Office365/RestoreProvider/RestoreProvider.Email.cs @@ -76,7 +76,7 @@ partial class RestoreProvider Uri.EscapeDataString(Office365UserType.Mailbox.ToString().ToLowerInvariant()) }); - var mailbox = await Provider.SourceProvider.GetEntry(path, true, cancel); + var mailbox = await Provider.SourceProvider.GetEntryAsync(path, true, cancel); if (mailbox == null) throw new InvalidOperationException($"Mailbox not found for user {userId}"); diff --git a/proprietary/Office365/RestoreProvider/RestoreProvider.cs b/proprietary/Office365/RestoreProvider/RestoreProvider.cs index 8482503c3..28e5030f9 100644 --- a/proprietary/Office365/RestoreProvider/RestoreProvider.cs +++ b/proprietary/Office365/RestoreProvider/RestoreProvider.cs @@ -192,8 +192,8 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule throw new UserInformationException(Strings.RestoreTargetMissingOverwriteOption("overwrite", OptionsHelper.OFFICE_IGNORE_EXISTING_OPTION), "OverwriteOptionNotSet"); await _apiHelper.AcquireAccessTokenAsync(true, cancel); - await SourceProvider.Initialize(cancel); - var entry = await SourceProvider.GetEntry(_restorePath, isFolder: true, cancel); + await SourceProvider.InitializeAsync(cancel); + var entry = await SourceProvider.GetEntryAsync(_restorePath, isFolder: true, cancel); if (entry == null) throw new UserInformationException($"Restore target path not found: {_restorePath}", "RestoreTargetNotFound"); var metadata = await entry.GetMinorMetadata(cancel); @@ -261,7 +261,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule if (_temporaryFiles.ContainsKey(path)) return true; - var entry = await SourceProvider.GetEntry(path, isFolder: false, cancel).ConfigureAwait(false); + var entry = await SourceProvider.GetEntryAsync(path, isFolder: false, cancel).ConfigureAwait(false); return entry != null; } @@ -282,7 +282,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule if (_temporaryFiles.ContainsKey(path)) return SystemIO.IO_OS.FileOpenRead(_temporaryFiles[path]); - var entry = await SourceProvider.GetEntry(path, isFolder: false, cancel).ConfigureAwait(false); + var entry = await SourceProvider.GetEntryAsync(path, isFolder: false, cancel).ConfigureAwait(false); if (entry != null) return await entry.OpenRead(cancel).ConfigureAwait(false); @@ -297,7 +297,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule if (_temporaryFiles.ContainsKey(path)) return SystemIO.IO_OS.FileOpenReadWrite(_temporaryFiles[path]); - var entry = await SourceProvider.GetEntry(path, isFolder: false, cancel).ConfigureAwait(false); + var entry = await SourceProvider.GetEntryAsync(path, isFolder: false, cancel).ConfigureAwait(false); if (entry == null) { _temporaryFiles.GetOrAdd(path, _ => new TempFile()); @@ -324,7 +324,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule if (_temporaryFiles.ContainsKey(path)) return SystemIO.IO_OS.FileLength(_temporaryFiles[path]); - var entry = await SourceProvider.GetEntry(path, isFolder: false, cancel).ConfigureAwait(false); + var entry = await SourceProvider.GetEntryAsync(path, isFolder: false, cancel).ConfigureAwait(false); if (entry == null) throw new FileNotFoundException($"File not found: {path}"); diff --git a/proprietary/Office365/SourceProvider/SourceProvider.cs b/proprietary/Office365/SourceProvider/SourceProvider.cs index 319e8acf3..abdd948f3 100644 --- a/proprietary/Office365/SourceProvider/SourceProvider.cs +++ b/proprietary/Office365/SourceProvider/SourceProvider.cs @@ -160,7 +160,7 @@ public sealed partial class SourceProvider : ISourceProviderModule, IDisposable /// - public Task Initialize(CancellationToken cancellationToken) + public Task InitializeAsync(CancellationToken cancellationToken) { if (!_hasSetMetadataStorageOption) throw new UserInformationException(Strings.MetadataStorageNotEnabled("store-metadata-content-in-database"), "DatabaseMetadataStorageNotEnabled"); @@ -169,17 +169,17 @@ public sealed partial class SourceProvider : ISourceProviderModule, IDisposable } /// - public Task Test(CancellationToken cancellationToken) + public Task TestAsync(CancellationToken cancellationToken) => _apiHelper.AcquireAccessTokenAsync(false, cancellationToken); /// - public async IAsyncEnumerable Enumerate([EnumeratorCancellation] CancellationToken cancellationToken) + public async IAsyncEnumerable EnumerateAsync([EnumeratorCancellation] CancellationToken cancellationToken) { yield return new RootSourceEntry(this, _mountPoint); } /// - public async Task GetEntry(string path, bool isFolder, CancellationToken cancellationToken) + public async Task GetEntryAsync(string path, bool isFolder, CancellationToken cancellationToken) { if (_entryCache.TryGetValue(path, out var cachedEntry)) return cachedEntry; diff --git a/proprietary/Office365/WebModule/WebModule.cs b/proprietary/Office365/WebModule/WebModule.cs index 846ebaaae..0733c4bd5 100644 --- a/proprietary/Office365/WebModule/WebModule.cs +++ b/proprietary/Office365/WebModule/WebModule.cs @@ -90,9 +90,9 @@ public class WebModule : IWebModule forwardoptions[key!] = uri.QueryParameters[key]; using var client = new SourceProvider(url, "", forwardoptions); - await client.Initialize(cancellationToken); + await client.InitializeAsync(cancellationToken); - var targetEntry = await client.GetEntry((path ?? "").TrimStart('/'), isFolder: true, cancellationToken).ConfigureAwait(false); + var targetEntry = await client.GetEntryAsync((path ?? "").TrimStart('/'), isFolder: true, cancellationToken).ConfigureAwait(false); if (targetEntry == null) throw new DirectoryNotFoundException($"Path not found: {path}");