// Copyright (C) 2026, 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;
using System.Collections.Generic;
using Duplicati.Library.Interface;
using System.Linq;
using System.Runtime.ExceptionServices;
using Duplicati.Library.SourceProviders;
using Duplicati.Library.SourceProvider;
using System.Threading.Tasks;
using System.Threading;
namespace Duplicati.Library.DynamicLoader
{
///
/// Loads all SourceProviders dynamically and exposes a list of those
///
public class RestoreDestinationProviderLoader
{
///
/// Implementation overrides specific to SourceProviders
///
private class RestoreDestinationProviderLoaderSub : DynamicLoader
{
///
/// Returns the protocol key
///
/// The item to load the key for
/// The protocol key
protected override string GetInterfaceKey(IRestoreDestinationProviderModule item)
{
return item.Key;
}
///
/// Returns the subfolders searched for SourceProviders
///
protected override string[] Subfolders => ["SourceProviders"];
///
/// The built-in modules
///
protected override IEnumerable BuiltInModules => RestoreDestinationProviderModules.BuiltInRestoreDestinationProviderModules;
///
/// Instanciates a specific SourceProvider, given the url and options
///
/// The url to create the instance for
/// The options to pass to the instance constructor
/// The instanciated SourceProvider or null if the url is not supported
public IRestoreDestinationProvider GetRestoreDestinationProvider(string url, Dictionary options)
{
var uri = new Utility.Uri(url);
LoadInterfaces();
var newOpts = new Dictionary(options);
foreach (var key in uri.QueryParameters.AllKeys)
newOpts[key] = uri.QueryParameters[key];
lock (m_lock)
{
try
{
if (m_interfaces.ContainsKey(uri.Scheme))
return (IRestoreDestinationProvider)Activator.CreateInstance(m_interfaces[uri.Scheme].GetType(), url, newOpts);
}
catch (System.Reflection.TargetInvocationException tex)
{
if (tex.InnerException != null)
{
// Unwrap exceptions for nicer display. The ExceptionDispatchInfo class allows us to
// rethrow an exception without changing the stack trace.
ExceptionDispatchInfo.Capture(tex.InnerException).Throw();
}
throw;
}
return null;
}
}
///
/// Gets the supported commands for a certain url
///
/// The url to find commands for
/// The supported commands or null if the url scheme was not supported
public IReadOnlyList GetSupportedCommands(string url)
{
var uri = new Utility.Uri(url);
LoadInterfaces();
// TODO: The loading logic is replicated in the "GetSourceProvider" method, should be refactored
lock (m_lock)
{
IRestoreDestinationProviderModule b;
if (m_interfaces.TryGetValue(uri.Scheme, out b) && b != null)
return GetSupportedCommandsCached(b).ToList();
else if (uri.Scheme.EndsWith("s", StringComparison.Ordinal))
{
var tmpscheme = uri.Scheme.Substring(0, uri.Scheme.Length - 1);
if (m_interfaces.TryGetValue(tmpscheme, out b) && b != null)
return GetSupportedCommandsCached(b).ToList();
}
return null;
}
}
}
///
/// The static instance used to access SourceProvider information
///
private static readonly RestoreDestinationProviderLoaderSub _RestoreDestinationProvider = new RestoreDestinationProviderLoaderSub();
#region Public static API
///
/// Gets a list of loaded SourceProviders, the instances can be used to extract interface information, not used to interact with the SourceProvider.
///
public static IRestoreDestinationProviderModule[] Modules { get { return _RestoreDestinationProvider.Interfaces; } }
///
/// Gets a list of keys supported
///
public static string[] Keys { get { return _RestoreDestinationProvider.Keys; } }
///
/// Gets the supported commands for a given SourceProvider
///
/// The url to find the commands for
/// The supported commands or null if the url is not supported
public static IReadOnlyList GetSupportedCommands(string url)
{
if (string.IsNullOrEmpty(url))
throw new ArgumentNullException(nameof(url));
var commands = _RestoreDestinationProvider.GetSupportedCommands(url);
if (commands != null)
return commands;
var backend = BackendLoader.GetBackend(url, []);
if (backend is IFolderEnabledBackend folderBackend)
commands = folderBackend.SupportedCommands.AsReadOnly();
backend?.Dispose();
return commands;
}
///
/// Instanciates a specific RestoreDestinationProvider, given the url and options
///
/// The url to create the instance for
/// The options to pass to the instance constructor
/// The cancellation token
/// The instanciated RestoreDestinationProvider or null if the url is not supported
public static Task GetRestoreDestinationProvider(string url, Dictionary options, CancellationToken cancellationToken)
=> GetRestoreDestinationProvider(url, options, false, cancellationToken);
///
/// Instanciates a specific RestoreDestinationProvider for testing purposes, given the url and options
///
/// The url to create the instance for
/// The options to pass to the instance constructor
/// The cancellation token
/// The instanciated RestoreDestinationProvider or null if the url is not supported
public static Task GetRestoreDestinationProviderForTesting(string url, Dictionary options, CancellationToken cancellationToken)
=> GetRestoreDestinationProvider(url, options, true, cancellationToken);
///
/// Instanciates a specific RestoreDestinationProvider, given the url and options
///
/// The url to create the instance for
/// The options to pass to the instance constructor
/// If true, the RestoreDestinationProvider is instanciated for testing purposes, and may not be used for actual backups
/// The cancellation token
/// The instanciated RestoreDestinationProvider or null if the url is not supported
private static async Task GetRestoreDestinationProvider(string url, Dictionary options, bool getForTesting, CancellationToken cancellationToken)
{
// Source providers are preferred over backends
var provider = _RestoreDestinationProvider.GetRestoreDestinationProvider(url, options);
// TODO: Support restoring to backends as well
// if (provider == null)
// {
// // See if there is a backend that can also be a destination provider
// var backend = BackendLoader.GetBackend(url, options);
// if (backend is IFolderEnabledBackend folderBackend)
// provider = new BackendRestoreDestinationProvider(folderBackend, mountPoint);
// else
// backend?.Dispose();
// }
if (provider == null)
return null;
try
{
if (!getForTesting)
await provider.Initialize(cancellationToken).ConfigureAwait(false);
return provider;
}
catch
{
provider.Dispose();
throw;
}
}
#endregion
///
/// Adds a SourceProvider to the loader
///
/// The SourceProvider to add
public static void AddSourceProvider(IRestoreDestinationProviderModule SourceProvider)
{
_RestoreDestinationProvider.AddModule(SourceProvider);
}
}
}