diff --git a/Duplicati.sln b/Duplicati.sln
index 36212bbde..e9f8d2405 100644
--- a/Duplicati.sln
+++ b/Duplicati.sln
@@ -178,6 +178,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RemoteSynchronization", "To
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.SyncTool", "Executables\net8\Duplicati.CommandLine.SyncTool\Duplicati.CommandLine.SyncTool.csproj", "{48D32674-0FE9-4407-B102-4AE46D93595F}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.SourceTool", "Duplicati\CommandLine\SourceTool\Duplicati.CommandLine.SourceTool.csproj", "{2873BCE2-2EA6-419D-BFFA-693FDB34926E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.SourceTool", "Executables\net8\Duplicati.CommandLine.SourceTool\Duplicati.CommandLine.SourceTool.csproj", "{4003BF90-6681-4155-9D06-639ED23FE3AB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -500,6 +504,14 @@ Global
{48D32674-0FE9-4407-B102-4AE46D93595F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{48D32674-0FE9-4407-B102-4AE46D93595F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{48D32674-0FE9-4407-B102-4AE46D93595F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2873BCE2-2EA6-419D-BFFA-693FDB34926E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2873BCE2-2EA6-419D-BFFA-693FDB34926E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2873BCE2-2EA6-419D-BFFA-693FDB34926E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2873BCE2-2EA6-419D-BFFA-693FDB34926E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4003BF90-6681-4155-9D06-639ED23FE3AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4003BF90-6681-4155-9D06-639ED23FE3AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4003BF90-6681-4155-9D06-639ED23FE3AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4003BF90-6681-4155-9D06-639ED23FE3AB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -563,6 +575,8 @@ Global
{6D9742A7-6F32-4571-8276-79716168B525} = {424CEF73-4984-430E-9C8B-E61CC0F22074}
{D3A7E41E-279D-4E0A-A2DB-5E8002E02B6D} = {15388C37-9218-4818-972E-738EEA8F1602}
{48D32674-0FE9-4407-B102-4AE46D93595F} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
+ {2873BCE2-2EA6-419D-BFFA-693FDB34926E} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
+ {4003BF90-6681-4155-9D06-639ED23FE3AB} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8B40BAFE-D862-4397-9495-8F5EAF5CE80C}
diff --git a/Duplicati/CommandLine/SourceTool/CommandExtensions.cs b/Duplicati/CommandLine/SourceTool/CommandExtensions.cs
new file mode 100644
index 000000000..019672143
--- /dev/null
+++ b/Duplicati/CommandLine/SourceTool/CommandExtensions.cs
@@ -0,0 +1,42 @@
+// 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.CommandLine;
+using System.CommandLine.Invocation;
+
+namespace Duplicati.CommandLine.SourceTool;
+
+///
+/// Extensions for .
+///
+public static class CommandExtensions
+{
+ ///
+ /// Adds the missing WithHandler method to .
+ ///
+ /// The command to add the handler to.
+ /// The handler to add.
+ /// The command with the handler added.
+ public static Command WithHandler(this Command command, ICommandHandler handler)
+ {
+ command.Handler = handler;
+ return command;
+ }
+}
diff --git a/Duplicati/CommandLine/SourceTool/Commands/Download.cs b/Duplicati/CommandLine/SourceTool/Commands/Download.cs
new file mode 100644
index 000000000..7f2f1d046
--- /dev/null
+++ b/Duplicati/CommandLine/SourceTool/Commands/Download.cs
@@ -0,0 +1,110 @@
+// 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.CommandLine;
+using System.CommandLine.NamingConventionBinder;
+
+namespace Duplicati.CommandLine.SourceTool.Commands;
+
+///
+/// The download command
+///
+public static class Download
+{
+ ///
+ /// Creates the download command
+ ///
+ /// The download command
+ public static Command Create() =>
+ new Command("download", "Downloads all files from the remote")
+ {
+ new Argument("url", "The source URL") {
+ Arity = ArgumentArity.ExactlyOne
+ },
+ new Option("--destination", description: "The destination folder", getDefaultValue: () => new DirectoryInfo(Directory.GetCurrentDirectory())),
+ new Option("--max-depth", description: "The maximum depth to visit", getDefaultValue: () => 0),
+ new Option("--max-size", description: "The maximum filesize to download", getDefaultValue: () => 0),
+ new Option("--overwrite", description: "Overwrite existing files", getDefaultValue: () => false),
+ }
+ .WithHandler(CommandHandler.Create(async (url, destination, maxdepth, maxsize, overwrite) =>
+ {
+ var token = new CancellationTokenSource().Token;
+
+ using var source = Common.GetProvider(url);
+
+ string localPath(string path)
+ {
+ var relpath = path.Substring(source.PathKey.Length).TrimStart('/');
+ return Path.Combine(destination.FullName, relpath);
+ }
+
+ var started = DateTime.UtcNow;
+ var fileCount = 0L;
+ var folderCount = 0L;
+ var totalSize = 0L;
+ var downloadCount = 0L;
+ var downloadSize = 0L;
+
+ await Common.Visit(source, maxdepth, async (entry, level) =>
+ {
+ if (entry.IsMetaEntry)
+ return true;
+
+ var path = localPath(entry.Path);
+ if (entry.IsFolder)
+ {
+ folderCount++;
+ if (!Directory.Exists(path))
+ Directory.CreateDirectory(path);
+ }
+ else
+ {
+ fileCount++;
+ totalSize += entry.Size;
+
+ if (entry.Size <= maxsize || maxsize <= 0)
+ {
+ var folder = Path.GetDirectoryName(path);
+ if (!string.IsNullOrWhiteSpace(folder) && !Directory.Exists(folder))
+ Directory.CreateDirectory(folder);
+
+ if (!overwrite && File.Exists(path))
+ {
+ Console.WriteLine($"Skipping {entry.Path} as it already exists");
+ }
+ else
+ {
+ downloadCount++;
+ downloadSize += entry.Size;
+ Console.WriteLine($"Downloading {entry.Path} to {path} ({Library.Utility.Utility.FormatSizeString(entry.Size)})");
+ await using var stream = await entry.OpenRead(token);
+ await using var dest = File.OpenWrite(path);
+ await stream.CopyToAsync(dest, token);
+ }
+ }
+ }
+
+ return true;
+ }, token);
+
+ Console.WriteLine($"Found {fileCount} files and {folderCount} folders with a total size of {Library.Utility.Utility.FormatSizeString(totalSize)}");
+ Console.WriteLine($"Downloaded {downloadCount} files with a total size of {Library.Utility.Utility.FormatSizeString(downloadSize)} in {DateTime.UtcNow - started}");
+ }));
+}
diff --git a/Duplicati/CommandLine/SourceTool/Commands/List.cs b/Duplicati/CommandLine/SourceTool/Commands/List.cs
new file mode 100644
index 000000000..a2169b99e
--- /dev/null
+++ b/Duplicati/CommandLine/SourceTool/Commands/List.cs
@@ -0,0 +1,66 @@
+// 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.CommandLine;
+using System.CommandLine.NamingConventionBinder;
+
+namespace Duplicati.CommandLine.SourceTool.Commands;
+
+///
+/// The list command
+///
+public static class List
+{
+ ///
+ /// Creates the list command
+ ///
+ /// The command
+ public static Command Create() =>
+ new Command("list", "Lists all paths on the remote")
+ {
+ new Argument("url", "The source URL") {
+ Arity = ArgumentArity.ExactlyOne
+ },
+ new Option("--max-depth", description: "The maximum depth to list", getDefaultValue: () => 0),
+ }
+ .WithHandler(CommandHandler.Create(async (url, maxdepth) =>
+ {
+ var token = new CancellationTokenSource().Token;
+ var folders = 0L;
+ var files = 0L;
+
+ using var source = Common.GetProvider(url);
+ await Common.Visit(source, maxdepth, (entry, level) =>
+ {
+ if (entry.IsFolder)
+ folders++;
+ else
+ files++;
+ if (entry.IsFolder && level > 0)
+ level--;
+ Console.WriteLine($"{new string(' ', (level + 1) * 2)}{entry.Path}");
+ return Task.FromResult(true);
+ }, token);
+
+ Console.WriteLine($"Found {folders} folders and {files} files");
+ })
+ );
+
+}
diff --git a/Duplicati/CommandLine/SourceTool/Duplicati.CommandLine.SourceTool.csproj b/Duplicati/CommandLine/SourceTool/Duplicati.CommandLine.SourceTool.csproj
new file mode 100644
index 000000000..99c103755
--- /dev/null
+++ b/Duplicati/CommandLine/SourceTool/Duplicati.CommandLine.SourceTool.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net8.0
+ enable
+ enable
+ Duplicati.CommandLine.SourceTool.Implementation
+ Copyright © 2025 Team Duplicati, MIT license
+ Duplicati.CommandLine.Sourcetool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Duplicati/CommandLine/SourceTool/GetProvider.cs b/Duplicati/CommandLine/SourceTool/GetProvider.cs
new file mode 100644
index 000000000..62888b184
--- /dev/null
+++ b/Duplicati/CommandLine/SourceTool/GetProvider.cs
@@ -0,0 +1,39 @@
+// 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 Duplicati.Library.DynamicLoader;
+using Duplicati.Library.Interface;
+
+namespace Duplicati.CommandLine.SourceTool;
+
+///
+/// Common methods for the source tool
+///
+public static partial class Common
+{
+ ///
+ /// Gets the source provider for the given URL
+ ///
+ /// The URL to get the provider for
+ /// The source provider
+ public static ISourceProvider GetProvider(string url) =>
+ SourceProviderLoader.GetSourceProvider(url, [])
+ ?? throw new UserInformationException(string.Format("The source \"{0}\" is not supported", url), "SourceNotSupported");
+}
\ No newline at end of file
diff --git a/Duplicati/CommandLine/SourceTool/ListEnumerator.cs b/Duplicati/CommandLine/SourceTool/ListEnumerator.cs
new file mode 100644
index 000000000..e8899c8b6
--- /dev/null
+++ b/Duplicati/CommandLine/SourceTool/ListEnumerator.cs
@@ -0,0 +1,60 @@
+// 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 Duplicati.Library.Interface;
+
+namespace Duplicati.CommandLine.SourceTool;
+
+///
+/// Common methods for the source tool
+///
+public static partial class Common
+{
+ ///
+ /// Visits all entries in the source provider
+ ///
+ /// The source provider to visit
+ /// The maximum depth to visit
+ /// The visitor function
+ /// The cancellation token
+ /// An awaitable task
+ public static async Task Visit(ISourceProvider source, int maxdepth, Func> visitor, CancellationToken token)
+ {
+ var visit = new Stack<(ISourceFileEntry Entry, int Level)>();
+ await foreach (var item in source.Enumerate(token))
+ visit.Push((item, 0));
+
+ while (visit.Count() != 0)
+ {
+ var item = visit.Pop();
+ var process = await visitor(item.Entry, item.Level);
+ if (item.Entry.IsFolder && process && (item.Level < maxdepth || maxdepth <= 0))
+ {
+ await foreach (var subitem in item.Entry.Enumerate(token))
+ {
+ if (subitem.IsFolder)
+ visit.Push((subitem, item.Level + 1));
+ else
+ await visitor(subitem, item.Level);
+ }
+ }
+ }
+ }
+}
diff --git a/Duplicati/CommandLine/SourceTool/Program.cs b/Duplicati/CommandLine/SourceTool/Program.cs
new file mode 100644
index 000000000..c8aea4225
--- /dev/null
+++ b/Duplicati/CommandLine/SourceTool/Program.cs
@@ -0,0 +1,68 @@
+// 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.CommandLine;
+using System.CommandLine.Builder;
+using System.CommandLine.Parsing;
+using Duplicati.Library.Interface;
+using Duplicati.Library.Utility;
+
+namespace Duplicati.CommandLine.SourceTool;
+
+///
+/// The entry point of the application
+///
+public static class Program
+{
+ ///
+ /// Executes the program
+ ///
+ /// The arguments
+ /// The return code
+ public static Task Main(string[] args)
+ {
+ Library.AutoUpdater.PreloadSettingsLoader.ConfigurePreloadSettings(ref args, Library.AutoUpdater.PackageHelper.NamedExecutable.ServerUtil);
+
+ var rootCmd = new RootCommand("Source CLI tool for Duplicati")
+ {
+ Commands.List.Create(),
+ Commands.Download.Create(),
+ };
+
+ return new CommandLineBuilder(rootCmd)
+ .UseDefaults()
+ .UseExceptionHandler((ex, context) =>
+ {
+ if (ex is UserInformationException uie)
+ {
+ Console.WriteLine(uie.Message);
+ context.ExitCode = 2;
+ }
+ else
+ {
+ Console.WriteLine(ex.ToString());
+ context.ExitCode = 1;
+ }
+ })
+ .UseAdditionalHelpAliases()
+ .Build()
+ .InvokeAsync(args);
+ }
+}
diff --git a/Duplicati/Library/AutoUpdater/PackageHelper.cs b/Duplicati/Library/AutoUpdater/PackageHelper.cs
index 314facb04..4ceefc41e 100644
--- a/Duplicati/Library/AutoUpdater/PackageHelper.cs
+++ b/Duplicati/Library/AutoUpdater/PackageHelper.cs
@@ -92,7 +92,11 @@ namespace Duplicati.Library.AutoUpdater
///
/// The sync tool
///
- SyncTool
+ SyncTool,
+ ///
+ /// The source tool
+ ///
+ SourceTool
}
@@ -120,6 +124,7 @@ namespace Duplicati.Library.AutoUpdater
NamedExecutable.Agent => OperatingSystem.IsWindows() ? "Duplicati.Agent.exe" : "duplicati-agent",
NamedExecutable.SecretTool => OperatingSystem.IsWindows() ? "Duplicati.CommandLine.SecretTool.exe" : "duplicati-secret-tool",
NamedExecutable.SyncTool => OperatingSystem.IsWindows() ? "Duplicati.CommandLine.SyncTool.exe" : "duplicati-sync-tool",
+ NamedExecutable.SourceTool => OperatingSystem.IsWindows() ? "Duplicati.CommandLine.Sourcetool.exe" : "duplicati-source-tool",
_ => throw new ArgumentException($"Named executable not known: {exe}", nameof(exe))
};
diff --git a/Duplicati/Library/DynamicLoader/SourceProviderLoader.cs b/Duplicati/Library/DynamicLoader/SourceProviderLoader.cs
index 23920c594..f73022611 100644
--- a/Duplicati/Library/DynamicLoader/SourceProviderLoader.cs
+++ b/Duplicati/Library/DynamicLoader/SourceProviderLoader.cs
@@ -25,6 +25,7 @@ using Duplicati.Library.Interface;
using System.Linq;
using System.Runtime.ExceptionServices;
using Duplicati.Library.SourceProviders;
+using Duplicati.Library.SourceProvider;
namespace Duplicati.Library.DynamicLoader
{
@@ -164,7 +165,19 @@ namespace Duplicati.Library.DynamicLoader
/// The instanciated SourceProvider or null if the url is not supported
public static ISourceProviderModule GetSourceProvider(string url, Dictionary options)
{
- return _SourceProviderLoader.GetSourceProvider(url, options);
+ // Source providers are preferred over backends
+ var provider = _SourceProviderLoader.GetSourceProvider(url, options);
+ if (provider != null)
+ return provider;
+
+ // See if there is a backend that can also be a source
+ var backend = BackendLoader.GetBackend(url, options);
+ if (backend is IFolderEnabledBackend folderBackend)
+ return new BackendSourceProvider(folderBackend);
+
+ backend?.Dispose();
+ return null;
+
}
#endregion
diff --git a/Duplicati/Library/Main/Operation/BackupHandler.cs b/Duplicati/Library/Main/Operation/BackupHandler.cs
index 059bbc8df..8d09a6f27 100644
--- a/Duplicati/Library/Main/Operation/BackupHandler.cs
+++ b/Duplicati/Library/Main/Operation/BackupHandler.cs
@@ -138,27 +138,10 @@ namespace Duplicati.Library.Main.Operation
results.Add(new LocalFileSource(SnapshotUtility.CreateSnapshot(entry, options.RawOptions, options.SymlinkPolicy == Options.SymlinkStrategy.Follow)));
else
{
+ // Assume it is a remote source
foreach (var url in entry)
- {
- // Source providers are preferred over backends
- var provider = SourceProviderLoader.GetSourceProvider(url, options.RawOptions);
- if (provider != null)
- {
- results.Add(provider);
- continue;
- }
-
- // See if there is a backend that can also be a source
- var backend = BackendLoader.GetBackend(url, options.RawOptions);
- if (backend is IFolderEnabledBackend folderBackend)
- {
- results.Add(new BackendSourceProvider(folderBackend));
- continue;
- }
-
- backend?.Dispose();
- throw new UserInformationException(string.Format("The source \"{0}\" is not supported", url), "SourceNotSupported");
- }
+ results.Add(SourceProviderLoader.GetSourceProvider(url, options.RawOptions)
+ ?? throw new UserInformationException(string.Format("The source \"{0}\" is not supported", url), "SourceNotSupported"));
}
}
}
diff --git a/Duplicati/Library/SourceProvider/Builtin/BackendSourceProvider.cs b/Duplicati/Library/SourceProvider/Builtin/BackendSourceProvider.cs
index 6411cc0a7..618de97f7 100644
--- a/Duplicati/Library/SourceProvider/Builtin/BackendSourceProvider.cs
+++ b/Duplicati/Library/SourceProvider/Builtin/BackendSourceProvider.cs
@@ -19,6 +19,8 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
+using System.Runtime.CompilerServices;
+using Duplicati.Library.Common.IO;
using Duplicati.Library.Interface;
namespace Duplicati.Library.SourceProvider;
@@ -27,7 +29,7 @@ namespace Duplicati.Library.SourceProvider;
/// A source provider that wraps a backend
///
/// The backend to wrap
-public class BackendSourceProvider(IFolderEnabledBackend backend) : ISourceProvider
+public class BackendSourceProvider(IFolderEnabledBackend backend) : ISourceProvider, ISourceProviderModule
{
///
/// The wrapped backend
@@ -39,6 +41,15 @@ public class BackendSourceProvider(IFolderEnabledBackend backend) : ISourceProvi
///
public string PathKey => backend.PathKey;
+ ///
+ public string Key => backend.ProtocolKey;
+
+ ///
+ public string DisplayName => backend.DisplayName;
+
+ ///
+ public IList SupportedCommands => backend.SupportedCommands;
+
///
public IAsyncEnumerable Enumerate(CancellationToken cancellationToken)
=> new[] { new BackendSourceFileEntry(this, PathKey, true, true, new DateTime(0), new DateTime(0), 0) }.ToAsyncEnumerable();
diff --git a/Executables/net8/Duplicati.CommandLine.SourceTool/Duplicati.CommandLine.SourceTool.csproj b/Executables/net8/Duplicati.CommandLine.SourceTool/Duplicati.CommandLine.SourceTool.csproj
new file mode 100644
index 000000000..038a3b98d
--- /dev/null
+++ b/Executables/net8/Duplicati.CommandLine.SourceTool/Duplicati.CommandLine.SourceTool.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net8.0
+ SyncTool for Duplicati
+ Copyright © 2025 Team Duplicati, MIT license
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
diff --git a/Executables/net8/Duplicati.CommandLine.SourceTool/Program.cs b/Executables/net8/Duplicati.CommandLine.SourceTool/Program.cs
new file mode 100644
index 000000000..490864a25
--- /dev/null
+++ b/Executables/net8/Duplicati.CommandLine.SourceTool/Program.cs
@@ -0,0 +1,31 @@
+// 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 Duplicati.Library.Crashlog;
+
+namespace Duplicati.CommandLine.SourceTool.Net8
+{
+ // Wrapper class to keep code independent
+ public static class Program
+ {
+ public static int Main(string[] args)
+ => CrashlogHelper.WrapWithCrashLog(() => SourceTool.Program.Main(args).ConfigureAwait(false).GetAwaiter().GetResult());
+ }
+}
\ No newline at end of file
diff --git a/ReleaseBuilder/Build/Command.cs b/ReleaseBuilder/Build/Command.cs
index 46ea9f935..94cd6a8c3 100644
--- a/ReleaseBuilder/Build/Command.cs
+++ b/ReleaseBuilder/Build/Command.cs
@@ -87,6 +87,7 @@ public static partial class Command
{ "Duplicati.CommandLine.ServerUtil", "duplicati-server-util" },
{ "Duplicati.CommandLine.SecretTool", "duplicati-secret-tool" },
{ "Duplicati.CommandLine.SyncTool", "duplicati-sync-tool" },
+ { "Duplicati.CommandLine.SourceTool", "duplicati-source-tool" },
{ "Duplicati.Service", "duplicati-service" },
{ "Duplicati.Agent", "duplicati-agent" },
{ "Duplicati.CommandLine", "duplicati-cli" },