From fa6dd0fd941706b36a4fccec5edbcad700afcfd0 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Thu, 5 Feb 2026 10:43:16 +0100 Subject: [PATCH] Fixed option to provide a connection string id for test methods --- .../Dto/V2/DestinationTestRequestDto.cs | 4 +++ .../Endpoints/Shared/SharedRemoteOperation.cs | 28 ++++++++++++------- .../Endpoints/V1/RemoteOperation.cs | 18 ++++++------ .../WebserverCore/Endpoints/V1/WebModules.cs | 2 +- .../Endpoints/V2/DestinationVerify.cs | 6 ++-- 5 files changed, 35 insertions(+), 23 deletions(-) diff --git a/Duplicati/WebserverCore/Dto/V2/DestinationTestRequestDto.cs b/Duplicati/WebserverCore/Dto/V2/DestinationTestRequestDto.cs index 359e6213c..61075c60b 100644 --- a/Duplicati/WebserverCore/Dto/V2/DestinationTestRequestDto.cs +++ b/Duplicati/WebserverCore/Dto/V2/DestinationTestRequestDto.cs @@ -53,6 +53,10 @@ public sealed record DestinationTestRequestDto /// The backup ID, if known /// public string? BackupId { get; init; } + /// + /// The connection string ID, if known + /// + public long? ConnectionStringId { get; init; } /// /// Any additional options to pass to the destination diff --git a/Duplicati/WebserverCore/Endpoints/Shared/SharedRemoteOperation.cs b/Duplicati/WebserverCore/Endpoints/Shared/SharedRemoteOperation.cs index 1e4da6306..be5baf572 100644 --- a/Duplicati/WebserverCore/Endpoints/Shared/SharedRemoteOperation.cs +++ b/Duplicati/WebserverCore/Endpoints/Shared/SharedRemoteOperation.cs @@ -67,9 +67,9 @@ public class SharedRemoteOperation return modules; } - public static async Task<(string Url, Dictionary Options)> ExpandUrl(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, CancellationToken cancelToken) + public static async Task<(string Url, Dictionary Options)> ExpandUrl(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, long connectionStringId, CancellationToken cancelToken) { - url = UnmaskUrl(connection, url, backupId); + url = UnmaskUrl(connection, url, backupId, connectionStringId); var uri = new Library.Utility.Uri(url); var opts = ParseUrlOptions(connection, uri); @@ -88,26 +88,26 @@ public class SharedRemoteOperation return (url, opts); } - public static async Task GetBackend(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, CancellationToken cancelToken) + public static async Task GetBackend(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, long connectionStringId, CancellationToken cancelToken) { - (url, var opts) = await ExpandUrl(connection, applicationSettings, url, backupId, cancelToken); + (url, var opts) = await ExpandUrl(connection, applicationSettings, url, backupId, connectionStringId, cancelToken); var modules = ConfigureModules(opts); var backend = Library.DynamicLoader.BackendLoader.GetBackend(url, opts); return new BackendTupleDisposeWrapper(backend, modules); } - public static async Task GetSourceProviderForTesting(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, CancellationToken cancelToken) + public static async Task GetSourceProviderForTesting(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, long connectionStringId, CancellationToken cancelToken) { - (url, var opts) = await ExpandUrl(connection, applicationSettings, url, backupId, cancelToken); + (url, var opts) = await ExpandUrl(connection, applicationSettings, url, backupId, connectionStringId, cancelToken); var modules = ConfigureModules(opts); var sourceProvider = await Library.DynamicLoader.SourceProviderLoader.GetSourceProviderForTesting(url, "", opts, cancelToken); return new SourceProviderTupleDisposeWrapper(sourceProvider, modules); } - public static async Task GetRestoreDestinationProviderForTesting(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, CancellationToken cancelToken) + public static async Task GetRestoreDestinationProviderForTesting(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, long connectionStringId, CancellationToken cancelToken) { - (url, var opts) = await ExpandUrl(connection, applicationSettings, url, backupId, cancelToken); + (url, var opts) = await ExpandUrl(connection, applicationSettings, url, backupId, connectionStringId, cancelToken); var modules = ConfigureModules(opts); var restoreDestinationProvider = await Library.DynamicLoader.RestoreDestinationProviderLoader.GetRestoreDestinationProviderForTesting(url, opts, cancelToken); @@ -132,9 +132,17 @@ public class SharedRemoteOperation } } - private static string UnmaskUrl(Connection connection, string maskedurl, string? backupId) + private static string UnmaskUrl(Connection connection, string maskedurl, string? backupId, long connectionStringId) { - var previousUrl = !string.IsNullOrWhiteSpace(backupId) ? connection.GetBackup(backupId)?.TargetURL : null; + + string? previousUrl = null; + + if (!string.IsNullOrWhiteSpace(backupId)) + previousUrl = connection.GetBackup(backupId)?.TargetURL; + + if (string.IsNullOrWhiteSpace(previousUrl) && connectionStringId > 0) + previousUrl = connection.GetConnectionString(connectionStringId)?.BaseUrl; + var unmasked = string.IsNullOrWhiteSpace(previousUrl) ? maskedurl : QuerystringMasking.Unmask(maskedurl, previousUrl); diff --git a/Duplicati/WebserverCore/Endpoints/V1/RemoteOperation.cs b/Duplicati/WebserverCore/Endpoints/V1/RemoteOperation.cs index ed66e6eef..522d076e7 100644 --- a/Duplicati/WebserverCore/Endpoints/V1/RemoteOperation.cs +++ b/Duplicati/WebserverCore/Endpoints/V1/RemoteOperation.cs @@ -31,7 +31,7 @@ namespace Duplicati.WebserverCore.Endpoints.V1 { public class RemoteOperation : IEndpointV1 { - private record RemoteOperationInput(string path, string? backupId); + private record RemoteOperationInput(string path, string? backupId, long? connectionStringId); public static void Map(RouteGroupBuilder group) { @@ -40,11 +40,11 @@ namespace Duplicati.WebserverCore.Endpoints.V1 .RequireAuthorization(); group.MapPost("/remoteoperation/test", ([FromServices] Connection connection, [FromServices] IApplicationSettings applicationSettings, [FromQuery] bool? autocreate, [FromQuery] Dto.V2.RemoteDestinationType? type, [FromBody] RemoteOperationInput input, CancellationToken cancelToken) - => ExecuteTest(connection, applicationSettings, input.path, input.backupId, autocreate ?? false, type ?? Dto.V2.RemoteDestinationType.Backend, cancelToken)) + => ExecuteTest(connection, applicationSettings, input.path, input.backupId, input.connectionStringId, autocreate ?? false, type ?? Dto.V2.RemoteDestinationType.Backend, cancelToken)) .RequireAuthorization(); group.MapPost("/remoteoperation/create", ([FromServices] Connection connection, [FromServices] IApplicationSettings applicationSettings, [FromBody] RemoteOperationInput input, CancellationToken cancelToken) - => ExecuteCreate(connection, applicationSettings, input.path, input.backupId, cancelToken)) + => ExecuteCreate(connection, applicationSettings, input.path, input.backupId, input.connectionStringId, cancelToken)) .RequireAuthorization(); } @@ -54,25 +54,25 @@ namespace Duplicati.WebserverCore.Endpoints.V1 return new Dto.GetDbPathDto(!string.IsNullOrWhiteSpace(path), path); } - private static async Task ExecuteTest(Connection connection, IApplicationSettings applicationSettings, string maskedurl, string? backupId, bool autoCreate, Dto.V2.RemoteDestinationType type, CancellationToken cancelToken) + private static async Task ExecuteTest(Connection connection, IApplicationSettings applicationSettings, string maskedurl, string? backupId, long? connectionStringId, bool autoCreate, Dto.V2.RemoteDestinationType type, CancellationToken cancelToken) { try { if (type == Dto.V2.RemoteDestinationType.SourceProvider) { - using var wrapper = await SharedRemoteOperation.GetSourceProviderForTesting(connection, applicationSettings, maskedurl, backupId, cancelToken); + using var wrapper = await SharedRemoteOperation.GetSourceProviderForTesting(connection, applicationSettings, maskedurl, backupId, connectionStringId ?? -1, cancelToken); using (var s = wrapper.SourceProvider) await s.Test(cancelToken).ConfigureAwait(false); } else if (type == Dto.V2.RemoteDestinationType.RestoreDestinationProvider) { - using var wrapper = await SharedRemoteOperation.GetRestoreDestinationProviderForTesting(connection, applicationSettings, maskedurl, backupId, cancelToken); + using var wrapper = await SharedRemoteOperation.GetRestoreDestinationProviderForTesting(connection, applicationSettings, maskedurl, backupId, connectionStringId ?? -1, cancelToken); using (var r = wrapper.RestoreDestinationProvider) await r.Test(cancelToken).ConfigureAwait(false); } else { - using var wrapper = await SharedRemoteOperation.GetBackend(connection, applicationSettings, maskedurl, backupId, cancelToken); + using var wrapper = await SharedRemoteOperation.GetBackend(connection, applicationSettings, maskedurl, backupId, connectionStringId ?? -1, cancelToken); using (var b = wrapper.Backend) { try { await b.TestAsync(cancelToken).ConfigureAwait(false); } @@ -120,11 +120,11 @@ namespace Duplicati.WebserverCore.Endpoints.V1 } - private static async Task ExecuteCreate(Connection connection, IApplicationSettings applicationSettings, string maskedurl, string? backupId, CancellationToken cancelToken) + private static async Task ExecuteCreate(Connection connection, IApplicationSettings applicationSettings, string maskedurl, string? backupId, long? connectionStringId, CancellationToken cancelToken) { try { - using var wrapper = await SharedRemoteOperation.GetBackend(connection, applicationSettings, maskedurl, backupId, cancelToken); + using var wrapper = await SharedRemoteOperation.GetBackend(connection, applicationSettings, maskedurl, backupId, connectionStringId ?? -1, cancelToken); using (var b = wrapper.Backend) await b.CreateFolderAsync(cancelToken).ConfigureAwait(false); } diff --git a/Duplicati/WebserverCore/Endpoints/V1/WebModules.cs b/Duplicati/WebserverCore/Endpoints/V1/WebModules.cs index 4292f5903..6f1906782 100644 --- a/Duplicati/WebserverCore/Endpoints/V1/WebModules.cs +++ b/Duplicati/WebserverCore/Endpoints/V1/WebModules.cs @@ -55,7 +55,7 @@ public record WebModules : IEndpointV1 var url = options.GetValueOrDefault("url"); if (!string.IsNullOrWhiteSpace(url)) { - var (newUrl, opts) = await SharedRemoteOperation.ExpandUrl(connection, applicationSettings, url, options.GetValueOrDefault("backup-id"), cancellationToken); + var (newUrl, opts) = await SharedRemoteOperation.ExpandUrl(connection, applicationSettings, url, options.GetValueOrDefault("backup-id"), Library.Utility.Utility.ParseLongOption(options, "connection-string-id", -1), cancellationToken); options["url"] = newUrl; foreach (var k in opts.Keys) options[k] = opts[k]; diff --git a/Duplicati/WebserverCore/Endpoints/V2/DestinationVerify.cs b/Duplicati/WebserverCore/Endpoints/V2/DestinationVerify.cs index c01b64abc..216246f49 100644 --- a/Duplicati/WebserverCore/Endpoints/V2/DestinationVerify.cs +++ b/Duplicati/WebserverCore/Endpoints/V2/DestinationVerify.cs @@ -47,7 +47,7 @@ public class DestinationVerify : IEndpointV2 { if (destinationType == RemoteDestinationType.SourceProvider) { - using var wrapper = await SharedRemoteOperation.GetSourceProviderForTesting(connection, applicationSettings, input.DestinationUrl, input.BackupId, cancelToken); + using var wrapper = await SharedRemoteOperation.GetSourceProviderForTesting(connection, applicationSettings, input.DestinationUrl, input.BackupId, input.ConnectionStringId ?? -1, cancelToken); await wrapper.SourceProvider.Test(cancelToken); // 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); @@ -60,7 +60,7 @@ public class DestinationVerify : IEndpointV2 } else if (destinationType == RemoteDestinationType.RestoreDestinationProvider) { - using var wrapper = await SharedRemoteOperation.GetRestoreDestinationProviderForTesting(connection, applicationSettings, input.DestinationUrl, input.BackupId, cancelToken); + using var wrapper = await SharedRemoteOperation.GetRestoreDestinationProviderForTesting(connection, applicationSettings, input.DestinationUrl, input.BackupId, input.ConnectionStringId ?? -1, cancelToken); await wrapper.RestoreDestinationProvider.Test(cancelToken); return DestinationTestResponseDto.Create( @@ -71,7 +71,7 @@ public class DestinationVerify : IEndpointV2 } else { - using var wrapper = await SharedRemoteOperation.GetBackend(connection, applicationSettings, input.DestinationUrl, input.BackupId, cancelToken); + using var wrapper = await SharedRemoteOperation.GetBackend(connection, applicationSettings, input.DestinationUrl, input.BackupId, input.ConnectionStringId ?? -1, cancelToken); using (var b = wrapper.Backend) {