2025-11-27 15:39:59 +01:00
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 ;
2026-01-25 11:56:28 +01:00
public record BackendTupleDisposeWrapper ( IBackend Backend , IEnumerable < IGenericModule > Modules ) : IDisposable
2025-11-27 15:39:59 +01:00
{
public void Dispose ()
{
2026-01-25 11:56:28 +01:00
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 ();
2025-11-27 15:39:59 +01:00
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 ;
}
2026-02-06 14:04:51 +01:00
/// <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>
2026-02-17 10:29:53 +01:00
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 )
2025-11-27 15:39:59 +01:00
{
2026-02-17 10:29:53 +01:00
url = UnmaskUrl ( connection , url , backupId , connectionStringId , sourcePrefix );
2025-11-27 15:39:59 +01:00
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 ();
2025-12-09 21:17:01 +01:00
if ( uri . Scheme . Equals ( Library . Backend . Duplicati . DuplicatiBackend . PROTOCOL , StringComparison . OrdinalIgnoreCase ))
url = Library . Backend . Duplicati . DuplicatiBackend . MergeArgsIntoUrl (
2025-11-27 15:39:59 +01:00
url ,
connection . ApplicationSettings . RemoteControlStorageApiId ,
connection . ApplicationSettings . RemoteControlStorageApiKey ,
connection . ApplicationSettings . RemoteControlStorageEndpointUrl
);
2025-12-09 21:17:01 +01:00
return ( url , opts );
}
2026-02-05 10:43:16 +01:00
public static async Task < BackendTupleDisposeWrapper > GetBackend ( Connection connection , IApplicationSettings applicationSettings , string url , string? backupId , long connectionStringId , CancellationToken cancelToken )
2025-12-09 21:17:01 +01:00
{
2026-02-17 10:29:53 +01:00
( url , var opts ) = await ExpandUrl ( connection , applicationSettings , url , backupId , connectionStringId , null , cancelToken );
2025-11-27 15:39:59 +01:00
var modules = ConfigureModules ( opts );
var backend = Library . DynamicLoader . BackendLoader . GetBackend ( url , opts );
2026-01-25 11:56:28 +01:00
return new BackendTupleDisposeWrapper ( backend , modules );
}
2026-02-17 10:29:53 +01:00
public static async Task < SourceProviderTupleDisposeWrapper > GetSourceProviderForTesting ( Connection connection , IApplicationSettings applicationSettings , string url , string? backupId , long connectionStringId , string? sourcePrefix , CancellationToken cancelToken )
2026-01-25 11:56:28 +01:00
{
2026-02-17 10:29:53 +01:00
( url , var opts ) = await ExpandUrl ( connection , applicationSettings , url , backupId , connectionStringId , sourcePrefix , cancelToken );
2026-01-25 11:56:28 +01:00
var modules = ConfigureModules ( opts );
var sourceProvider = await Library . DynamicLoader . SourceProviderLoader . GetSourceProviderForTesting ( url , "" , opts , cancelToken );
return new SourceProviderTupleDisposeWrapper ( sourceProvider , modules );
}
2026-02-17 10:29:53 +01:00
public static async Task < RestoreDestinationProviderTupleDisposeWrapper > GetRestoreDestinationProviderForTesting ( Connection connection , IApplicationSettings applicationSettings , string url , string? backupId , long connectionStringId , string? sourcePrefix , CancellationToken cancelToken )
2026-01-25 11:56:28 +01:00
{
2026-02-17 10:29:53 +01:00
( url , var opts ) = await ExpandUrl ( connection , applicationSettings , url , backupId , connectionStringId , sourcePrefix , cancelToken );
2026-01-25 11:56:28 +01:00
var modules = ConfigureModules ( opts );
var restoreDestinationProvider = await Library . DynamicLoader . RestoreDestinationProviderLoader . GetRestoreDestinationProviderForTesting ( url , opts , cancelToken );
return new RestoreDestinationProviderTupleDisposeWrapper ( restoreDestinationProvider , modules );
2025-11-27 15:39:59 +01:00
}
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 ;
}
}
2026-02-06 14:04:51 +01:00
/// <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>
2026-02-17 10:29:53 +01:00
private static string UnmaskUrl ( Connection connection , string maskedurl , string? backupId , long connectionStringId , string? prefix )
2025-11-27 15:39:59 +01:00
{
2026-02-06 14:04:51 +01:00
var backup = ! string . IsNullOrWhiteSpace ( backupId ) ? connection . GetBackup ( backupId ) : null ;
var previousUrl = backup ?. TargetURL ;
2026-02-05 10:43:16 +01:00
if ( string . IsNullOrWhiteSpace ( previousUrl ) && connectionStringId > 0 )
previousUrl = connection . GetConnectionString ( connectionStringId )?. BaseUrl ;
2026-02-06 14:04:51 +01:00
if (! string . IsNullOrWhiteSpace ( prefix ) && backup ?. Sources != null )
2026-02-16 22:05:44 +01:00
{
previousUrl = backup . Sources ?. FirstOrDefault ( s => s . StartsWith ( prefix + '|' , StringComparison . Ordinal ));
if (! string . IsNullOrWhiteSpace ( previousUrl ))
previousUrl = previousUrl . Split ( '|' , 2 ). LastOrDefault ();
}
2026-02-06 14:04:51 +01:00
2025-11-27 15:39:59 +01:00
var unmasked = string . IsNullOrWhiteSpace ( previousUrl )
? maskedurl
: QuerystringMasking . Unmask ( maskedurl , previousUrl );
if ( Connection . UrlContainsPasswordPlaceholder ( unmasked ))
throw new ArgumentException ( "Unmasked URL contains password placeholder" );
return unmasked ;
}
}