From 34023c2b4604b6d53c8f05266f36d9a9e91569a9 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 23 Sep 2025 13:48:04 +0200 Subject: [PATCH] Merge pull request #6518 from duplicati/feature/simplify-webmodule-lookups Simplify webmodule lookups --- .../Backend/GoogleServices/GCSConfig.cs | 24 ++++------ .../Backend/OpenStack/OpenStackStorage.cs | 4 +- .../Library/Backend/OpenStack/SwiftConfig.cs | 27 +++++------ Duplicati/Library/Backend/S3/S3Backend.cs | 17 +++---- Duplicati/Library/Backend/S3/S3Config.cs | 32 ++++++------- Duplicati/Library/Backend/S3/S3IAM.cs | 3 ++ .../Library/Backend/SSHv2/KeyGenerator.cs | 4 ++ .../Library/Backend/SSHv2/KeyUploader.cs | 3 ++ .../Library/Backend/Storj/StorjBackend.cs | 4 +- .../Library/Backend/Storj/StorjConfig.cs | 26 +++++------ Duplicati/Library/Interface/IWebModule.cs | 6 +++ .../RestAPI/Serializable/ServerSettings.cs | 6 +++ .../Interface/IDynamicModule.cs | 46 +++++++++++-------- 13 files changed, 108 insertions(+), 94 deletions(-) diff --git a/Duplicati/Library/Backend/GoogleServices/GCSConfig.cs b/Duplicati/Library/Backend/GoogleServices/GCSConfig.cs index 4effd5c35..a1cc64f6d 100644 --- a/Duplicati/Library/Backend/GoogleServices/GCSConfig.cs +++ b/Duplicati/Library/Backend/GoogleServices/GCSConfig.cs @@ -44,20 +44,9 @@ namespace Duplicati.Library.Backend.GoogleServices public IDictionary Execute(IDictionary options) { - options.TryGetValue(KEY_CONFIGTYPE, out var k); - if (string.IsNullOrWhiteSpace(k)) - k = DEFAULT_CONFIG_TYPE_STR; - - if (!Enum.TryParse(k, true, out var ct)) - ct = DEFAULT_CONFIG_TYPE; - - switch (ct) - { - case ConfigType.StorageClasses: - return WebApi.GoogleCloudStorage.KNOWN_GCS_STORAGE_CLASSES.ToDictionary((x) => x.Key, (y) => y.Value); - default: - return WebApi.GoogleCloudStorage.KNOWN_GCS_LOCATIONS.ToDictionary((x) => x.Key, (y) => y.Value); - } + var ct = Utility.Utility.ParseEnumOption(options.AsReadOnly(), KEY_CONFIGTYPE, DEFAULT_CONFIG_TYPE); + GetLookups().TryGetValue(ct.ToString(), out var dict); + return dict ?? new Dictionary(); } public string Key => "gcs-getconfig"; @@ -72,6 +61,13 @@ namespace Duplicati.Library.Backend.GoogleServices new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, Strings.GCSConfig.ConfigTypeShort, Strings.GCSConfig.ConfigTypeLong, DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType))) ]; + public IDictionary> GetLookups() + => new Dictionary>() + { + { ConfigType.Locations.ToString(), WebApi.GoogleCloudStorage.KNOWN_GCS_LOCATIONS.ToDictionary((x) => x.Key, (y) => y.Value) }, + { ConfigType.StorageClasses.ToString(), WebApi.GoogleCloudStorage.KNOWN_GCS_STORAGE_CLASSES.ToDictionary((x) => x.Key, (y) => y.Value) }, + }; + #endregion } } diff --git a/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs b/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs index 134058b10..ff2f9967b 100644 --- a/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs +++ b/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs @@ -71,7 +71,7 @@ public class OpenStackStorage : IStreamingBackend private readonly OpenStackWebHelper m_helper; private OpenStackAuthResponse.TokenClass? m_accessToken; - public static readonly KeyValuePair[] KnownOpenstackProviders = + public static readonly KeyValuePair[] KnownOpenstackProviders = [ new("Rackspace US", "https://identity.api.rackspacecloud.com/v2.0"), new("Rackspace UK", "https://lon.identity.api.rackspacecloud.com/v2.0"), @@ -88,7 +88,7 @@ public class OpenStackStorage : IStreamingBackend new("Catalyst Cloud - nz-por-1 (NZ)", "https://api.nz-por-1.catalystcloud.io:5000/v3") ]; - public static readonly KeyValuePair[] OpenstackVersions = + public static readonly KeyValuePair[] OpenstackVersions = [ new("v2.0", "v2"), new("v3", "v3") diff --git a/Duplicati/Library/Backend/OpenStack/SwiftConfig.cs b/Duplicati/Library/Backend/OpenStack/SwiftConfig.cs index 8429badb5..25dbd36da 100644 --- a/Duplicati/Library/Backend/OpenStack/SwiftConfig.cs +++ b/Duplicati/Library/Backend/OpenStack/SwiftConfig.cs @@ -38,22 +38,11 @@ namespace Duplicati.Library.Backend.OpenStack { } - public IDictionary Execute(IDictionary options) + public IDictionary Execute(IDictionary options) { - options.TryGetValue(KEY_CONFIGTYPE, out var k); - if (string.IsNullOrWhiteSpace(k)) - k = DEFAULT_CONFIG_TYPE_STR; - - if (!Enum.TryParse(k, true, out var ct)) - ct = DEFAULT_CONFIG_TYPE; - - switch (ct) - { - case ConfigType.Versions: - return OpenStackStorage.OpenstackVersions.ToDictionary((x) => x.Key, (y) => y.Value); - default: - return OpenStackStorage.KnownOpenstackProviders.ToDictionary((x) => x.Key, (y) => y.Value); - } + var ct = Utility.Utility.ParseEnumOption(options.AsReadOnly(), KEY_CONFIGTYPE, DEFAULT_CONFIG_TYPE); + GetLookups().TryGetValue(ct.ToString(), out var dict); + return dict ?? new Dictionary(); } public string Key => "openstack-getconfig"; @@ -65,5 +54,13 @@ namespace Duplicati.Library.Backend.OpenStack public IList SupportedCommands => [ new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, Strings.SwiftConfig.ConfigTypeOptionShort, Strings.SwiftConfig.ConfigTypeOptionLong, DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType))) ]; + + public IDictionary> GetLookups() + => new Dictionary>() + { + { ConfigType.Providers.ToString(), OpenStackStorage.KnownOpenstackProviders.ToDictionary((x) => x.Key, (y) => y.Value) }, + { ConfigType.Versions.ToString(), OpenStackStorage.OpenstackVersions.ToDictionary((x) => x.Key, (y) => y.Value) }, + }; + } } diff --git a/Duplicati/Library/Backend/S3/S3Backend.cs b/Duplicati/Library/Backend/S3/S3Backend.cs index 09505a5ec..5d4dc9942 100644 --- a/Duplicati/Library/Backend/S3/S3Backend.cs +++ b/Duplicati/Library/Backend/S3/S3Backend.cs @@ -45,7 +45,7 @@ namespace Duplicati.Library.Backend private const string S3_LIST_API_VERSION_OPTION = "s3-list-api-version"; private const string S3_RECURSIVE_LIST = "s3-recursive-list"; - public static readonly Dictionary KNOWN_S3_PROVIDERS = new Dictionary(StringComparer.OrdinalIgnoreCase) { + public static readonly Dictionary KNOWN_S3_PROVIDERS = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "Amazon S3", "s3.amazonaws.com" }, { "MyCloudyPlace (EU)", "s3.mycloudyplace.com" }, { "Impossible Cloud (US)", "us-west-1.storage.impossibleapi.net" }, @@ -95,7 +95,7 @@ namespace Duplicati.Library.Backend }; //Updated list: http://docs.amazonwebservices.com/general/latest/gr/rande.html#s3_region - public static readonly Dictionary KNOWN_S3_LOCATIONS = new Dictionary(StringComparer.OrdinalIgnoreCase) { + public static readonly Dictionary KNOWN_S3_LOCATIONS = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "(default)", "" }, { "US East (Ohio)", "us-east-2" }, { "US East (N. Virginia)", "us-east-1" }, @@ -137,13 +137,13 @@ namespace Duplicati.Library.Backend { "EU", "eu-west-1" } }; - public static readonly Dictionary DEFAULT_S3_LOCATION_BASED_HOSTS; + public static readonly Dictionary DEFAULT_S3_LOCATION_BASED_HOSTS; - public static readonly Dictionary KNOWN_S3_STORAGE_CLASSES; + public static readonly Dictionary KNOWN_S3_STORAGE_CLASSES; static S3() { - var ns = new Dictionary(StringComparer.OrdinalIgnoreCase) { + var ns = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "(default)", "" }, { "Standard", "STANDARD" }, { "Infrequent Access (IA)", "STANDARD_IA" }, @@ -167,8 +167,8 @@ namespace Duplicati.Library.Backend DEFAULT_S3_LOCATION_BASED_HOSTS = KNOWN_S3_LOCATIONS .Where(x => !string.IsNullOrWhiteSpace(x.Value)) - .Select(x => new KeyValuePair(x.Value, $"s3.{x.Value}.amazonaws.com")) - .Append(new KeyValuePair("EU", "s3.eu-west-1.amazonaws.com")) + .Select(x => new KeyValuePair(x.Value!, $"s3.{x.Value}.amazonaws.com")) + .Append(new KeyValuePair("EU", "s3.eu-west-1.amazonaws.com")) .DistinctBy(x => x.Key, StringComparer.OrdinalIgnoreCase) .ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase); } @@ -233,7 +233,8 @@ namespace Duplicati.Library.Backend if (!string.IsNullOrEmpty(locationConstraint)) { if (DEFAULT_S3_LOCATION_BASED_HOSTS.TryGetValue(locationConstraint, out var s3hostmatch)) - hostname = s3hostmatch; + if (!string.IsNullOrEmpty(s3hostmatch)) + hostname = s3hostmatch; } } diff --git a/Duplicati/Library/Backend/S3/S3Config.cs b/Duplicati/Library/Backend/S3/S3Config.cs index 03777b5df..6fe9a6ac3 100644 --- a/Duplicati/Library/Backend/S3/S3Config.cs +++ b/Duplicati/Library/Backend/S3/S3Config.cs @@ -43,26 +43,11 @@ namespace Duplicati.Library.Backend #region IWebModule implementation - public IDictionary Execute(IDictionary options) + public IDictionary Execute(IDictionary options) { - options.TryGetValue(KEY_CONFIGTYPE, out var k); - if (string.IsNullOrWhiteSpace(k)) - k = DEFAULT_CONFIG_TYPE_STR; - - if (!Enum.TryParse(k, true, out var ct)) - ct = DEFAULT_CONFIG_TYPE; - - switch (ct) - { - case ConfigType.RegionHosts: - return S3.DEFAULT_S3_LOCATION_BASED_HOSTS; - case ConfigType.Regions: - return S3.KNOWN_S3_LOCATIONS; - case ConfigType.StorageClasses: - return S3.KNOWN_S3_STORAGE_CLASSES; - default: - return S3.KNOWN_S3_PROVIDERS; - } + var ct = Utility.Utility.ParseEnumOption(options.AsReadOnly(), KEY_CONFIGTYPE, DEFAULT_CONFIG_TYPE); + GetLookups().TryGetValue(ct.ToString(), out var dict); + return dict ?? new Dictionary(); } public string Key => "s3-getconfig"; @@ -76,6 +61,15 @@ namespace Duplicati.Library.Backend new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, Strings.S3Config.ConfigTypeShort, Strings.S3Config.ConfigTypeLong, DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType))) ]; + public IDictionary> GetLookups() + => new Dictionary>() + { + { ConfigType.Providers.ToString(), S3.KNOWN_S3_PROVIDERS.ToDictionary((x) => x.Key, (y) => y.Value) }, + { ConfigType.Regions.ToString(), S3.KNOWN_S3_LOCATIONS.ToDictionary((x) => x.Key, (y) => y.Value) }, + { ConfigType.RegionHosts.ToString(), S3.DEFAULT_S3_LOCATION_BASED_HOSTS.ToDictionary((x) => x.Key, (y) => y.Value) }, + { ConfigType.StorageClasses.ToString(), S3.KNOWN_S3_STORAGE_CLASSES.ToDictionary((x) => x.Key, (y) => y.Value) }, + }; + #endregion } } diff --git a/Duplicati/Library/Backend/S3/S3IAM.cs b/Duplicati/Library/Backend/S3/S3IAM.cs index 5c6b6d09e..ba3b7f472 100644 --- a/Duplicati/Library/Backend/S3/S3IAM.cs +++ b/Duplicati/Library/Backend/S3/S3IAM.cs @@ -107,6 +107,9 @@ namespace Duplicati.Library.Backend } } + public IDictionary> GetLookups() + => new Dictionary>(); + private static void ValidateArgument(string? arg, string type) { if (string.IsNullOrWhiteSpace(arg)) diff --git a/Duplicati/Library/Backend/SSHv2/KeyGenerator.cs b/Duplicati/Library/Backend/SSHv2/KeyGenerator.cs index 05ac23ca4..fab3dec3c 100644 --- a/Duplicati/Library/Backend/SSHv2/KeyGenerator.cs +++ b/Duplicati/Library/Backend/SSHv2/KeyGenerator.cs @@ -211,6 +211,10 @@ namespace Duplicati.Library.Backend new CommandLineArgument(KEY_TYPE_NAME, CommandLineArgument.ArgumentType.Enumeration, Strings.KeyGenerator.KeyTypeShort, Strings.KeyGenerator.KeyTypeLong, DEFAULT_KEYTYPE, null, new string[] {KEYTYPE_DSA, KEYTYPE_RSA}), new CommandLineArgument(KEY_USERNAME, CommandLineArgument.ArgumentType.Integer, Strings.KeyGenerator.KeyUsernameShort, Strings.KeyGenerator.KeyUsernameLong, DEFAULT_USERNAME), ]; + + public IDictionary> GetLookups() + => new Dictionary>(); + #endregion } } diff --git a/Duplicati/Library/Backend/SSHv2/KeyUploader.cs b/Duplicati/Library/Backend/SSHv2/KeyUploader.cs index 24a00af5b..930a5b4a1 100644 --- a/Duplicati/Library/Backend/SSHv2/KeyUploader.cs +++ b/Duplicati/Library/Backend/SSHv2/KeyUploader.cs @@ -147,6 +147,9 @@ namespace Duplicati.Library.Backend new CommandLineArgument(OPTION_KEY, CommandLineArgument.ArgumentType.String, Strings.KeyUploader.PubkeyShort, Strings.KeyUploader.PubkeyLong), }.Union(new SSHv2().SupportedCommands).ToList(); + public IDictionary> GetLookups() + => new Dictionary>(); + #endregion } } diff --git a/Duplicati/Library/Backend/Storj/StorjBackend.cs b/Duplicati/Library/Backend/Storj/StorjBackend.cs index e08ae37f0..8921f9a2c 100644 --- a/Duplicati/Library/Backend/Storj/StorjBackend.cs +++ b/Duplicati/Library/Backend/Storj/StorjBackend.cs @@ -59,13 +59,13 @@ namespace Duplicati.Library.Backend.Storj private readonly IObjectService _objectService; private readonly TimeoutOptionsHelper.Timeouts _timeouts; - public static readonly Dictionary KNOWN_STORJ_SATELLITES = new Dictionary(StringComparer.OrdinalIgnoreCase){ + public static readonly Dictionary KNOWN_STORJ_SATELLITES = new Dictionary(StringComparer.OrdinalIgnoreCase){ { "US Central", "us1.storj.io:7777" }, { "Asia East", "ap1.storj.io:7777" }, { "Europe", "eu1.storj.io:7777" }, }; - public static readonly Dictionary KNOWN_AUTHENTICATION_METHODS = new Dictionary(StringComparer.OrdinalIgnoreCase){ + public static readonly Dictionary KNOWN_AUTHENTICATION_METHODS = new Dictionary(StringComparer.OrdinalIgnoreCase){ { "API key", "API key" }, { "Access grant", "Access grant" }, }; diff --git a/Duplicati/Library/Backend/Storj/StorjConfig.cs b/Duplicati/Library/Backend/Storj/StorjConfig.cs index 99a3da856..9ba7f762f 100644 --- a/Duplicati/Library/Backend/Storj/StorjConfig.cs +++ b/Duplicati/Library/Backend/Storj/StorjConfig.cs @@ -48,22 +48,20 @@ 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 IDictionary Execute(IDictionary options) { - options.TryGetValue(KEY_CONFIGTYPE, out var k); - if (string.IsNullOrWhiteSpace(k)) - k = DEFAULT_CONFIG_TYPE_STR; - - if (!Enum.TryParse(k, true, out var ct)) - ct = DEFAULT_CONFIG_TYPE; - - return ct switch - { - ConfigType.Satellites => Storj.KNOWN_STORJ_SATELLITES, - ConfigType.AuthenticationMethods => Storj.KNOWN_AUTHENTICATION_METHODS, - _ => Storj.KNOWN_STORJ_SATELLITES, - }; + var ct = Utility.Utility.ParseEnumOption(options.AsReadOnly(), KEY_CONFIGTYPE, DEFAULT_CONFIG_TYPE); + GetLookups().TryGetValue(ct.ToString(), out var dict); + return dict ?? new Dictionary(); } + + public IDictionary> GetLookups() + => new Dictionary>() + { + { ConfigType.Satellites.ToString(), Storj.KNOWN_STORJ_SATELLITES.ToDictionary((x) => x.Key, (y) => y.Value) }, + { ConfigType.AuthenticationMethods.ToString(), Storj.KNOWN_AUTHENTICATION_METHODS.ToDictionary((x) => x.Key, (y) => y.Value) }, + }; + #endregion } } diff --git a/Duplicati/Library/Interface/IWebModule.cs b/Duplicati/Library/Interface/IWebModule.cs index a0a316fc2..42c957f98 100644 --- a/Duplicati/Library/Interface/IWebModule.cs +++ b/Duplicati/Library/Interface/IWebModule.cs @@ -47,6 +47,12 @@ namespace Duplicati.Library.Interface /// A list of output values IDictionary Execute(IDictionary options); + /// + /// Returns lookup data from the module, if any. + /// + /// A dictionary of lookup dictionaries + IDictionary> GetLookups(); + } } diff --git a/Duplicati/Library/RestAPI/Serializable/ServerSettings.cs b/Duplicati/Library/RestAPI/Serializable/ServerSettings.cs index 6174fab70..bba9005f5 100644 --- a/Duplicati/Library/RestAPI/Serializable/ServerSettings.cs +++ b/Duplicati/Library/RestAPI/Serializable/ServerSettings.cs @@ -19,6 +19,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System.Collections.Generic; using System.Linq; using Duplicati.Server.Serialization.Interface; @@ -87,6 +88,7 @@ namespace Duplicati.Server.Serializable this.Description = module.Description; this.DisplayName = module.DisplayName; this.Options = module.SupportedCommands?.ToArray() ?? []; + this.Lookups = module.GetLookups(); } /// @@ -116,6 +118,10 @@ namespace Duplicati.Server.Serializable /// The options supported by the module /// public Library.Interface.ICommandLineArgument[] Options { get; private set; } + /// + /// The lookups supported by the module + /// + public IDictionary> Lookups { get; private set; } } /// diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IDynamicModule.cs b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IDynamicModule.cs index 72fd28601..fa41e30b6 100644 --- a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IDynamicModule.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IDynamicModule.cs @@ -1,24 +1,26 @@ -// Copyright (C) 2025, 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) 2025, 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.Collections.Generic; + namespace Duplicati.Server.Serialization.Interface { /// @@ -42,7 +44,11 @@ namespace Duplicati.Server.Serialization.Interface /// /// The options supported by the module /// - Duplicati.Library.Interface.ICommandLineArgument[] Options { get; } + Library.Interface.ICommandLineArgument[] Options { get; } + /// + /// The lookups supported by the module + /// + IDictionary> Lookups { get; } } }