From 6ac07531bd7a4e7bbb9b8811b2a8a768cb07d976 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Wed, 14 Jan 2026 09:49:56 +0100 Subject: [PATCH] Change webmodule to have async return value --- .../Backend/Duplicati/ListBackupsModule.cs | 4 +-- .../Library/Backend/Filen/GetApiKeyModule.cs | 4 +-- .../Backend/GoogleServices/GCSConfig.cs | 4 +-- .../Library/Backend/OpenStack/SwiftConfig.cs | 4 +-- Duplicati/Library/Backend/S3/S3Config.cs | 4 +-- Duplicati/Library/Backend/S3/S3IAM.cs | 35 +++++++++---------- .../Library/Backend/SSHv2/KeyGenerator.cs | 6 ++-- .../Library/Backend/SSHv2/KeyUploader.cs | 22 ++++++------ .../Library/Backend/Storj/StorjConfig.cs | 4 +-- Duplicati/Library/Interface/IWebModule.cs | 22 +++--------- .../WebserverCore/Endpoints/V1/WebModules.cs | 2 +- 11 files changed, 49 insertions(+), 62 deletions(-) diff --git a/Duplicati/Library/Backend/Duplicati/ListBackupsModule.cs b/Duplicati/Library/Backend/Duplicati/ListBackupsModule.cs index 179096b14..0b9e01dcf 100644 --- a/Duplicati/Library/Backend/Duplicati/ListBackupsModule.cs +++ b/Duplicati/Library/Backend/Duplicati/ListBackupsModule.cs @@ -43,7 +43,7 @@ public class ListBackupsModule : IWebModule } /// - public IDictionary Execute(IDictionary options) + public async Task> Execute(IDictionary options, CancellationToken cancellationToken) { var opts = new Dictionary(options); @@ -71,7 +71,7 @@ public class ListBackupsModule : IWebModule using var backend = new DuplicatiBackend(url, opts); return new Dictionary() { - { "folders", JsonSerializer.Serialize( backend.ListBackupFolders(CancellationToken.None).ToArrayAsync().Await()) } + { "folders", JsonSerializer.Serialize( await backend.ListBackupFolders(cancellationToken).ToArrayAsync(cancellationToken) ) } }; } diff --git a/Duplicati/Library/Backend/Filen/GetApiKeyModule.cs b/Duplicati/Library/Backend/Filen/GetApiKeyModule.cs index ac0cf6325..329bd13d5 100644 --- a/Duplicati/Library/Backend/Filen/GetApiKeyModule.cs +++ b/Duplicati/Library/Backend/Filen/GetApiKeyModule.cs @@ -53,7 +53,7 @@ public class GetApiKeyModule : IWebModule ]; /// - public IDictionary Execute(IDictionary options) + public async Task> Execute(IDictionary options, CancellationToken cancellationToken) { options.TryGetValue("filen-operation", out var operation); if (operation != "GetApiKey") @@ -71,7 +71,7 @@ public class GetApiKeyModule : IWebModule newOpts[key] = uri.QueryParameters[key]; var backend = new FilenBackend(url, newOpts); - var apiKey = backend.GetApiKey(CancellationToken.None).Await(); + var apiKey = await backend.GetApiKey(cancellationToken).ConfigureAwait(false); return new Dictionary { { "api-key", apiKey ?? string.Empty } }; } diff --git a/Duplicati/Library/Backend/GoogleServices/GCSConfig.cs b/Duplicati/Library/Backend/GoogleServices/GCSConfig.cs index a1cc64f6d..ba9156b82 100644 --- a/Duplicati/Library/Backend/GoogleServices/GCSConfig.cs +++ b/Duplicati/Library/Backend/GoogleServices/GCSConfig.cs @@ -42,11 +42,11 @@ namespace Duplicati.Library.Backend.GoogleServices #region IWebModule implementation - public IDictionary Execute(IDictionary options) + public Task> Execute(IDictionary options, CancellationToken cancellationToken) { var ct = Utility.Utility.ParseEnumOption(options.AsReadOnly(), KEY_CONFIGTYPE, DEFAULT_CONFIG_TYPE); GetLookups().TryGetValue(ct.ToString(), out var dict); - return dict ?? new Dictionary(); + return Task.FromResult(dict ?? new Dictionary()); } public string Key => "gcs-getconfig"; diff --git a/Duplicati/Library/Backend/OpenStack/SwiftConfig.cs b/Duplicati/Library/Backend/OpenStack/SwiftConfig.cs index 25dbd36da..9fa5a1135 100644 --- a/Duplicati/Library/Backend/OpenStack/SwiftConfig.cs +++ b/Duplicati/Library/Backend/OpenStack/SwiftConfig.cs @@ -38,11 +38,11 @@ namespace Duplicati.Library.Backend.OpenStack { } - public IDictionary Execute(IDictionary options) + public Task> Execute(IDictionary options, CancellationToken cancellationToken) { var ct = Utility.Utility.ParseEnumOption(options.AsReadOnly(), KEY_CONFIGTYPE, DEFAULT_CONFIG_TYPE); GetLookups().TryGetValue(ct.ToString(), out var dict); - return dict ?? new Dictionary(); + return Task.FromResult(dict ?? new Dictionary()); } public string Key => "openstack-getconfig"; diff --git a/Duplicati/Library/Backend/S3/S3Config.cs b/Duplicati/Library/Backend/S3/S3Config.cs index 1af03f4a8..8c709619d 100644 --- a/Duplicati/Library/Backend/S3/S3Config.cs +++ b/Duplicati/Library/Backend/S3/S3Config.cs @@ -43,11 +43,11 @@ namespace Duplicati.Library.Backend #region IWebModule implementation - public IDictionary Execute(IDictionary options) + public Task> Execute(IDictionary options, CancellationToken cancellationToken) { var ct = Utility.Utility.ParseEnumOption(options.AsReadOnly(), KEY_CONFIGTYPE, DEFAULT_CONFIG_TYPE); GetLookups().TryGetValue(ct.ToString(), out var dict); - return dict ?? new Dictionary(); + return Task.FromResult(dict ?? new Dictionary()); } public string Key => "s3-getconfig"; diff --git a/Duplicati/Library/Backend/S3/S3IAM.cs b/Duplicati/Library/Backend/S3/S3IAM.cs index ba3b7f472..f9c0f628b 100644 --- a/Duplicati/Library/Backend/S3/S3IAM.cs +++ b/Duplicati/Library/Backend/S3/S3IAM.cs @@ -76,7 +76,7 @@ namespace Duplicati.Library.Backend new CommandLineArgument(KEY_PASSWORD, CommandLineArgument.ArgumentType.String, Strings.S3IAM.PasswordShort, Strings.S3IAM.PasswordLong) ]); - public IDictionary Execute(IDictionary options) + public async Task> Execute(IDictionary options, CancellationToken cancellationToken) { options.TryGetValue(KEY_OPERATION, out var operationstring); options.TryGetValue(KEY_USERNAME, out var username); @@ -98,12 +98,12 @@ namespace Duplicati.Library.Backend ValidateArgument(username, KEY_USERNAME); ValidateArgument(password, KEY_PASSWORD); ValidateArgument(path, KEY_PATH); - return CreateUnprivilegedUser(username!, password!, path!); + return await CreateUnprivilegedUser(username!, password!, path!, cancellationToken).ConfigureAwait(false); default: ValidateArgument(username, KEY_USERNAME); ValidateArgument(password, KEY_PASSWORD); - return CanCreateUser(username!, password!); + return await CanCreateUser(username!, password!, cancellationToken).ConfigureAwait(false); } } @@ -138,7 +138,7 @@ namespace Duplicati.Library.Backend } - private static bool DetermineIfCreateUserIsAllowed(User user, AmazonIdentityManagementServiceClient cl) + private static async Task DetermineIfCreateUserIsAllowed(User user, AmazonIdentityManagementServiceClient cl, CancellationToken cancellationToken) { var simulatePrincipalPolicy = new SimulatePrincipalPolicyRequest { @@ -146,13 +146,12 @@ namespace Duplicati.Library.Backend ActionNames = new[] { "iam:CreateUser" }.ToList() }; - return cl.SimulatePrincipalPolicyAsync(simulatePrincipalPolicy) - .GetAwaiter().GetResult() - .EvaluationResults.First() - .EvalDecision == PolicyEvaluationDecisionType.Allowed; + return (await cl.SimulatePrincipalPolicyAsync(simulatePrincipalPolicy, cancellationToken).ConfigureAwait(false)) + .EvaluationResults.First() + .EvalDecision == PolicyEvaluationDecisionType.Allowed; } - private static IDictionary GetCreateUserDict(User user, AmazonIdentityManagementServiceClient cl) + private static async Task> GetCreateUserDict(User user, AmazonIdentityManagementServiceClient cl, CancellationToken cancellationToken) { var resultDict = new Dictionary { @@ -164,7 +163,7 @@ namespace Duplicati.Library.Backend try { - resultDict["isroot"] = DetermineIfCreateUserIsAllowed(user, cl).ToString(); + resultDict["isroot"] = (await DetermineIfCreateUserIsAllowed(user, cl, cancellationToken)).ToString(); } catch (Exception ex) { @@ -180,13 +179,13 @@ namespace Duplicati.Library.Backend return resultDict; } - private static IDictionary CanCreateUser(string awsid, string awskey) + private static async Task> CanCreateUser(string awsid, string awskey, CancellationToken cancellationToken) { var cl = new AmazonIdentityManagementServiceClient(awsid, awskey); User user; try { - user = cl.GetUserAsync().Await().User; + user = (await cl.GetUserAsync(cancellationToken).ConfigureAwait(false)).User; } catch (Exception ex) { @@ -198,10 +197,10 @@ namespace Duplicati.Library.Backend }; } - return GetCreateUserDict(user, cl); + return await GetCreateUserDict(user, cl, cancellationToken); } - private static IDictionary CreateUnprivilegedUser(string awsid, string awskey, string path) + private static async Task> CreateUnprivilegedUser(string awsid, string awskey, string path, CancellationToken cancellationToken) { var now = Utility.Utility.SerializeDateTime(DateTime.Now); var username = string.Format("duplicati-autocreated-backup-user-{0}", now); @@ -209,13 +208,13 @@ namespace Duplicati.Library.Backend var policydoc = GeneratePolicyDoc(path); var cl = new AmazonIdentityManagementServiceClient(awsid, awskey); - var user = cl.CreateUserAsync(new CreateUserRequest(username)).GetAwaiter().GetResult().User; - cl.PutUserPolicyAsync(new PutUserPolicyRequest( + var user = (await cl.CreateUserAsync(new CreateUserRequest(username), cancellationToken).ConfigureAwait(false)).User; + await cl.PutUserPolicyAsync(new PutUserPolicyRequest( user.UserName, policyname, policydoc - )).GetAwaiter().GetResult(); - var key = cl.CreateAccessKeyAsync(new CreateAccessKeyRequest { UserName = user.UserName }).GetAwaiter().GetResult().AccessKey; + )).ConfigureAwait(false); + var key = (await cl.CreateAccessKeyAsync(new CreateAccessKeyRequest { UserName = user.UserName }, cancellationToken).ConfigureAwait(false)).AccessKey; return new Dictionary { diff --git a/Duplicati/Library/Backend/SSHv2/KeyGenerator.cs b/Duplicati/Library/Backend/SSHv2/KeyGenerator.cs index fab3dec3c..3484b7a6b 100644 --- a/Duplicati/Library/Backend/SSHv2/KeyGenerator.cs +++ b/Duplicati/Library/Backend/SSHv2/KeyGenerator.cs @@ -143,7 +143,7 @@ namespace Duplicati.Library.Backend } #region IWebModule implementation - public IDictionary Execute(IDictionary options) + public Task> Execute(IDictionary options, CancellationToken cancellationToken) { if (!options.TryGetValue(KEY_TYPE_NAME, out var keytype)) keytype = DEFAULT_KEYTYPE; @@ -174,7 +174,7 @@ namespace Duplicati.Library.Backend key.Modulus ?? [] }; - return OutputKey(EncodeDER(privateEntries), EncodePEM(publicEntries), KEY_TEMPLATE_RSA, PUB_KEY_FORMAT_RSA, username); + return Task.FromResult(OutputKey(EncodeDER(privateEntries), EncodePEM(publicEntries), KEY_TEMPLATE_RSA, PUB_KEY_FORMAT_RSA, username)); } else if (KEYTYPE_DSA.Equals(keytype, StringComparison.OrdinalIgnoreCase)) { @@ -195,7 +195,7 @@ namespace Duplicati.Library.Backend key.Y ?? [] }; - return OutputKey(EncodeDER(privateEntries), EncodePEM(publicEntries), KEY_TEMPLATE_DSA, PUB_KEY_FORMAT_DSA, username); + return Task.FromResult(OutputKey(EncodeDER(privateEntries), EncodePEM(publicEntries), KEY_TEMPLATE_DSA, PUB_KEY_FORMAT_DSA, username)); } else { diff --git a/Duplicati/Library/Backend/SSHv2/KeyUploader.cs b/Duplicati/Library/Backend/SSHv2/KeyUploader.cs index 930a5b4a1..825efd3a4 100644 --- a/Duplicati/Library/Backend/SSHv2/KeyUploader.cs +++ b/Duplicati/Library/Backend/SSHv2/KeyUploader.cs @@ -39,7 +39,7 @@ namespace Duplicati.Library.Backend #region IWebModule implementation - public IDictionary Execute(IDictionary options) + public async Task> Execute(IDictionary options, CancellationToken cancellationToken) { var res = new Dictionary(); @@ -65,20 +65,20 @@ namespace Duplicati.Library.Backend using (var connection = new SSHv2(url, (Dictionary)options)) { - var client = connection.CreateConnection(CancellationToken.None).Await(); + var client = await connection.CreateConnection(cancellationToken).ConfigureAwait(false); try { - client.ChangeDirectory(SSH_FOLDER); + await client.ChangeDirectoryAsync(SSH_FOLDER, cancellationToken).ConfigureAwait(false); } catch { - client.CreateDirectory(SSH_FOLDER); + await client.CreateDirectoryAsync(SSH_FOLDER, cancellationToken).ConfigureAwait(false); client.ChangePermissions(SSH_FOLDER, SSH_FOLDER_PERMISSIONS); - client.ChangeDirectory(SSH_FOLDER); + await client.ChangeDirectoryAsync(SSH_FOLDER, cancellationToken).ConfigureAwait(false); } - var sshfolder = client.ListDirectory(".").First(x => x.Name == "."); - client.ChangeDirectory(".."); + var sshfolder = (await client.ListDirectoryAsync(".", cancellationToken).ToListAsync(cancellationToken)).First(x => x.Name == "."); + await client.ChangeDirectoryAsync("..", cancellationToken).ConfigureAwait(false); if (!sshfolder.OwnerCanRead || !sshfolder.OwnerCanWrite) client.ChangePermissions(SSH_FOLDER, SSH_FOLDER_PERMISSIONS); @@ -86,12 +86,12 @@ namespace Duplicati.Library.Backend var authorized_keys = ""; byte[]? authorized_keys_bytes = null; - var existing_authorized_keys = client.ListDirectory(SSH_FOLDER).Any(x => x.Name == AUTHORIZED_KEYS_FILE); + var existing_authorized_keys = (await client.ListDirectoryAsync(SSH_FOLDER, cancellationToken).ToListAsync(cancellationToken)).Any(x => x.Name == AUTHORIZED_KEYS_FILE); if (existing_authorized_keys) { using (var ms = new MemoryStream()) { - client.DownloadFile(AUTHORIZED_KEYS_PATH, ms); + await client.DownloadFileAsync(AUTHORIZED_KEYS_PATH, ms, cancellationToken).ConfigureAwait(false); authorized_keys_bytes = ms.ToArray(); authorized_keys = System.Text.Encoding.ASCII.GetString(authorized_keys_bytes); } @@ -120,12 +120,12 @@ namespace Duplicati.Library.Backend { var filename = AUTHORIZED_KEYS_PATH + ".backup-" + Utility.Utility.SerializeDateTime(DateTime.UtcNow); using (var ms = new MemoryStream(authorized_keys_bytes)) - client.UploadFile(ms, filename); + await client.UploadFileAsync(ms, filename, cancellationToken).ConfigureAwait(false); client.ChangePermissions(filename, AUTHORIZED_KEYS_BACKUP_PERMISSIONS); } using (var ms = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(new_file))) - client.UploadFile(ms, AUTHORIZED_KEYS_PATH); + await client.UploadFileAsync(ms, AUTHORIZED_KEYS_PATH, cancellationToken).ConfigureAwait(false); if (!existing_authorized_keys) client.ChangePermissions(AUTHORIZED_KEYS_PATH, AUTHORIZED_KEYS_PERMISSIONS); diff --git a/Duplicati/Library/Backend/Storj/StorjConfig.cs b/Duplicati/Library/Backend/Storj/StorjConfig.cs index 9ba7f762f..11e436ee7 100644 --- a/Duplicati/Library/Backend/Storj/StorjConfig.cs +++ b/Duplicati/Library/Backend/Storj/StorjConfig.cs @@ -48,11 +48,11 @@ namespace Duplicati.Library.Backend.Storj new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provide different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType))) ]; - public IDictionary Execute(IDictionary options) + public Task> Execute(IDictionary options, CancellationToken cancellationToken) { var ct = Utility.Utility.ParseEnumOption(options.AsReadOnly(), KEY_CONFIGTYPE, DEFAULT_CONFIG_TYPE); GetLookups().TryGetValue(ct.ToString(), out var dict); - return dict ?? new Dictionary(); + return Task.FromResult(dict ?? new Dictionary()); } public IDictionary> GetLookups() diff --git a/Duplicati/Library/Interface/IWebModule.cs b/Duplicati/Library/Interface/IWebModule.cs index 42c957f98..ab59e34a1 100644 --- a/Duplicati/Library/Interface/IWebModule.cs +++ b/Duplicati/Library/Interface/IWebModule.cs @@ -20,32 +20,20 @@ // DEALINGS IN THE SOFTWARE. using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace Duplicati.Library.Interface { - public interface IWebModule : IDynamicModule + public interface IWebModule : ICommonModule { - /// - /// The module key, used to activate or deactivate the module on the commandline - /// - string Key { get; } - - /// - /// A localized string describing the module with a friendly name - /// - string DisplayName { get; } - - /// - /// A localized description of the module - /// - string Description { get; } - /// /// Execute the specified command with the given options. /// /// The options to use + /// The cancellation token /// A list of output values - IDictionary Execute(IDictionary options); + Task> Execute(IDictionary options, CancellationToken cancellationToken); /// /// Returns lookup data from the module, if any. diff --git a/Duplicati/WebserverCore/Endpoints/V1/WebModules.cs b/Duplicati/WebserverCore/Endpoints/V1/WebModules.cs index 363ee6833..4292f5903 100644 --- a/Duplicati/WebserverCore/Endpoints/V1/WebModules.cs +++ b/Duplicati/WebserverCore/Endpoints/V1/WebModules.cs @@ -63,7 +63,7 @@ public record WebModules : IEndpointV1 return new Dto.WebModuleOutputDto( Status: "OK", - Result: m.Execute(options) + Result: await m.Execute(options, cancellationToken).ConfigureAwait(false) ); }