From 5a4fcaa6d77b30d29b4dc78c128d86ece686d546 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Fri, 21 Feb 2025 11:51:45 +0100 Subject: [PATCH] Added tool to work with remote sources --- Duplicati.sln | 14 +++ .../SourceTool/CommandExtensions.cs | 42 +++++++ .../SourceTool/Commands/Download.cs | 110 ++++++++++++++++++ .../CommandLine/SourceTool/Commands/List.cs | 66 +++++++++++ .../Duplicati.CommandLine.SourceTool.csproj | 25 ++++ .../CommandLine/SourceTool/GetProvider.cs | 39 +++++++ .../CommandLine/SourceTool/ListEnumerator.cs | 60 ++++++++++ Duplicati/CommandLine/SourceTool/Program.cs | 68 +++++++++++ .../Library/AutoUpdater/PackageHelper.cs | 7 +- .../DynamicLoader/SourceProviderLoader.cs | 15 ++- .../Library/Main/Operation/BackupHandler.cs | 23 +--- .../Builtin/BackendSourceProvider.cs | 13 ++- .../Duplicati.CommandLine.SourceTool.csproj | 22 ++++ .../Program.cs | 31 +++++ ReleaseBuilder/Build/Command.cs | 1 + 15 files changed, 513 insertions(+), 23 deletions(-) create mode 100644 Duplicati/CommandLine/SourceTool/CommandExtensions.cs create mode 100644 Duplicati/CommandLine/SourceTool/Commands/Download.cs create mode 100644 Duplicati/CommandLine/SourceTool/Commands/List.cs create mode 100644 Duplicati/CommandLine/SourceTool/Duplicati.CommandLine.SourceTool.csproj create mode 100644 Duplicati/CommandLine/SourceTool/GetProvider.cs create mode 100644 Duplicati/CommandLine/SourceTool/ListEnumerator.cs create mode 100644 Duplicati/CommandLine/SourceTool/Program.cs create mode 100644 Executables/net8/Duplicati.CommandLine.SourceTool/Duplicati.CommandLine.SourceTool.csproj create mode 100644 Executables/net8/Duplicati.CommandLine.SourceTool/Program.cs 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" },