Files
duplicati/Duplicati/WebserverCore/Endpoints/Shared/SharedRemoteOperation.cs
T
2026-02-20 08:56:40 +01:00

178 lines
7.8 KiB
C#

using Duplicati.Library.Interface;
using Duplicati.Library.Main;
using Duplicati.Library.Utility;
using Duplicati.Server;
using Duplicati.Server.Database;
using Duplicati.WebserverCore.Abstractions;
namespace Duplicati.WebserverCore.Endpoints.Shared;
public record BackendTupleDisposeWrapper(IBackend Backend, IEnumerable<IGenericModule> Modules) : IDisposable
{
public void Dispose()
{
Backend?.Dispose();
foreach (var n in Modules)
if (n is IDisposable disposable)
disposable.Dispose();
}
}
public record SourceProviderTupleDisposeWrapper(ISourceProvider SourceProvider, IEnumerable<IGenericModule> Modules) : IDisposable
{
public void Dispose()
{
SourceProvider?.Dispose();
foreach (var n in Modules)
if (n is IDisposable disposable)
disposable.Dispose();
}
}
public record RestoreDestinationProviderTupleDisposeWrapper(IRestoreDestinationProvider RestoreDestinationProvider, IEnumerable<IGenericModule> Modules) : IDisposable
{
public void Dispose()
{
RestoreDestinationProvider?.Dispose();
foreach (var n in Modules)
if (n is IDisposable disposable)
disposable.Dispose();
}
}
public class SharedRemoteOperation
{
public static Dictionary<string, string?> ParseUrlOptions(Connection connection, Library.Utility.Uri uri)
{
var qp = uri.QueryParameters;
var opts = Runner.GetCommonOptions(connection);
foreach (var k in qp.Keys.Cast<string>())
opts[k] = qp[k];
return opts;
}
public static IEnumerable<IGenericModule> ConfigureModules(IDictionary<string, string?> opts)
{
var modules = Library.DynamicLoader.GenericLoader.Modules.OfType<IConnectionModule>()
.Select(x => Library.DynamicLoader.GenericLoader.GetModule(x.Key))
.WhereNotNull()
.ToArray();
foreach (var n in modules)
n.Configure(opts);
return modules;
}
/// <summary>
/// This method will unmask the URL if needed, apply secrets from the secret provider, and merge in remote control storage API credentials if it's a Duplicati backend URL.
/// It returns the expanded URL and the options used for the expansion, which may be needed for later operations to ensure consistency.
/// </summary>
/// <param name="connection">The database connection</param>
/// <param name="applicationSettings">The application settings</param>
/// <param name="url">The URL to expand</param>
/// <param name="backupId">The backup ID</param>
/// <param name="sourcePrefix">The prefix, if this is a remote source</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>The unmasked URL and the options after the expansion</returns>
public static async Task<(string Url, Dictionary<string, string?> Options)> ExpandUrl(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, long connectionStringId, string? sourcePrefix, CancellationToken cancelToken)
{
url = UnmaskUrl(connection, url, backupId, connectionStringId, sourcePrefix);
var uri = new Library.Utility.Uri(url);
var opts = ParseUrlOptions(connection, uri);
var tmp = new[] { uri };
await SecretProviderHelper.ApplySecretProviderAsync([], tmp, opts, Library.Utility.TempFolder.SystemTempPath, applicationSettings.SecretProvider, cancelToken);
url = tmp[0].ToString();
if (uri.Scheme.Equals(Library.Backend.Duplicati.DuplicatiBackend.PROTOCOL, StringComparison.OrdinalIgnoreCase))
url = Library.Backend.Duplicati.DuplicatiBackend.MergeArgsIntoUrl(
url,
connection.ApplicationSettings.RemoteControlStorageApiId,
connection.ApplicationSettings.RemoteControlStorageApiKey,
connection.ApplicationSettings.RemoteControlStorageEndpointUrl
);
return (url, opts);
}
public static async Task<BackendTupleDisposeWrapper> GetBackend(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, long connectionStringId, CancellationToken cancelToken)
{
(url, var opts) = await ExpandUrl(connection, applicationSettings, url, backupId, connectionStringId, null, cancelToken);
var modules = ConfigureModules(opts);
var backend = Library.DynamicLoader.BackendLoader.GetBackend(url, opts);
return new BackendTupleDisposeWrapper(backend, modules);
}
public static async Task<SourceProviderTupleDisposeWrapper> GetSourceProviderForTesting(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, long connectionStringId, string? sourcePrefix, CancellationToken cancelToken)
{
(url, var opts) = await ExpandUrl(connection, applicationSettings, url, backupId, connectionStringId, sourcePrefix, cancelToken);
var modules = ConfigureModules(opts);
var sourceProvider = await Library.DynamicLoader.SourceProviderLoader.GetSourceProviderForTesting(url, "", opts, cancelToken);
return new SourceProviderTupleDisposeWrapper(sourceProvider, modules);
}
public static async Task<RestoreDestinationProviderTupleDisposeWrapper> GetRestoreDestinationProviderForTesting(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, long connectionStringId, string? sourcePrefix, CancellationToken cancelToken)
{
(url, var opts) = await ExpandUrl(connection, applicationSettings, url, backupId, connectionStringId, sourcePrefix, cancelToken);
var modules = ConfigureModules(opts);
var restoreDestinationProvider = await Library.DynamicLoader.RestoreDestinationProviderLoader.GetRestoreDestinationProviderForTesting(url, opts, cancelToken);
return new RestoreDestinationProviderTupleDisposeWrapper(restoreDestinationProvider, modules);
}
public static Exception GetInnerException<T>(Exception ex) where T : Exception
{
var original = ex;
while (true)
{
if (ex == null)
return original;
if (ex is T)
return (T)ex;
if (ex.InnerException == null)
throw new ArgumentNullException(nameof(ex.InnerException));
ex = ex.InnerException;
}
}
/// <summary>
/// Unmask the URL if it is masked
/// </summary>
/// <param name="connection">The database connection</param>
/// <param name="maskedurl">The masked URL</param>
/// <param name="backupId">The backup ID</param>
/// <param name="prefix">The prefix, if this is a remote source</param>
/// <returns>The unmasked URL</returns>
private static string UnmaskUrl(Connection connection, string maskedurl, string? backupId, long connectionStringId, string? prefix)
{
var backup = !string.IsNullOrWhiteSpace(backupId) ? connection.GetBackup(backupId) : null;
var previousUrl = backup?.TargetURL;
if (string.IsNullOrWhiteSpace(previousUrl) && connectionStringId > 0)
previousUrl = connection.GetConnectionString(connectionStringId)?.BaseUrl;
if (!string.IsNullOrWhiteSpace(prefix) && backup?.Sources != null)
{
previousUrl = backup.Sources?.FirstOrDefault(s => s.StartsWith(prefix + '|', StringComparison.Ordinal));
if (!string.IsNullOrWhiteSpace(previousUrl))
previousUrl = previousUrl.Split('|', 2).LastOrDefault();
}
var unmasked = string.IsNullOrWhiteSpace(previousUrl)
? maskedurl
: QuerystringMasking.Unmask(maskedurl, previousUrl);
if (Connection.UrlContainsPasswordPlaceholder(unmasked))
throw new ArgumentException("Unmasked URL contains password placeholder");
return unmasked;
}
}