Merge branch 'master' into stylelint

This commit is contained in:
Suguru Hirahara
2024-08-31 12:30:07 -04:00
262 changed files with 23170 additions and 9525 deletions
+4 -4
View File
@@ -49,22 +49,22 @@
"console": "internalConsole"
},
{
"name": "Launch ConfigurationImporter executable",
"name": "Launch RecoveryTool executable",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Executables/net8/Duplicati.CommandLine.ConfigurationImporter/bin/Debug/net8.0/Duplicati.CommandLine.ConfigurationImporter",
"program": "${workspaceFolder}/Executables/net8/Duplicati.CommandLine.RecoveryTool/bin/Debug/net8.0/Duplicati.CommandLine.RecoveryTool",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "internalConsole"
},
{
"name": "Launch RecoveryTool executable",
"name": "Launch ServerUtil executable",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Executables/net8/Duplicati.CommandLine.RecoveryTool/bin/Debug/net8.0/Duplicati.CommandLine.RecoveryTool",
"program": "${workspaceFolder}/Executables/net8/Duplicati.CommandLine.ServerUtil/bin/Debug/net8.0/Duplicati.CommandLine.ServerUtil",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
+22 -29
View File
@@ -93,12 +93,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.R
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Common", "Duplicati\Library\Common\Duplicati.Library.Common.csproj", "{D63E53E4-A458-4C2F-914D-92F715F58ACF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.ConfigurationImporter", "Duplicati\CommandLine\ConfigurationImporter\Duplicati.CommandLine.ConfigurationImporter.csproj", "{B93E3BF0-DAA7-49B7-B07D-0559C5816735}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Encryption", "Duplicati\Library\Encryption\Duplicati.Library.Encryption.csproj", "{2CF2D90E-C25B-47AD-91E0-98451BAB8058}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.AutoUpdater", "Duplicati\CommandLine\AutoUpdater\Duplicati.CommandLine.AutoUpdater.csproj", "{4D1B198B-F773-44F2-872B-2B283053323C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.Storj", "Duplicati\Library\Backend\Storj\Duplicati.Library.Backend.Storj.csproj", "{E9AB8491-BD4C-4E4F-84C3-0BD551CC7489}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.TencentCOS", "Duplicati\Library\Backend\TencentCOS\Duplicati.Library.Backend.TencentCOS.csproj", "{545DD6D4-9476-42D6-B51C-A28E000C489E}"
@@ -115,16 +111,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.Backe
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.BackendTool", "Executables\net8\Duplicati.CommandLine.BackendTool\Duplicati.CommandLine.BackendTool.csproj", "{31FA5B9B-4CD6-4BF3-B7A9-12C1E30DCAF6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.ConfigurationImporter", "Executables\net8\Duplicati.CommandLine.ConfigurationImporter\Duplicati.CommandLine.ConfigurationImporter.csproj", "{156B9B77-C2B7-4169-8FE7-1171D221446C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.RecoveryTool", "Executables\net8\Duplicati.CommandLine.RecoveryTool\Duplicati.CommandLine.RecoveryTool.csproj", "{0FFC557E-1B84-46A2-B6E8-06064FA7EA58}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.GUI.TrayIcon", "Executables\net8\Duplicati.GUI.TrayIcon\Duplicati.GUI.TrayIcon.csproj", "{AF32C621-30DC-40F5-8CE6-DD69053068E9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Server", "Executables\net8\Duplicati.Server\Duplicati.Server.csproj", "{55EEEBD2-CE45-45D6-9838-958F1C7354E4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsService", "Duplicati\WindowsService\WindowsService.csproj", "{8A651E2D-A7C8-4EC7-B421-7A457654811D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.WindowsService", "Executables\net8\Duplicati.WindowsService\Duplicati.WindowsService.csproj", "{5D20B150-C445-47BE-8CE8-C9F74F19A4F2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine", "Executables\net8\Duplicati.CommandLine\Duplicati.CommandLine.csproj", "{0F5A1F4E-25FA-4D02-920D-CA2138498081}"
@@ -144,7 +136,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backends"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.WebserverCore", "Duplicati\WebserverCore\Duplicati.WebserverCore.csproj", "{5A702CEE-DB36-4153-BD94-D8CF867E75A9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.RestAPI", "Duplicati.Library.RestAPI\Duplicati.Library.RestAPI.csproj", "{C1D4D665-23A3-4216-9CD1-D67AE9AAAA4C}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.RestAPI", "Duplicati\Library\RestAPI\Duplicati.Library.RestAPI.csproj", "{C1D4D665-23A3-4216-9CD1-D67AE9AAAA4C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.AliyunOSS", "Duplicati\Library\Backend\AliyunOSS\Duplicati.Library.Backend.AliyunOSS.csproj", "{4EB3DABC-D412-4C12-8876-41A1427A389E}"
EndProject
@@ -152,6 +144,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.Sharp
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.Snapshots", "Executables\net8\Duplicati.CommandLine.Snapshots\Duplicati.CommandLine.Snapshots.csproj", "{0364E724-1929-445E-9145-90A70B01DDC0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.ServerUtil", "Duplicati\CommandLine\ServerUtil\Duplicati.CommandLine.ServerUtil.csproj", "{5AF834B1-D227-4A98-9377-6BFA6BCF99A7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.ServerUtil", "Executables\net8\Duplicati.CommandLine.ServerUtil\Duplicati.CommandLine.ServerUtil.csproj", "{C09A7DE2-F0F3-4FA4-B36C-A0DE7844739B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.WindowsService", "Duplicati\WindowsService\Duplicati.WindowsService.csproj", "{3476A88B-4123-45F4-AC96-700B747367EB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -338,18 +336,10 @@ Global
{D63E53E4-A458-4C2F-914D-92F715F58ACF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D63E53E4-A458-4C2F-914D-92F715F58ACF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D63E53E4-A458-4C2F-914D-92F715F58ACF}.Release|Any CPU.Build.0 = Release|Any CPU
{B93E3BF0-DAA7-49B7-B07D-0559C5816735}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B93E3BF0-DAA7-49B7-B07D-0559C5816735}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B93E3BF0-DAA7-49B7-B07D-0559C5816735}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B93E3BF0-DAA7-49B7-B07D-0559C5816735}.Release|Any CPU.Build.0 = Release|Any CPU
{2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Release|Any CPU.Build.0 = Release|Any CPU
{4D1B198B-F773-44F2-872B-2B283053323C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4D1B198B-F773-44F2-872B-2B283053323C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4D1B198B-F773-44F2-872B-2B283053323C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4D1B198B-F773-44F2-872B-2B283053323C}.Release|Any CPU.Build.0 = Release|Any CPU
{E9AB8491-BD4C-4E4F-84C3-0BD551CC7489}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E9AB8491-BD4C-4E4F-84C3-0BD551CC7489}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E9AB8491-BD4C-4E4F-84C3-0BD551CC7489}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -374,10 +364,6 @@ Global
{31FA5B9B-4CD6-4BF3-B7A9-12C1E30DCAF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31FA5B9B-4CD6-4BF3-B7A9-12C1E30DCAF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{31FA5B9B-4CD6-4BF3-B7A9-12C1E30DCAF6}.Release|Any CPU.Build.0 = Release|Any CPU
{156B9B77-C2B7-4169-8FE7-1171D221446C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{156B9B77-C2B7-4169-8FE7-1171D221446C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{156B9B77-C2B7-4169-8FE7-1171D221446C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{156B9B77-C2B7-4169-8FE7-1171D221446C}.Release|Any CPU.Build.0 = Release|Any CPU
{0FFC557E-1B84-46A2-B6E8-06064FA7EA58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0FFC557E-1B84-46A2-B6E8-06064FA7EA58}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0FFC557E-1B84-46A2-B6E8-06064FA7EA58}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -390,10 +376,6 @@ Global
{55EEEBD2-CE45-45D6-9838-958F1C7354E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{55EEEBD2-CE45-45D6-9838-958F1C7354E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{55EEEBD2-CE45-45D6-9838-958F1C7354E4}.Release|Any CPU.Build.0 = Release|Any CPU
{8A651E2D-A7C8-4EC7-B421-7A457654811D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A651E2D-A7C8-4EC7-B421-7A457654811D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A651E2D-A7C8-4EC7-B421-7A457654811D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A651E2D-A7C8-4EC7-B421-7A457654811D}.Release|Any CPU.Build.0 = Release|Any CPU
{5D20B150-C445-47BE-8CE8-C9F74F19A4F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D20B150-C445-47BE-8CE8-C9F74F19A4F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D20B150-C445-47BE-8CE8-C9F74F19A4F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -426,6 +408,18 @@ Global
{0364E724-1929-445E-9145-90A70B01DDC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0364E724-1929-445E-9145-90A70B01DDC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0364E724-1929-445E-9145-90A70B01DDC0}.Release|Any CPU.Build.0 = Release|Any CPU
{5AF834B1-D227-4A98-9377-6BFA6BCF99A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5AF834B1-D227-4A98-9377-6BFA6BCF99A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5AF834B1-D227-4A98-9377-6BFA6BCF99A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5AF834B1-D227-4A98-9377-6BFA6BCF99A7}.Release|Any CPU.Build.0 = Release|Any CPU
{C09A7DE2-F0F3-4FA4-B36C-A0DE7844739B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C09A7DE2-F0F3-4FA4-B36C-A0DE7844739B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C09A7DE2-F0F3-4FA4-B36C-A0DE7844739B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C09A7DE2-F0F3-4FA4-B36C-A0DE7844739B}.Release|Any CPU.Build.0 = Release|Any CPU
{3476A88B-4123-45F4-AC96-700B747367EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3476A88B-4123-45F4-AC96-700B747367EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3476A88B-4123-45F4-AC96-700B747367EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3476A88B-4123-45F4-AC96-700B747367EB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -461,8 +455,6 @@ Global
{2CD5DBC3-3DA6-432D-BA97-F0B8D24501C2} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
{32A74526-3E5F-413A-8CB4-1EFDAD4C8B91} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
{851A1CB8-3CEB-41B4-956F-34D760D2A8E5} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
{B93E3BF0-DAA7-49B7-B07D-0559C5816735} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
{4D1B198B-F773-44F2-872B-2B283053323C} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
{E9AB8491-BD4C-4E4F-84C3-0BD551CC7489} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
{545DD6D4-9476-42D6-B51C-A28E000C489E} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
{6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3} = {FA88A246-EF8E-46E3-90AF-539B8C0A6ADE}
@@ -470,17 +462,18 @@ Global
{34149709-F3ED-4FB5-A087-43EB195C948B} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{2F1C0C8D-5C15-4BC0-811F-87F2C98D9790} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{31FA5B9B-4CD6-4BF3-B7A9-12C1E30DCAF6} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{156B9B77-C2B7-4169-8FE7-1171D221446C} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{0FFC557E-1B84-46A2-B6E8-06064FA7EA58} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{AF32C621-30DC-40F5-8CE6-DD69053068E9} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{55EEEBD2-CE45-45D6-9838-958F1C7354E4} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{8A651E2D-A7C8-4EC7-B421-7A457654811D} = {566EBBDA-19A4-4056-A615-D901D57D2439}
{5D20B150-C445-47BE-8CE8-C9F74F19A4F2} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{0F5A1F4E-25FA-4D02-920D-CA2138498081} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{6B594D23-B629-465C-B799-70EE9E56C218} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
{D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B} = {FA88A246-EF8E-46E3-90AF-539B8C0A6ADE}
{FE6FD36C-E171-4599-8D55-62DA579C0864} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{0364E724-1929-445E-9145-90A70B01DDC0} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{5AF834B1-D227-4A98-9377-6BFA6BCF99A7} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
{C09A7DE2-F0F3-4FA4-B36C-A0DE7844739B} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{3476A88B-4123-45F4-AC96-700B747367EB} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8B40BAFE-D862-4397-9495-8F5EAF5CE80C}
@@ -1,24 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>Duplicati.CommandLine.AutoUpdater.Implementation</AssemblyName>
<RootNamespace>Duplicati.CommandLine.AutoUpdater</RootNamespace>
<Copyright>Copyright © 2024 Team Duplicati, MIT license</Copyright>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj" />
<ProjectReference Include="..\..\Library\Common\Duplicati.Library.Common.csproj" />
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
+12 -10
View File
@@ -34,9 +34,9 @@ namespace Duplicati.CommandLine.BackendTester
/// <summary>
/// Used to maintain a reference to initialized system settings.
/// </summary>
#pragma warning disable CS0414 // The private field `Duplicati.CommandLine.BackendTester.Program.SystemSettings' is assigned but its value is never used
#pragma warning disable CS0414 // The private field `Duplicati.CommandLine.BackendTester.Program.SystemSettings' is assigned but its value is never used
private static IDisposable SystemSettings;
#pragma warning restore CS0414 // The private field `Duplicati.CommandLine.BackendTester.Program.SystemSettings' is assigned but its value is never used
#pragma warning restore CS0414 // The private field `Duplicati.CommandLine.BackendTester.Program.SystemSettings' is assigned but its value is never used
class TempFile
{
@@ -66,6 +66,8 @@ namespace Duplicati.CommandLine.BackendTester
{
try
{
Library.AutoUpdater.PreloadSettingsLoader.ConfigurePreloadSettings(ref _args, Library.AutoUpdater.PackageHelper.NamedExecutable.BackendTester);
if (_args.Length == 1)
{
try
@@ -73,8 +75,8 @@ namespace Duplicati.CommandLine.BackendTester
var p = Environment.ExpandEnvironmentVariables(_args[0]);
if (System.IO.File.Exists(p))
_args = (from x in System.IO.File.ReadLines(p)
where !string.IsNullOrWhiteSpace(x) && !x.Trim().StartsWith("#", StringComparison.Ordinal)
select x.Trim()
where !string.IsNullOrWhiteSpace(x) && !x.Trim().StartsWith("#", StringComparison.Ordinal)
select x.Trim()
).ToArray();
}
catch
@@ -105,7 +107,7 @@ namespace Duplicati.CommandLine.BackendTester
if (options.ContainsKey("tempdir") && !string.IsNullOrEmpty(options["tempdir"]))
Library.Utility.SystemContextSettings.DefaultTempPath = options["tempdir"];
SystemSettings = Duplicati.Library.Utility.SystemContextSettings.StartSession();
if (!options.ContainsKey("auth_password") && !string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("AUTH_PASSWORD")))
@@ -594,11 +596,11 @@ namespace Duplicati.CommandLine.BackendTester
new CommandLineArgument("max-file-size", CommandLineArgument.ArgumentType.Size, "The maximum allowed file size", "File sizes are chosen at random, this value is the upper bound", "50mb"),
new CommandLineArgument("min-filename-length", CommandLineArgument.ArgumentType.Integer, "The minimum allowed filename length", "File name lengths are chosen at random, this value is the lower bound", "5"),
new CommandLineArgument("max-filename-length", CommandLineArgument.ArgumentType.Integer, "The minimum allowed filename length", "File name lengths are chosen at random, this value is the upper bound", "80"),
new CommandLineArgument("trim-filename-spaces", CommandLineArgument.ArgumentType.Boolean, "Trims whitespace from filenames", "A value that indicates if whitespace should be trimmed from the ends of randomly generated filenames", "false"),
new CommandLineArgument("auto-create-folder", CommandLineArgument.ArgumentType.Boolean, "Allows automatic folder creation", "A value that indicates if missing folders are created automatically", "false"),
new CommandLineArgument("skip-overwrite-test", CommandLineArgument.ArgumentType.Boolean, "Bypasses the overwrite test", "A value that indicates if dummy files should be uploaded prior to uploading the real files", "false"),
new CommandLineArgument("auto-clean", CommandLineArgument.ArgumentType.Boolean, "Removes any files found in target folder", "A value that indicates if all files in the target folder should be deleted before starting the first test", "false"),
new CommandLineArgument("force", CommandLineArgument.ArgumentType.Boolean, "Activates file deletion", "A value that indicates if existing files should really be deleted when using auto-clean", "false"),
new CommandLineArgument("trim-filename-spaces", CommandLineArgument.ArgumentType.Boolean, "Trim whitespace from filenames", "A value that indicates if whitespace should be trimmed from the ends of randomly generated filenames", "false"),
new CommandLineArgument("auto-create-folder", CommandLineArgument.ArgumentType.Boolean, "Allow automatic folder creation", "A value that indicates if missing folders are created automatically", "false"),
new CommandLineArgument("skip-overwrite-test", CommandLineArgument.ArgumentType.Boolean, "Bypass the overwrite test", "A value that indicates if dummy files should be uploaded prior to uploading the real files", "false"),
new CommandLineArgument("auto-clean", CommandLineArgument.ArgumentType.Boolean, "Remove any files found in target folder", "A value that indicates if all files in the target folder should be deleted before starting the first test", "false"),
new CommandLineArgument("force", CommandLineArgument.ArgumentType.Boolean, "Activate file deletion", "A value that indicates if existing files should really be deleted when using auto-clean", "false"),
});
}
}
+32 -28
View File
@@ -39,6 +39,8 @@ namespace Duplicati.CommandLine.BackendTool
bool debugoutput = false;
try
{
Library.AutoUpdater.PreloadSettingsLoader.ConfigurePreloadSettings(ref _args, Library.AutoUpdater.PackageHelper.NamedExecutable.BackendTool);
List<string> args = new List<string>(_args);
Dictionary<string, string> options = Library.Utility.CommandLineParser.ExtractOptions(args);
@@ -77,8 +79,8 @@ namespace Duplicati.CommandLine.BackendTool
{
Console.WriteLine("Unsupported command: {0}", args[0]);
Console.WriteLine();
}
}
Console.WriteLine("Usage: <command> <protocol>://<username>:<password>@<path> [filename]");
Console.WriteLine("Example: LIST ftp://user:pass@server/folder");
Console.WriteLine();
@@ -89,37 +91,39 @@ namespace Duplicati.CommandLine.BackendTool
}
var modules = (from n in Library.DynamicLoader.GenericLoader.Modules
where n is Library.Interface.IConnectionModule
select n).ToArray();
where n is Library.Interface.IConnectionModule
select n).ToArray();
var uri = new Library.Utility.Uri(args[1]);
var qp = uri.QueryParameters;
var uri = new Library.Utility.Uri(args[1]);
var qp = uri.QueryParameters;
var backendOpts = new Dictionary<string, string>();
foreach (var k in qp.Keys.Cast<string>())
backendOpts[k] = qp[k];
var backendOpts = new Dictionary<string, string>();
foreach (var k in qp.Keys.Cast<string>())
backendOpts[k] = qp[k];
foreach (var k in backendOpts.Keys) {
options.Remove(k);
}
foreach (var k in backendOpts.Keys)
{
options.Remove(k);
}
foreach (var n in modules) {
n.Configure(options);
n.Configure(backendOpts);
}
using(var backend = Library.DynamicLoader.BackendLoader.GetBackend(args[1], options))
foreach (var n in modules)
{
n.Configure(options);
n.Configure(backendOpts);
}
using (var backend = Library.DynamicLoader.BackendLoader.GetBackend(args[1], options))
{
if (backend == null)
throw new UserInformationException("Backend not supported", "InvalidBackend");
if (command == "list")
{
if (args.Count != 2)
throw new UserInformationException(string.Format("too many arguments: {0}", string.Join(",", args)), "BackendToolTooManyArguments");
Console.WriteLine("{0}\t{1}\t{2}\t{3}", "Name", "Dir/File", "LastChange", "Size");
foreach(var e in backend.List())
foreach (var e in backend.List())
Console.WriteLine("{0}\t{1}\t{2}\t{3}", e.Name, e.IsFolder ? "Dir" : "File", e.LastModification, e.Size < 0 ? "" : Library.Utility.Utility.FormatSizeString(e.Size));
return 0;
@@ -130,7 +134,7 @@ namespace Duplicati.CommandLine.BackendTool
throw new UserInformationException(string.Format("too many arguments: {0}", string.Join(",", args)), "BackendToolTooManyArguments");
backend.CreateFolder();
return 0;
}
else if (command == "delete")
@@ -140,7 +144,7 @@ namespace Duplicati.CommandLine.BackendTool
if (args.Count > 3)
throw new Exception(string.Format("too many arguments: {0}", string.Join(",", args)));
backend.Delete(Path.GetFileName(args[2]));
return 0;
}
else if (command == "get")
@@ -152,21 +156,21 @@ namespace Duplicati.CommandLine.BackendTool
if (File.Exists(args[2]))
throw new UserInformationException("File already exists, not overwriting!", "BackendToolFileAlreadyExists");
backend.Get(Path.GetFileName(args[2]), args[2]);
return 0;
}
else if (command == "put")
{
if (args.Count < 3)
throw new UserInformationException("PUT requires a filename argument","BackendToolPutRequiresAndArgument");
throw new UserInformationException("PUT requires a filename argument", "BackendToolPutRequiresAndArgument");
if (args.Count > 3)
throw new UserInformationException(string.Format("too many arguments: {0}", string.Join(",", args)), "BackendToolTooManyArguments");
backend.PutAsync(Path.GetFileName(args[2]), args[2], CancellationToken.None).Wait();
return 0;
}
throw new Exception("Internal error");
}
}
+1 -1
View File
@@ -662,7 +662,7 @@ namespace Duplicati.CommandLine
}
var parsedStats = result.BackendStatistics as Duplicati.Library.Interface.IParsedBackendStatistics;
output.MessageEvent(string.Format(" Duration of backup: {0:hh\\:mm\\:ss}", result.Duration));
output.MessageEvent(string.Format(" Duration of backup: {0:c}", result.Duration));
if (parsedStats != null)
{
if (parsedStats.KnownFileCount > 0)
+3 -1
View File
@@ -40,6 +40,8 @@ namespace Duplicati.CommandLine
/// </summary>
public static int Main(string[] args)
{
PreloadSettingsLoader.ConfigurePreloadSettings(ref args, PackageHelper.NamedExecutable.CommandLine);
Library.UsageReporter.Reporter.Initialize();
FROM_COMMANDLINE = true;
try
@@ -85,7 +87,7 @@ namespace Duplicati.CommandLine
["systeminfo"] = Commands.SystemInfo,
["send-mail"] = Commands.SendMail
};
return knownCommands;
}
}
@@ -1,22 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<AssemblyName>Duplicati.CommandLine.ConfigurationImporter.Implementation</AssemblyName>
<Copyright>Copyright © 2024 Team Duplicati, MIT license</Copyright>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj" />
<ProjectReference Include="..\..\Server\Duplicati.Server.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
@@ -1,103 +0,0 @@
// Copyright (C) 2024, 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.AutoUpdater;
using Duplicati.Library.RestAPI;
using Duplicati.Server.Serializable;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Duplicati.CommandLine.ConfigurationImporter
{
public static class ConfigurationImporter
{
private static readonly string UsageString = $"Usage: {PackageHelper.GetExecutableName(PackageHelper.NamedExecutable.ConfigurationImporter)} <configuration-file> --import-metadata=(true | false) --server-datafolder=<folder containing Duplicati-server.sqlite>";
public static int Main(string[] args)
{
if (args.Length != 3)
{
Console.WriteLine($"Incorrect number of input arguments.");
Console.WriteLine(UsageString);
return 1;
}
string configurationFile = args[0];
Dictionary<string, string> importOptions = Duplicati.Library.Utility.CommandLineParser.ExtractOptions(args.Skip(1).ToList());
if (!importOptions.TryGetValue("import-metadata", out string importMetadataString))
{
Console.WriteLine($"Missing import-metadata argument.");
Console.WriteLine(UsageString);
return 1;
}
bool importMetadata = Duplicati.Library.Utility.Utility.ParseBool(importMetadataString, false);
if (!importOptions.TryGetValue("server-datafolder", out string serverDatafolder))
{
Console.WriteLine($"Missing server-datafolder argument.");
Console.WriteLine(UsageString);
return 1;
}
Dictionary<string, string> advancedOptions = new Dictionary<string, string>
{
{ "server-datafolder", serverDatafolder }
};
ImportExportStructure importedStructure = BackupImportExportHandler.ImportBackup(configurationFile, importMetadata, () => ConfigurationImporter.ReadPassword($"Password for {configurationFile}: "), advancedOptions);
Console.WriteLine($"Imported \"{importedStructure.Backup.Name}\" with ID {importedStructure.Backup.ID} and local database at {importedStructure.Backup.DBPath}.");
return 0;
}
private static string ReadPassword(string prompt)
{
Console.Write(prompt);
string password = "";
while (true)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
if (keyInfo.Key == ConsoleKey.Enter)
{
break;
}
if (keyInfo.Key == ConsoleKey.Backspace)
{
if (password.Length > 0)
{
password = password.Substring(0, password.Length - 1);
Console.Write("\b \b");
}
}
else if (keyInfo.KeyChar != '\u0000') // Only accept if the key maps to a Unicode character (e.g., ignore F1 or Home).
{
password += keyInfo.KeyChar;
Console.Write("*");
}
}
Console.WriteLine();
return password;
}
}
}
@@ -37,6 +37,8 @@ namespace Duplicati.CommandLine.RecoveryTool
{
try
{
Library.AutoUpdater.PreloadSettingsLoader.ConfigurePreloadSettings(ref _args, Library.AutoUpdater.PackageHelper.NamedExecutable.RecoveryTool);
var args = new List<string>(_args);
var tmpparsed = Library.Utility.FilterCollector.ExtractOptions(args);
var options = tmpparsed.Item1;
@@ -99,7 +101,7 @@ namespace Duplicati.CommandLine.RecoveryTool
return command(args, options, filter);
}
catch(Exception ex)
catch (Exception ex)
{
if (ex is Duplicati.Library.Interface.UserInformationException)
Console.WriteLine(ex.Message);
@@ -127,7 +129,7 @@ namespace Duplicati.CommandLine.RecoveryTool
if (!newfilter.Empty)
filter = newfilter;
foreach(KeyValuePair<String, String> keyvalue in opt)
foreach (KeyValuePair<String, String> keyvalue in opt)
options[keyvalue.Key] = keyvalue.Value;
cargs.AddRange(
@@ -135,7 +137,7 @@ namespace Duplicati.CommandLine.RecoveryTool
where !string.IsNullOrWhiteSpace(c) && !c.StartsWith("#", StringComparison.Ordinal) && !c.StartsWith("!", StringComparison.Ordinal) && !c.StartsWith("REM ", StringComparison.OrdinalIgnoreCase)
select c
);
return true;
}
catch (Exception e)
@@ -0,0 +1,22 @@
using System.CommandLine;
using System.CommandLine.Invocation;
namespace Duplicati.CommandLine.ServerUtil;
/// <summary>
/// Extensions for <see cref="Command"/>.
/// </summary>
public static class CommandExtensions
{
/// <summary>
/// Adds the missing WithHandler method to <see cref="Command"/>.
/// </summary>
/// <param name="command">The command to add the handler to.</param>
/// <param name="handler">The handler to add.</param>
/// <returns>The command with the handler added.</returns>
public static Command WithHandler(this Command command, ICommandHandler handler)
{
command.Handler = handler;
return command;
}
}
@@ -0,0 +1,28 @@
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
namespace Duplicati.CommandLine.ServerUtil.Commands;
public static class ChangePassword
{
public static Command Create() =>
new Command("change-password", "Changes the server password")
{
new Argument<string>("new-password", "The new password to use") {
Arity = ArgumentArity.ZeroOrOne
},
}
.WithHandler(CommandHandler.Create<Settings, string>(async (settings, newPassword) =>
{
// Ask for previous password first, if needed
var connection = await settings.GetConnection();
if (string.IsNullOrWhiteSpace(newPassword))
newPassword = HelperMethods.ReadPasswordFromConsole("Please provide the new password: ");
if (string.IsNullOrWhiteSpace(newPassword))
throw new UserReportedException("No password provided");
await connection.ChangePassword(newPassword);
}));
}
@@ -0,0 +1,49 @@
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
namespace Duplicati.CommandLine.ServerUtil.Commands;
public static class Import
{
public static Command Create() =>
new Command("import", "Import a backup configuration")
{
new Argument<FileInfo>("file", "The file to import, may be encrypted") {
Arity = ArgumentArity.ExactlyOne
},
new Argument<string>("passphrase", "The passphrase to use for decryption") {
Arity = ArgumentArity.ZeroOrOne
},
new Option<bool>(name: "--import-metadata", description: "Import metadata from the backup", getDefaultValue: () => false)
}
.WithHandler(CommandHandler.Create<Settings, FileInfo, string, bool>(async (settings, file, passphrase, importMetadata) =>
{
if (!file.Exists)
throw new UserReportedException($"File {file.FullName} does not exist");
Console.WriteLine($"Importing backup configuration from {file.FullName}");
if (IsEncrypted(file))
{
if (string.IsNullOrWhiteSpace(passphrase))
passphrase = HelperMethods.ReadPasswordFromConsole("The file is encrypted. Please provide the encryption password: ");
if (string.IsNullOrWhiteSpace(passphrase))
throw new UserReportedException("No password provided");
}
var connection = await settings.GetConnection();
var result = await connection.ImportBackup(file.FullName, passphrase, importMetadata);
Console.WriteLine($"Imported \"{result.Name}\" with ID {result.ID}");
}));
private static bool IsEncrypted(FileInfo file)
{
using var fs = file.OpenRead();
var header = new byte[3].AsSpan();
if (fs.Read(header) != 3)
return false;
return header.SequenceEqual("AES"u8);
}
}
@@ -0,0 +1,27 @@
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
namespace Duplicati.CommandLine.ServerUtil.Commands;
public static class ListBackups
{
public static Command Create() =>
new Command("list-backups", "List all backups")
.WithHandler(CommandHandler.Create<Settings>(async (settings) =>
{
var bks = await (await settings.GetConnection()).ListBackups();
if (!bks.Any())
{
Console.WriteLine("No backups found");
return;
}
foreach (var bk in bks)
{
Console.WriteLine($"{bk.ID}: {bk.Name}");
Console.WriteLine($" {bk.Description}");
Console.WriteLine();
}
}));
}
@@ -0,0 +1,18 @@
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
namespace Duplicati.CommandLine.ServerUtil.Commands;
public static class Login
{
public static Command Create() =>
new Command("login", "Logs in to the server")
.WithHandler(CommandHandler.Create<Settings>(async (settings) =>
{
Console.WriteLine("Logging in to the server");
await Connection.Connect(settings, true);
Console.WriteLine("Logged in, persistent token saved");
})
);
}
@@ -0,0 +1,13 @@
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
namespace Duplicati.CommandLine.ServerUtil.Commands;
public static class Logout
{
public static Command Create()
=> new Command("logout", "Logs out of the server")
.WithHandler(CommandHandler.Create<Settings>(async (settings) =>
await (await settings.GetConnection()).Logout(settings))
);
}
@@ -0,0 +1,24 @@
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
namespace Duplicati.CommandLine.ServerUtil.Commands;
public static class Pause
{
public static Command Create() =>
new Command("pause", "Pauses the server")
{
new Argument<string?>("duration", description: "The duration to pause the server for", getDefaultValue: () => null) {
Arity = ArgumentArity.ZeroOrOne
},
}
.WithHandler(CommandHandler.Create<Settings, string?>(async (settings, duration) =>
{
if (string.IsNullOrWhiteSpace(duration))
Console.WriteLine("Pausing the server indefinitely");
else
Console.WriteLine($"Pausing the server for {duration}");
await (await settings.GetConnection()).Pause(duration);
}));
}
@@ -0,0 +1,13 @@
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
namespace Duplicati.CommandLine.ServerUtil.Commands;
public static class Resume
{
public static Command Create()
=> new Command("resume", "Resumes the server")
.WithHandler(CommandHandler.Create<Settings>(async (settings) =>
await (await settings.GetConnection()).Resume())
);
}
@@ -0,0 +1,28 @@
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
namespace Duplicati.CommandLine.ServerUtil.Commands;
public static class RunBackup
{
public static Command Create() =>
new Command("run", "Runs a backup")
{
new Argument<string>("backup", "The backup to run, either ID or exact name (case-insensitive)") {
Arity = ArgumentArity.ExactlyOne
},
}
.WithHandler(CommandHandler.Create<Settings, string>(async (settings, backup) =>
{
var connection = await settings.GetConnection();
var matchingBackup = (await connection.ListBackups())
.FirstOrDefault(b => string.Equals(b.Name == backup, StringComparison.OrdinalIgnoreCase) || string.Equals(b.ID, backup));
if (matchingBackup == null)
throw new UserReportedException("No backup found with supplied ID or name");
Console.WriteLine($"Running backup {matchingBackup.Name} ({matchingBackup.ID})");
await connection.RunBackup(matchingBackup.ID);
}));
}
@@ -0,0 +1,402 @@
using System.Net.Http.Json;
using System.Text.Json;
namespace Duplicati.CommandLine.ServerUtil;
/// <summary>
/// Implementation of actions performed on the server.
/// </summary>
public class Connection
{
/// <summary>
/// The reported backup data
/// </summary>
/// <param name="ID">The ID of the backup</param>
/// <param name="Name">The name of the backup</param>
/// <param name="Description">The description of the backup</param>
/// <param name="Metadata">The metadata of the backup</param>
public sealed record BackupEntry(
string ID,
string Name,
string Description,
Dictionary<string, string>? Metadata
);
/// <summary>
/// The response backup data returned from the server
/// </summary>
/// <param name="Backup">The backup details</param>
private sealed record ResponseBackupEntry(ResponseBackupEntry.ResponseBackupDetailsEntry Backup)
{
/// <summary>
/// The response backup details entry
/// </summary>
/// <param name="ID">The ID of the backup</param>
/// <param name="Name">The name of the backup</param>
/// <param name="Description">The description of the backup</param>
/// <param name="DBPath">The path to the local database</param>
/// <param name="Metadata">The metadata of the backup</param>
public sealed record ResponseBackupDetailsEntry(
string ID,
string Name,
string? Description,
string? DBPath,
Dictionary<string, string>? Metadata
);
/// <summary>
/// Converts the response backup entry to a backup entry
/// </summary>
/// <returns>The backup entry</returns>
public BackupEntry ToBackupEntry()
=> new BackupEntry(Backup.ID, Backup.Name, Backup.Description ?? "", Backup.Metadata);
}
/// <summary>
/// The task entry
/// </summary>
/// <param name="TaskID">The ID of the task</param>
/// <param name="BackupID">The ID of the backup</param>
/// <param name="Operation">The operation of the task</param>
public sealed record TaskEntry(
long TaskID,
string BackupID,
string Operation
);
/// <summary>
/// The stop level
/// </summary>
public enum StopLevel
{
/// <summary>
/// Stop after the current file
/// </summary>
AfterCurrentFile,
/// <summary>
/// Stop now
/// </summary>
StopNow,
/// <summary>
/// Stop immediately
/// </summary>
Abort
}
/// <summary>
/// The HTTP client used to connect to the server
/// </summary>
private readonly HttpClient client;
/// <summary>
/// Initializes a new instance of the <see cref="Connection"/> class
/// </summary>
private Connection(HttpClient client)
{
this.client = client;
}
/// <summary>
/// Connects to the server
/// </summary>
/// <param name="settings">The settings to use for the connection</param>
/// <param name="obtainRefreshToken">Whether to obtain a refresh token</param>
/// <returns>The connection</returns>
public static async Task<Connection> Connect(Settings settings, bool obtainRefreshToken = false)
{
Console.WriteLine($"Connecting to {settings.HostUrl}...");
// Configure the client for requests
var client = new HttpClient()
{
BaseAddress = new Uri(settings.HostUrl + "api/v1/"),
};
// If we already have a refresh token, try that first
try
{
if (!string.IsNullOrWhiteSpace(settings.RefreshToken))
{
var (accessToken, refreshToken) = await LoginWithRefreshToken(client, settings.RefreshToken);
if (string.IsNullOrWhiteSpace(accessToken))
throw new InvalidOperationException("Failed to get access token");
if (string.IsNullOrWhiteSpace(refreshToken))
throw new InvalidOperationException("Failed to get refresh token");
(settings with { RefreshToken = refreshToken }).Save();
return CreateConnectionWithClient(client, accessToken);
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to use refresh token: {ex.Message}");
}
// If we can read the server database, try to create a signin token
try
{
var opts = new Dictionary<string, string>();
if (!string.IsNullOrWhiteSpace(settings.ServerDatafolder))
opts.Add("server-datafolder", settings.ServerDatafolder);
if (File.Exists(Path.Combine(Server.Program.GetDataFolderPath(opts), Server.Program.SERVER_DATABASE_FILENAME)))
{
string? cfg = null;
using (var connection = Duplicati.Server.Program.GetDatabaseConnection(opts))
cfg = connection.ApplicationSettings.JWTConfig;
if (!string.IsNullOrWhiteSpace(cfg))
{
var signinjwt = new WebserverCore.Middlewares.JWTTokenProvider(
JsonSerializer.Deserialize<WebserverCore.Middlewares.JWTConfig>(cfg)
?? throw new InvalidOperationException("Failed to deserialize JWTConfig")
).CreateSigninToken("server-cli");
var response = await client.PostAsync("auth/signin", JsonContent.Create(new { SigninToken = signinjwt, RememberMe = obtainRefreshToken }));
var (accessToken, refreshToken) = ParseAuthResponse(response);
if (string.IsNullOrWhiteSpace(accessToken))
throw new InvalidOperationException("Failed to get access token");
if (!string.IsNullOrWhiteSpace(refreshToken))
(settings with { RefreshToken = refreshToken }).Save();
return CreateConnectionWithClient(client, accessToken);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to obtain a signin token: {ex.Message}");
}
// Otherwise, we need a password to log in
try
{
// Try obtaining the password from the user
if (string.IsNullOrWhiteSpace(settings.Password))
settings = settings with { Password = HelperMethods.ReadPasswordFromConsole("Enter server password: ") };
if (string.IsNullOrWhiteSpace(settings.Password))
throw new UserReportedException("Password is required");
var (accessToken, refreshToken) = await LoginWithPassword(client, settings.Password, obtainRefreshToken);
if (string.IsNullOrWhiteSpace(accessToken))
throw new InvalidOperationException("Failed to get access token");
if (!string.IsNullOrWhiteSpace(refreshToken))
(settings with { RefreshToken = refreshToken }).Save();
return CreateConnectionWithClient(client, accessToken);
}
catch (Exception ex)
{
client.Dispose();
throw new UserReportedException($"Failed to connect to server: {ex.Message}", ex);
}
}
/// <summary>
/// Creates the connection and adds the authorization header
/// </summary>
/// <param name="client">The HTTP client</param>
/// <param name="accessToken">The access token</param>
/// <returns>The connection</returns>
private static Connection CreateConnectionWithClient(HttpClient client, string accessToken)
{
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {accessToken}");
return new Connection(client);
}
/// <summary>
/// Logs in with a password
/// </summary>
/// <param name="client">The HTTP client</param>
/// <param name="password">The password to use</param>
/// <param name="obtainRefreshToken">Whether to obtain a refresh token</param>
/// <returns>The access and refresh tokens</returns>
private static async Task<(string? AccessToken, string? RefreshToken)> LoginWithPassword(HttpClient client, string password, bool obtainRefreshToken)
=> ParseAuthResponse(
await client.PostAsync($"auth/login", JsonContent.Create(new { Password = password, RememberMe = obtainRefreshToken }))
);
/// <summary>
/// Logs in with a refresh token
/// </summary>
/// <param name="client">The HTTP client</param>
/// <param name="refreshToken">The refresh token to use</param>
/// <returns>The access and refresh tokens</returns>
private static async Task<(string? AccessToken, string? RefreshToken)> LoginWithRefreshToken(HttpClient client, string refreshToken)
=> ParseAuthResponse(await client.SendAsync(new HttpRequestMessage(HttpMethod.Post, "auth/refresh")
{
Headers = { { "Cookie", $"RefreshToken_{client.BaseAddress!.Port}={refreshToken}" } },
}));
/// <summary>
/// Parses the authentication response
/// </summary>
/// <param name="response">The response to parse</param>
/// <returns>The access and refresh tokens</returns>
private static (string? AccessToken, string? RefreshToken) ParseAuthResponse(HttpResponseMessage response)
{
response.EnsureSuccessStatusCode();
var json = JsonSerializer.Deserialize<Dictionary<string, string>>(response.Content.ReadAsStringAsync().Result)
?? throw new InvalidOperationException("Failed to parse response");
if (!json.TryGetValue("AccessToken", out var accessToken))
throw new InvalidOperationException("Failed to get access token");
response.Headers.TryGetValues("Set-Cookie", out var cookies);
var refreshToken = cookies?.SelectMany(c => c.Split(';')).FirstOrDefault(c => c.StartsWith($"RefreshToken_"))?.Split('=', 2)[1];
return (accessToken, refreshToken);
}
/// <summary>
/// Pauses the server
/// </summary>
/// <param name="duration">The duration to pause for</param>
/// <returns>The task</returns>
public async Task Pause(string? duration)
{
var query = string.IsNullOrWhiteSpace(duration) ? "" : $"?duration={Uri.EscapeDataString(duration)}";
var response = await client.PostAsync($"serverstate/pause{query}", null);
response.EnsureSuccessStatusCode();
}
/// <summary>
/// Resumes the server
/// </summary>
/// <returns>The task</returns>
public async Task Resume()
{
var response = await client.PostAsync($"serverstate/resume", null);
response.EnsureSuccessStatusCode();
}
/// <summary>
/// Lists the backups configured on the server
/// </summary>
/// <returns>The backups</returns>
public async Task<IEnumerable<BackupEntry>> ListBackups()
{
var response = await client.GetAsync("backups");
response.EnsureSuccessStatusCode();
return (JsonSerializer.Deserialize<IEnumerable<ResponseBackupEntry>>(await response.Content.ReadAsStringAsync())
?? throw new UserReportedException("Failed to parse response"))
.Select(x => x.ToBackupEntry())
.ToArray();
}
/// <summary>
/// Gets a backup by ID
/// </summary>
/// <param name="backupId">The ID of the backup</param>
/// <returns>The backup</returns>
public async Task<BackupEntry> GetBackup(string backupId)
{
var response = await client.GetAsync($"backup/{Uri.EscapeDataString(backupId)}");
response.EnsureSuccessStatusCode();
return (JsonSerializer.Deserialize<ResponseBackupEntry>(await response.Content.ReadAsStringAsync())
?? throw new UserReportedException("Failed to parse response"))
.ToBackupEntry();
}
/// <summary>
/// Runs a backup
/// </summary>
/// <param name="backupId">The ID of the backup</param>
/// <returns>The task</returns>
public async Task RunBackup(string backupId)
{
var response = await client.PostAsync($"backup/{Uri.EscapeDataString(backupId)}/run", null);
response.EnsureSuccessStatusCode();
}
/// <summary>
/// Lists the active tasks
/// </summary>
/// <returns>The tasks</returns>
public async Task<IEnumerable<TaskEntry>> ListTasks()
{
var response = await client.GetAsync("tasks");
response.EnsureSuccessStatusCode();
return JsonSerializer.Deserialize<IEnumerable<TaskEntry>>(await response.Content.ReadAsStringAsync())
?? throw new InvalidOperationException("Failed to parse response");
}
/// <summary>
/// Stops a task
/// </summary>
/// <param name="taskId">The ID of the task</param>
/// <param name="level">The level to stop at</param>
/// <returns>The task</returns>
public async Task StopTask(string taskId, StopLevel level = StopLevel.AfterCurrentFile)
{
var levelString = level switch
{
StopLevel.AfterCurrentFile => "stopaftercurrentfile",
StopLevel.StopNow => "stopnow",
StopLevel.Abort => "abort",
_ => throw new ArgumentOutOfRangeException(nameof(level)),
};
var response = await client.PostAsync($"task/{Uri.EscapeDataString(taskId)}/{levelString}", null);
response.EnsureSuccessStatusCode();
}
/// <summary>
/// Logs out of the server
/// </summary>
/// <param name="settings">The settings to use</param>
/// <returns>The task</returns>
public async Task Logout(Settings settings)
{
var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Post, "auth/refresh/logout")
{
Headers = { { "Cookie", $"RefreshToken_{client.BaseAddress!.Port}={settings.RefreshToken}" } }
});
response.EnsureSuccessStatusCode();
(settings with { RefreshToken = null }).Save();
}
/// <summary>
/// Changes the server password
/// </summary>
/// <param name="newPassword">The new password to use</param>
/// <returns>The task</returns>
public async Task ChangePassword(string newPassword)
{
var response = await client.PutAsync("serversetting/server-passphrase", JsonContent.Create(newPassword));
response.EnsureSuccessStatusCode();
}
/// <summary>
/// Imports a backup
/// </summary>
/// <param name="file">The file to import</param>
/// <param name="password">The password to use</param>
/// <param name="importMetadata">Whether to import metadata</param>
/// <returns>The backup</returns>
public async Task<BackupEntry> ImportBackup(string file, string? password, bool importMetadata)
{
var payload = JsonContent.Create(new
{
config = Convert.ToBase64String(await File.ReadAllBytesAsync(file)),
import_metadata = importMetadata,
passphrase = password,
direct = true
});
var response = await client.PostAsync("backups/import", payload);
response.EnsureSuccessStatusCode();
var json = JsonSerializer.Deserialize<Dictionary<string, string>>(await response.Content.ReadAsStringAsync())
?? throw new UserReportedException("Failed to parse response");
if (!json.TryGetValue("Id", out var id))
throw new UserReportedException("Added backup but failed to get from response");
return await GetBackup(id);
}
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Duplicati.CommandLine.ServerUtil.Implementation</AssemblyName>
<Copyright>Copyright © 2024 Team Duplicati, MIT license</Copyright>
<DefaultNamespace>Duplicati.CommandLine.ServerUtil</DefaultNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.CommandLine.NamingConventionBinder" Version="2.0.0-beta4.22272.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Server\Duplicati.Server.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,41 @@
namespace Duplicati.CommandLine.ServerUtil;
/// <summary>
/// Various helper methods.
/// </summary>
public static class HelperMethods
{
/// <summary>
/// Reads a password from the console.
/// </summary>
/// <param name="prompt">The prompt to display.</param>
public static string ReadPasswordFromConsole(string prompt)
{
Console.Write(prompt);
string password = "";
while (true)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
if (keyInfo.Key == ConsoleKey.Enter)
{
break;
}
if (keyInfo.Key == ConsoleKey.Backspace)
{
if (password.Length > 0)
{
password = password.Substring(0, password.Length - 1);
Console.Write("\b \b");
}
}
else if (keyInfo.KeyChar != '\u0000') // Only accept if the key maps to a Unicode character (e.g., ignore F1 or Home).
{
password += keyInfo.KeyChar;
Console.Write("*");
}
}
Console.WriteLine();
return password;
}
}
@@ -0,0 +1,62 @@
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Parsing;
using Duplicati.CommandLine.ServerUtil.Commands;
namespace Duplicati.CommandLine.ServerUtil;
/// <summary>
/// The entry point of the application
/// </summary>
public static class Program
{
/// <summary>
/// Invokes the builder
/// </summary>
/// <param name="args"></param>
/// <returns>The return code</returns>
public static Task<int> Main(string[] args)
{
Library.AutoUpdater.PreloadSettingsLoader.ConfigurePreloadSettings(ref args, Library.AutoUpdater.PackageHelper.NamedExecutable.ServerUtil);
var rootCmd = new RootCommand("Server CLI tool for Duplicati")
{
Pause.Create(),
Resume.Create(),
ListBackups.Create(),
RunBackup.Create(),
Login.Create(),
ChangePassword.Create(),
Logout.Create(),
Import.Create(),
};
rootCmd = SettingsBinder.AddGlobalOptions(rootCmd);
return new CommandLineBuilder(rootCmd)
.UseDefaults()
.UseExceptionHandler((ex, context) =>
{
if (ex is UserReportedException ure)
{
Console.WriteLine(ure.Message);
context.ExitCode = 2;
}
else
{
Console.WriteLine(ex.ToString());
context.ExitCode = 1;
}
})
.AddMiddleware(async (context, next) =>
{
// Inject settings with custom binder
if (context.ParseResult.CommandResult?.Command is Command cmd)
context.BindingContext.AddService(_ => SettingsBinder.GetSettings(context.BindingContext));
await next(context);
})
.Build()
.InvokeAsync(args);
}
}
@@ -0,0 +1,115 @@
using System.Text.Json;
using Duplicati.Library.Encryption;
using Duplicati.Library.Main;
namespace Duplicati.CommandLine.ServerUtil;
/// <summary>
/// Settings instance for the server utility.
/// </summary>
/// <param name="Password">The commandline password</param>
/// <param name="RefreshToken">The saved refresh token</param>
/// <param name="HostUrl">The host url to connect to</param>
/// <param name="ServerDatafolder">The server datafolder for password-free connections</param>
/// <param name="SettingsFile">The settings file where data is loaded/saved</param>
public sealed record Settings(
string? Password,
string? RefreshToken,
Uri HostUrl,
string? ServerDatafolder,
string SettingsFile
)
{
/// <summary>
/// The JSON serialized settings for a single host
/// </summary>
/// <param name="RefreshToken">Encrypted refresh token</param>
/// <param name="HostUrl">The host url to connect to</param>
/// <param name="ServerDatafolder">The server datafolder, if any</param>
private sealed record PersistedSettings(
string? RefreshToken,
Uri HostUrl,
string? ServerDatafolder
);
private static string GetDefaultStorageFolder(string filename)
{
var folder = DatabaseLocator.GetDefaultStorageFolderWithDebugSupport(filename);
if (!Directory.Exists(folder))
Directory.CreateDirectory(folder);
return folder;
}
/// <summary>
/// Loads the settings from the settings file
/// </summary>
/// <param name="password">The password to use</param>
/// <param name="hostUrl">The host URL to use</param>
/// <param name="serverDataFolder">The server data folder to use</param>
/// <param name="settingsFile">The settings file to use</param>
/// <returns>The loaded settings</returns>
public static Settings Load(string? password, Uri? hostUrl, string? serverDataFolder, string settingsFile)
{
hostUrl ??= new Uri("http://localhost:8200");
if (string.IsNullOrWhiteSpace(serverDataFolder))
serverDataFolder = GetDefaultStorageFolder("Duplicati-server.sqlite");
if (!string.IsNullOrWhiteSpace(settingsFile) && !Path.IsPathRooted(settingsFile))
settingsFile = Path.Combine(GetDefaultStorageFolder(settingsFile), settingsFile);
var persistedSettings = LoadSettings(settingsFile)
.FirstOrDefault(x => x.HostUrl == hostUrl);
return new Settings(
password,
persistedSettings?.RefreshToken,
hostUrl,
serverDataFolder,
settingsFile
);
}
/// <summary>
/// Saves the settings to the settings file
/// </summary>
public void Save()
{
if (!string.IsNullOrWhiteSpace(RefreshToken) && EncryptedFieldHelper.IsCurrentKeyBlacklisted)
Console.WriteLine("Warning: The current encryption key is blacklisted and cannot be used, saving login token without encryption");
File.WriteAllText(SettingsFile, JsonSerializer.Serialize(LoadSettings(SettingsFile)
.Where(x => x.HostUrl != HostUrl)
.Append(new PersistedSettings(RefreshToken, HostUrl, ServerDatafolder))
.Select(x => x with
{
RefreshToken = string.IsNullOrWhiteSpace(x.RefreshToken) || EncryptedFieldHelper.IsCurrentKeyBlacklisted
? x.RefreshToken
: EncryptedFieldHelper.Encrypt(x.RefreshToken)
})
));
}
/// <summary>
/// Gets a connection to the server
/// </summary>
/// <returns>The connection</returns>
public Task<Connection> GetConnection()
{
return Connection.Connect(this);
}
/// <summary>
/// Loads the settings from the settings file
/// </summary>
/// <param name="filename">The filename to load</param>
/// <returns>The loaded settings</returns>
private static List<PersistedSettings> LoadSettings(string filename)
{
if (File.Exists(filename))
return (JsonSerializer.Deserialize<List<PersistedSettings>>(File.ReadAllText(filename)) ?? [])
.Select(x => x with { RefreshToken = EncryptedFieldHelper.Decrypt(x.RefreshToken) })
.ToList();
return [];
}
}
@@ -0,0 +1,63 @@
using System.CommandLine;
using System.CommandLine.Binding;
namespace Duplicati.CommandLine.ServerUtil;
/// <summary>
/// Binds settings from command line options.
/// </summary>
public class SettingsBinder : BinderBase<Settings>
{
/// <summary>
/// The password option.
/// </summary>
public static readonly Option<string?> passwordOption = new Option<string?>("--password", description: "The password to use", getDefaultValue: () => null);
/// <summary>
/// The host URL option.
/// </summary>
public static readonly Option<Uri> hostUrlOption = new Option<Uri>("--hosturl", description: "The host URL to use", getDefaultValue: () => new Uri("http://localhost:8200"));
/// <summary>
/// The server datafolder option.
/// </summary>
public static readonly Option<DirectoryInfo?> serverDatafolderOption = new Option<DirectoryInfo?>("--server-datafolder", description: "The server datafolder to use for locating the database", getDefaultValue: () => null);
/// <summary>
/// The settings file option.
/// </summary>
public static readonly Option<FileInfo?> settingsFileOption = new Option<FileInfo?>("--settings-file", description: "The settings file to use", getDefaultValue: () => null);
/// <summary>
/// Adds global options to the root command.
/// </summary>
/// <param name="rootCommand">The root command to add the options to.</param>
/// <returns>The root command with the options added.</returns>
public static RootCommand AddGlobalOptions(RootCommand rootCommand)
{
rootCommand.AddGlobalOption(passwordOption);
rootCommand.AddGlobalOption(hostUrlOption);
rootCommand.AddGlobalOption(serverDatafolderOption);
rootCommand.AddGlobalOption(settingsFileOption);
return rootCommand;
}
/// <summary>
/// Gets the settings instance from the binding context.
/// </summary>
/// <param name="bindingContext">The binding context to get the settings from.</param>
/// <returns>The settings instance.</returns>
public static Settings GetSettings(BindingContext bindingContext) =>
Settings.Load(
bindingContext.ParseResult.GetValueForOption(passwordOption),
bindingContext.ParseResult.GetValueForOption(hostUrlOption),
bindingContext.ParseResult.GetValueForOption(serverDatafolderOption)?.FullName,
bindingContext.ParseResult.GetValueForOption(settingsFileOption)?.FullName ?? "settings.json"
);
/// <summary>
/// Gets the bound value.
/// </summary>
/// <param name="bindingContext">The binding context to get the value from.</param>
/// <returns>The bound value.</returns>
protected override Settings GetBoundValue(BindingContext bindingContext) =>
GetSettings(bindingContext);
}
@@ -0,0 +1,9 @@
namespace Duplicati.CommandLine.ServerUtil;
/// <summary>
/// An exception that should be reported to the user.
/// </summary>
/// <param name="message">The message of the exception</param>
/// <param name="innerException">The inner exception</param>
[Serializable]
public class UserReportedException(string message, Exception? innerException = null) : Exception(message, innerException);
@@ -73,13 +73,13 @@ namespace Duplicati.GUI.TrayIcon
if (!Duplicati.Server.Program.ServerStartedEvent.WaitOne(TimeSpan.FromSeconds(100), true))
{
if (m_runnerException != null)
throw m_runnerException;
throw new Duplicati.Library.Interface.UserInformationException("Server crashed on startup", "HostedStartupErrorCrash", m_runnerException);
else
throw new Duplicati.Library.Interface.UserInformationException("Hosted server startup timed out", "HostedStartupError");
}
if (m_runnerException != null)
throw m_runnerException;
throw new Duplicati.Library.Interface.UserInformationException("Server crashed on startup", "HostedStartupErrorCrash", m_runnerException);
}
public void Dispose()
@@ -279,7 +279,7 @@ namespace Duplicati.GUI.TrayIcon
private T PerformRequest<T>(string method, string urlfragment, string body, TimeSpan? timeout)
{
if (string.IsNullOrWhiteSpace(m_accesstoken))
if (string.IsNullOrWhiteSpace(m_accesstoken) && !urlfragment.StartsWith("/auth/"))
ObtainAccessToken();
var hasTriedPassword = false;
@@ -373,7 +373,7 @@ namespace Duplicati.GUI.TrayIcon
string signinjwt = null;
// If we host the server, issue the token from the service
if (FIXMEGlobal.IsServerStarted || m_passwordSource == Program.PasswordSource.HostedServer)
if (FIXMEGlobal.IsServerStarted && m_passwordSource == Program.PasswordSource.HostedServer)
signinjwt = FIXMEGlobal.Provider.GetRequiredService<IJWTTokenProvider>().CreateSigninToken("trayicon");
// If we have database access, grab the issuer key from the db and issue a token
+33 -14
View File
@@ -21,6 +21,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using Duplicati.Library.Interface;
using Duplicati.Server;
@@ -61,6 +62,7 @@ namespace Duplicati.GUI.TrayIcon
[STAThread]
public static int Main(string[] _args)
{
Library.AutoUpdater.PreloadSettingsLoader.ConfigurePreloadSettings(ref _args, Library.AutoUpdater.PackageHelper.NamedExecutable.TrayIcon);
List<string> args = new List<string>(_args);
Dictionary<string, string> options = Library.Utility.CommandLineParser.ExtractOptions(args);
@@ -128,23 +130,26 @@ namespace Duplicati.GUI.TrayIcon
}
else if (Library.Utility.Utility.ParseBoolOption(options, READCONFIGFROMDB_OPTION))
{
databaseConnection = Server.Program.GetDatabaseConnection(options);
if (databaseConnection != null)
if (File.Exists(Path.Combine(Server.Program.GetDataFolderPath(options), Server.Program.SERVER_DATABASE_FILENAME)))
{
disableTrayIconLogin = databaseConnection.ApplicationSettings.DisableTrayIconLogin;
databaseConnection = Server.Program.GetDatabaseConnection(options);
var cert = databaseConnection.ApplicationSettings.ServerSSLCertificate;
var scheme = "http";
if (cert != null && cert.HasPrivateKey)
scheme = "https";
serverURL = new UriBuilder(serverURL)
if (databaseConnection != null)
{
Port = databaseConnection.ApplicationSettings.LastWebserverPort == -1 ? serverURL.Port : databaseConnection.ApplicationSettings.LastWebserverPort,
Scheme = scheme
}.Uri;
disableTrayIconLogin = databaseConnection.ApplicationSettings.DisableTrayIconLogin;
var cert = databaseConnection.ApplicationSettings.ServerSSLCertificate;
var scheme = "http";
if (cert != null && cert.HasPrivateKey)
scheme = "https";
serverURL = new UriBuilder(serverURL)
{
Port = databaseConnection.ApplicationSettings.LastWebserverPort == -1 ? serverURL.Port : databaseConnection.ApplicationSettings.LastWebserverPort,
Scheme = scheme
}.Uri;
}
}
}
@@ -154,9 +159,23 @@ namespace Duplicati.GUI.TrayIcon
if (options.TryGetValue(WebServerLoader.OPTION_WEBSERVICE_PASSWORD, out pwd))
password = pwd;
// Let the user specify the port, if they are not providing a hosturl
if (!options.ContainsKey(HOSTURL_OPTION) && options.TryGetValue(WebServerLoader.OPTION_PORT, out var portString) && int.TryParse(portString, out var port))
serverURL = new UriBuilder(serverURL) { Port = port }.Uri;
if (options.TryGetValue(HOSTURL_OPTION, out var url))
serverURL = new Uri(url);
if (string.IsNullOrWhiteSpace(password) && databaseConnection == null && hosted == null)
{
Console.WriteLine($@"
When running the TrayIcon without a hosted server, you must provide the server password via the option --{WebServerLoader.OPTION_WEBSERVICE_PASSWORD}=<password>.
If the TrayIcon instance has read access to the server database, you can also or use the option --{READCONFIGFROMDB_OPTION}, possibly with --server-datafolder=<path>.
No password provided, unable to connect to server, exiting");
return 1;
}
StartTray(_args, options, hosted, password);
return 0;
@@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Duplicati.Library.Common;
using Duplicati.Library.Utility;
namespace Duplicati.Library.AutoUpdater
{
@@ -58,9 +53,13 @@ namespace Duplicati.Library.AutoUpdater
/// </summary>
Snapshots,
/// <summary>
/// The configuration importer
/// The server utility
/// </summary>
ConfigurationImporter
ServerUtil,
/// <summary>
/// The service wrapping the server
/// </summary>
Service
}
@@ -83,7 +82,8 @@ namespace Duplicati.Library.AutoUpdater
NamedExecutable.BackendTester => OperatingSystem.IsWindows() ? "Duplicati.CommandLine.BackendTester.exe" : "duplicati-backend-tester",
NamedExecutable.SharpAESCrypt => OperatingSystem.IsWindows() ? "Duplicati.CommandLine.SharpAESCrypt.exe" : "duplicati-aescrypt",
NamedExecutable.Snapshots => OperatingSystem.IsWindows() ? "Duplicati.CommandLine.Snapshots.exe" : "duplicati-snapshots",
NamedExecutable.ConfigurationImporter => OperatingSystem.IsWindows() ? "Duplicati.CommandLine.ConfigurationImporter.exe" : "duplicati-configuration-importer",
NamedExecutable.ServerUtil => OperatingSystem.IsWindows() ? "Duplicati.CommandLine.ServerUtil.exe" : "duplicati-server-util",
NamedExecutable.Service => OperatingSystem.IsWindows() ? "Duplicati.Service.exe" : "duplicati-service",
_ => throw new ArgumentException($"Named executable not known: {exe}", nameof(exe))
};
@@ -0,0 +1,314 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
namespace Duplicati.Library.AutoUpdater;
/// <summary>
/// Utility class for loading the preload settings
/// </summary>
public static class PreloadSettingsLoader
{
/// <summary>
/// The environment variable to specify the preload settings file
/// </summary>
private const string PreloadSettingsEnvVar = "DUPLICATI_PRELOAD_SETTINGS";
/// <summary>
/// The environment variable to enable debug output for preload settings
/// </summary>
private const string PreloadSettingsDebugEnvVar = "DUPLICATI_PRELOAD_SETTINGS_DEBUG";
/// <summary>
/// The marker for any executable
/// </summary>
private const string AnyExecutableMarker = "*";
/// <summary>
/// Cached value for toggling debug code
/// </summary>
private static readonly bool PreloadDebug = !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(PreloadSettingsDebugEnvVar));
/// <summary>
/// The preload paths to search for settings in.
/// Each path is checked and applied to obtain the final settings.
/// Later paths take precedence over earlier ones, so the env variable is most specific.
/// These are statically loaded so the preload cannot change the settings after startup.
/// </summary>
private static readonly string[] PreloadPaths = new string[]
{
// The default path for preload settings
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"Duplicati",
"preload.json"
),
// The path for preload settings with the install directory
Path.Combine(
Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) ?? "",
"preload.json"
),
// The path for preload settings specified with an environment variable
Environment.GetEnvironmentVariable(PreloadSettingsEnvVar) ?? "",
}
.Where(x => !string.IsNullOrEmpty(x))
.ToArray();
/// <summary>
/// Configures the preload settings for the given executable
/// </summary>
/// <param name="arguments">The source commandline arguments</param>
/// <param name="executable">The executable to match</param>
public static void ConfigurePreloadSettings(ref string[] arguments, PackageHelper.NamedExecutable executable)
=> ConfigurePreloadSettings(ref arguments, executable, out _);
/// <summary>
/// Configures the preload settings for the given executable
/// </summary>
/// <param name="arguments">The source commandline arguments</param>
/// <param name="executable">The executable to match</param>
/// <param name="dbsettings">The database settings</param>
public static void ConfigurePreloadSettings(ref string[] arguments, PackageHelper.NamedExecutable executable, out Dictionary<string, string?> dbsettings)
{
var (env, args, db) = GetExecutableMergedSettings(executable);
dbsettings = db;
ApplyEnvironmentVariables(env);
ApplyCommandLineArguments(ref arguments, args);
}
/// <summary>
/// Gets the argument name from the given argument
/// </summary>
/// <param name="arg">The argument to get the name from</param>
/// <returns>The argument name</returns>
private static string GetArgumentName(string arg)
=> arg.Split('=', 2)[0];
/// <summary>
/// Gets the merged settings for the given executable
/// </summary>
/// <param name="executable">The executable to get settings for</param>
/// <returns>The merged settings</returns>
private static (Dictionary<string, string> env, List<string> args, Dictionary<string, string?> db) GetExecutableMergedSettings(PackageHelper.NamedExecutable executable)
{
// Collect settings in generic and specific dictionaries
// The executable-specific settings take precedence over the generic ones,
// but the loading order is used so that the most specific file is loaded last
var env_generic = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
var env_specific = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
var args_generic = new List<string>();
var args_specific = new List<string>();
var db_generic = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
var db_specific = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
var exename = MapExecutableName(executable);
void MergeDicts(Dictionary<string, string?> target, Dictionary<string, string?>? source)
{
if (source != null)
foreach (var kvp in source)
target[kvp.Key] = kvp.Value;
}
foreach (var path in PreloadPaths)
{
if (!Path.IsPathRooted(path))
{
if (PreloadDebug)
Console.WriteLine($"Preload settings path is not rooted, ignoring: {path}");
continue;
}
if (!File.Exists(path))
{
if (PreloadDebug)
Console.WriteLine($"Preload settings file does not exist, ignoring: {path}");
continue;
}
var settings = LoadSettings(path);
if (settings == null)
continue;
if (settings.db != null)
{
if (settings.db.TryGetValue(AnyExecutableMarker, out var entry))
MergeDicts(db_generic, entry);
if (exename != AnyExecutableMarker && settings.db.TryGetValue(exename, out entry))
MergeDicts(db_specific, entry);
}
if (settings.env != null)
{
if (settings.env.TryGetValue(AnyExecutableMarker, out var entry))
MergeDicts(env_generic, entry);
if (exename != AnyExecutableMarker && settings.env.TryGetValue(exename, out entry))
MergeDicts(env_specific, entry);
}
if (settings.args != null)
{
if (settings.args.TryGetValue(AnyExecutableMarker, out var entry))
args_generic.AddRange(entry ?? []);
if (exename != AnyExecutableMarker && settings.args.TryGetValue(exename, out entry))
args_specific.AddRange(entry ?? []);
}
}
// Merge specific settings into generic ones
foreach (var kvp in db_specific)
db_generic[kvp.Key] = kvp.Value;
foreach (var kvp in env_specific)
env_generic[kvp.Key] = kvp.Value;
args_generic.AddRange(args_specific);
// Remove duplicates from the arguments, preserve order
var mapped = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var args = new List<string>();
foreach (var value in args_generic)
{
var argname = GetArgumentName(value);
if (mapped.TryGetValue(argname, out var index))
args.RemoveAt(index);
mapped[argname] = args.Count;
args.Add(value);
}
var env = env_generic.ToDictionary(x => x.Key, x => x.Value ?? "");
return (env, args, db_generic);
}
/// <summary>
/// Applies loaded environment variables, but does not overwrite existing ones
/// </summary>
/// <param name="env">The environment variables to apply</param>
private static void ApplyEnvironmentVariables(Dictionary<string, string> env)
{
var current = Environment.GetEnvironmentVariables();
foreach (var kvp in env)
if (!current.Contains(kvp.Key))
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value ?? "");
}
/// <summary>
/// Applies loaded commandline arguments, but does not overwrite existing ones
/// </summary>
/// <param name="arguments">The source commandline arguments</param>
/// <param name="args">The arguments to apply</param>
private static void ApplyCommandLineArguments(ref string[] arguments, List<string> args)
{
arguments ??= [];
var existing = new HashSet<string>(arguments.Select(GetArgumentName), StringComparer.OrdinalIgnoreCase);
var result = arguments.ToList();
foreach (var value in args)
if (!existing.Contains(GetArgumentName(value)))
result.Add(value);
arguments = result.ToArray();
}
/// <summary>
/// Loads the settings from the given path
/// </summary>
/// <param name="path">The path to load settings from</param>
/// <returns>The loaded settings, or null if an error occurred</returns>
private static PreloadSettingsRoot? LoadSettings(string path)
{
try
{
var result = JsonSerializer.Deserialize<PreloadSettingsRoot>(File.ReadAllText(path));
if (PreloadDebug)
{
if (result == null)
{
Console.WriteLine($"Loaded empty preload settings from {path}");
return null;
}
var jsData = JsonSerializer.Deserialize<JsonElement>(File.ReadAllText(path));
var unmatched_keys = jsData.EnumerateObject().Select(x => x.Name).Except(["db", "env", "args"]);
if (unmatched_keys.Any())
Console.WriteLine($"Unexpected key(s) in preload settings: {string.Join(", ", unmatched_keys)}");
Console.WriteLine($"Loaded preload settings from {path}");
var allowedSources = Enum.GetValues<PackageHelper.NamedExecutable>()
.Select(MapExecutableName)
.Append(AnyExecutableMarker)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var unmatched_db = result.db?.Keys.Where(x => !allowedSources.Contains(x)) ?? [];
var unmatched_env = result.env?.Keys.Where(x => !allowedSources.Contains(x)) ?? [];
var unmatched_args = result.args?.Keys.Where(x => !allowedSources.Contains(x)) ?? [];
if (unmatched_db.Any())
Console.WriteLine($"Found unknown executable name(s) in db preload settings: {string.Join(", ", unmatched_db)}");
if (unmatched_env.Any())
Console.WriteLine($"Found unknown executable name(s) in env preload settings: {string.Join(", ", unmatched_env)}");
if (unmatched_args.Any())
Console.WriteLine($"Found unknown executable name(s) in args preload settings: {string.Join(", ", unmatched_args)}");
}
return result;
}
catch (Exception ex)
{
// Logging is usually not set up at this point
if (PreloadDebug)
Console.WriteLine($"Failed to load preload settings from {path}: {ex}");
return null;
}
}
/// <summary>
/// Maps the executable name to the preload settings key
/// </summary>
/// <param name="exe">The executable to map</param>
/// <returns>The mapped name</returns>
private static string MapExecutableName(PackageHelper.NamedExecutable exe)
=> exe switch
{
PackageHelper.NamedExecutable.TrayIcon => "tray",
PackageHelper.NamedExecutable.CommandLine => "cli",
PackageHelper.NamedExecutable.AutoUpdater => "autoupdater",
PackageHelper.NamedExecutable.Server => "server",
PackageHelper.NamedExecutable.WindowsService => "winservice",
PackageHelper.NamedExecutable.BackendTool => "backendtool",
PackageHelper.NamedExecutable.RecoveryTool => "recoverytool",
PackageHelper.NamedExecutable.BackendTester => "backendtester",
PackageHelper.NamedExecutable.SharpAESCrypt => "aescrypt",
PackageHelper.NamedExecutable.Snapshots => "snapshots",
PackageHelper.NamedExecutable.ServerUtil => "serverutil",
PackageHelper.NamedExecutable.Service => "service",
_ => AnyExecutableMarker,
};
/// <summary>
/// JSON root object for preload settings
/// </summary>
/// <param name="db">The database settings</param>
/// <param name="env">The environment variables</param>
/// <param name="args">The executable settings</param>
private sealed record PreloadSettingsRoot(
Dictionary<string, Dictionary<string, string?>>? db,
Dictionary<string, Dictionary<string, string?>>? env,
Dictionary<string, List<string>>? args
);
}
@@ -30,9 +30,9 @@ namespace Duplicati.Library.Backend.AlternativeFTP
public static string Description { get { return LC.L(@"This backend can read and write data to an FTP based backend using an alternative FTP client. Allowed formats are ""aftp://hostname/folder"" and ""aftp://username:password@hostname/folder""."); } }
public static string DisplayName { get { return LC.L(@"Alternative FTP"); } }
public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string DescriptionLogToConsoleLong { get { return LC.L(@"Use this option to log FTP dialog to terminal console for debugging purposes."); } }
public static string DescriptionLogToConsoleShort { get { return LC.L(@"Log FTP dialog to terminal console"); } }
public static string DescriptionLogPrivateInfoToConsoleLong { get { return LC.L(@"Use this option to log FTP PRIVATE info (username, password) to console for debugging purposes (DO NOT POST THIS TO THE INTERNET!)"); } }
@@ -35,8 +35,8 @@ namespace Duplicati.Library.Backend.AzureBlob.Strings {
public static string SasTokenDescriptionShort { get { return LC.L(@"The SAS token"); } }
public static string NoAccessKeyOrSasToken { get { return LC.L(@"No Azure access key or SAS token given"); } }
public static string AuthPasswordDescriptionLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string AuthPasswordDescriptionShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string AuthPasswordDescriptionShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string AuthUsernameDescriptionLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string AuthUsernameDescriptionShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string AuthUsernameDescriptionShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
}
}
@@ -29,9 +29,9 @@ namespace Duplicati.Library.Backend.Strings {
public static string B2accountidDescriptionLong { get { return LC.L(@"B2 Cloud Storage Account ID can be obtained after logging into your Backblaze account. This can also be supplied through the option --{0}.", "auth-username"); } }
public static string B2accountidDescriptionShort { get { return LC.L(@"B2 Cloud Storage Account ID"); } }
public static string AuthPasswordDescriptionLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string AuthPasswordDescriptionShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string AuthPasswordDescriptionShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string AuthUsernameDescriptionLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string AuthUsernameDescriptionShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string AuthUsernameDescriptionShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string NoB2KeyError { get { return LC.L(@"No B2 Cloud Storage Application Key given"); } }
public static string NoB2UserIDError { get { return LC.L(@"No B2 Cloud Storage Account ID given"); } }
public static string B2createbuckettypeDescriptionLong { get { return LC.L(@"By default, a private bucket is created. Use this option to set the bucket type. Refer to the B2 documentation for allowed types."); } }
@@ -26,15 +26,15 @@ namespace Duplicati.Library.Backend.Strings {
public static string DescriptionAuthenticationURLLong_v2(string optionname) { return LC.L(@"CloudFiles use different servers for authentication based on where the account resides. Use this option to set an alternate authentication URL. This option overrides --{0}.", optionname); }
public static string DescriptionAuthenticationURLShort { get { return LC.L(@"Provide another authentication URL"); } }
public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string DescriptionPasswordLong { get { return LC.L(@"The API Access Key used to authenticate with CloudFiles."); } }
public static string DescriptionPasswordShort { get { return LC.L(@"Supplies the access key used to connect to the server"); } }
public static string DescriptionPasswordShort { get { return LC.L(@"Supply the access key used to connect to the server"); } }
public static string DescriptionUKAccountLong(string optionname, string optionvalue) { return LC.L(@"Duplicati will assume that the credentials given are for a US account. Use this option if the account is a UK based account. Note that this is equivalent to setting --{0}={1}.", optionname, optionvalue); }
public static string DescriptionUKAccountShort { get { return LC.L(@"Use a UK account"); } }
public static string DescriptionUsernameLong { get { return LC.L(@"The username used to authenticate with CloudFiles."); } }
public static string DescriptionUsernameShort { get { return LC.L(@"Supplies the username used to authenticate with CloudFiles"); } }
public static string DescriptionUsernameShort { get { return LC.L(@"Supply the username used to authenticate with CloudFiles"); } }
public static string ETagVerificationError { get { return LC.L(@"MD5 Hash (ETag) verification failed"); } }
public static string FileDeleteError { get { return LC.L(@"Failed to delete file"); } }
public static string FileUploadError { get { return LC.L(@"Failed to upload file"); } }
+5 -5
View File
@@ -27,15 +27,15 @@ namespace Duplicati.Library.Backend.Strings {
public static string Description { get { return LC.L(@"This backend can read and write data to an FTP based backend. Allowed formats are ""ftp://hostname/folder"" and ""ftp://username:password@hostname/folder""."); } }
public static string DisplayName { get { return LC.L(@"FTP"); } }
public static string DescriptionFTPActiveLong { get { return LC.L(@"Activate this option to make the FTP connection in active mode. Even if the option --{0} is also set, the connection will be made in active mode.", "ftp-passive"); } }
public static string DescriptionFTPActiveShort { get { return LC.L(@"Toggles the FTP connections method"); } }
public static string DescriptionFTPActiveShort { get { return LC.L(@"Toggle the FTP connections method"); } }
public static string DescriptionFTPPassiveLong { get { return LC.L(@"Activate this option to make the FTP connection in passive mode, which works better with some firewalls. If the option --{0} is set, this option is ignored.", "ftp-regular"); } }
public static string DescriptionFTPPassiveShort { get { return LC.L(@"Toggles the FTP connections method"); } }
public static string DescriptionFTPPassiveShort { get { return LC.L(@"Toggle the FTP connections method"); } }
public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string DescriptionUseSSLLong { get { return LC.L(@"Use this option to communicate using Secure Socket Layer (SSL) over ftp (ftps)."); } }
public static string DescriptionUseSSLShort { get { return LC.L(@"Instructs Duplicati to use an SSL (ftps) connection"); } }
public static string DescriptionUseSSLShort { get { return LC.L(@"Instruct Duplicati to use an SSL (ftps) connection"); } }
public static string DescriptionDisableUploadVerifyLong { get { return LC.L(@"To protect against network failures, every upload will be attempted verified. Use this option to disable this verification to make the upload faster but less reliable."); } }
public static string DescriptionDisableUploadVerifyShort { get { return LC.L(@"Disable upload verification"); } }
public static string MissingFolderError(string foldername, string message) { return LC.L(@"The folder {0} was not found, message: {1}", foldername, message); }
+2 -2
View File
@@ -28,9 +28,9 @@ namespace Duplicati.Library.Backend.Strings {
public static string AlternateTargetPathsLong(string optionname, char pathseparator) { return LC.L(@"This option allows multiple targets to be specified. The primary target path is placed before the list of paths supplied with this option. Before starting the backup, each folder in the list is checked for existence and optionally the presence of the marker file supplied by --{0}. The first existing path that optionally contains the marker file is then used as the destination. Multiple destinations are separated with a ""{1}"". On Windows, the path may be a UNC path, and the drive letter may be substituted with an asterisk (*), e.g: ""*:\backup"", which will examine all drive letters. If a username and password is supplied, the same credentials are used for all destinations.", optionname, pathseparator); }
public static string AlternateTargetPathsShort { get { return LC.L(@"A list of secondary target paths"); } }
public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string FolderMissingError(string foldername) { return LC.L(@"The folder {0} does not exist", foldername); }
public static string NoDestinationWithMarkerFileError(string markername, string[] folders) { return LC.L(@"The marker file ""{0}"" was not found in any of the examined destinations: {1}", markername, string.Join(", ", folders)); }
public static string UseMoveForPutLong { get { return LC.L(@"When storing the file, the standard operation is to copy the file and delete the original. This sequence ensures that the operation can be retried if something goes wrong. Activating this option may cause the retry operation to fail. This option has no effect unless the option --{0} is activated.", "disable-streaming-transfers"); } }
@@ -70,7 +70,7 @@ namespace Duplicati.Library.Backend.GoogleServices
public string DisplayName { get { return LC.L("Google Cloud Storage configuration module"); } }
public string Description { get { return LC.L("Exposes Google Cloud Storage configuration as a web module"); } }
public string Description { get { return LC.L("Expose Google Cloud Storage configuration as a web module"); } }
public System.Collections.Generic.IList<ICommandLineArgument> SupportedCommands
@@ -78,7 +78,7 @@ namespace Duplicati.Library.Backend.GoogleServices
get
{
return new List<ICommandLineArgument>([
new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provides different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provide different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
]);
}
@@ -32,12 +32,12 @@ namespace Duplicati.Library.Backend.Strings
public static string AuthidShort { get { return LC.L(@"The authorization code"); } }
public static string LocationDescriptionLong(string regions) { return LC.L(@"This option is only used when creating new buckets. Use this option to change what region the data is stored in. Charges vary with bucket location. Known bucket locations:
{0}", regions); }
public static string LocationDescriptionShort { get { return LC.L(@"Specifies location option for creating a bucket"); } }
public static string LocationDescriptionShort { get { return LC.L(@"Specify location option for creating a bucket"); } }
public static string StorageclassDescriptionLong(string classes) { return LC.L(@"This option is only used when creating new buckets. Use this option to change what storage type the bucket has. Charges and functionality vary with bucket storage class. Known storage classes:
{0}", classes); }
public static string StorageclassDescriptionShort { get { return LC.L(@"Specifies storage class for creating a bucket"); } }
public static string StorageclassDescriptionShort { get { return LC.L(@"Specify storage class for creating a bucket"); } }
public static string ProjectDescriptionLong { get { return LC.L(@"This option is only used when creating new buckets. Use this option to supply the project ID that the bucket is attached to. The project determines where usage charges are applied."); } }
public static string ProjectDescriptionShort { get { return LC.L(@"Specifies project for creating a bucket"); } }
public static string ProjectDescriptionShort { get { return LC.L(@"Specify project for creating a bucket"); } }
}
internal static class GoogleDrive {
@@ -31,9 +31,9 @@ namespace Duplicati.Library.Backend.Strings {
public static string IllegalMountPoint { get { return LC.L(@"Illegal mount point given."); } }
public static string FileUploadError { get { return LC.L(@"Failed to upload file"); } }
public static string DescriptionDeviceLong(string mountPointOption) { return LC.L(@"The backup device to use. Will be created if not already exists. You can manage your devices from the backup panel in the Jottacloud web interface. When you specify a custom device you should also specify the mount point to use on this device with the ""{0}"" option.", mountPointOption); }
public static string DescriptionDeviceShort { get { return LC.L(@"Supplies the backup device to use"); } }
public static string DescriptionDeviceShort { get { return LC.L(@"Supply the backup device to use"); } }
public static string DescriptionMountPointLong(string deviceOptionName) { return LC.L(@"The mount point to use on the server. The default is ""Archive"" for using the built-in archive mount point. Set this option to ""Sync"" to use the built-in synchronization mount point instead, or if you have specified a custom device with option ""{0}"" you are free to name the mount point as you like.", deviceOptionName); }
public static string DescriptionMountPointShort { get { return LC.L(@"Supplies the mount point to use on the server"); } }
public static string DescriptionMountPointShort { get { return LC.L(@"Supply the mount point to use on the server"); } }
public static string ThreadsLong { get { return LC.L(@"Number of threads for restore operations. In some cases the download rate is limited to 18.5 Mbps per stream. Use multiple threads to increase throughput."); } }
public static string ThreadsShort { get { return LC.L(@"Number of threads for restore operations"); } }
public static string ChunksizeLong { get { return LC.L(@"The chunk size for simultaneous downloading. These chunks will be held in memory, so keep it as low as possible."); } }
+2 -2
View File
@@ -25,9 +25,9 @@ namespace Duplicati.Library.Backend.Strings {
public static string Description { get { return LC.L(@"This backend can read and write data to Mega.co.nz. Allowed format is ""mega://folder/subfolder""."); } }
public static string DisplayName { get { return LC.L(@"mega.nz"); } }
public static string AuthPasswordDescriptionLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string AuthPasswordDescriptionShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string AuthPasswordDescriptionShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string AuthUsernameDescriptionLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string AuthUsernameDescriptionShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string AuthUsernameDescriptionShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string AuthTwoFactorKeyDescriptionLong { get { return LC.L(@"For accounts with two-factor authentication enabled, set the shared secret used to generate the two-factor TOTP codes."); } }
public static string AuthTwoFactorKeyDescriptionShort { get { return LC.L(@"The shared secret used to generate two-factor TOTP codes"); } }
public static string NoPasswordError { get { return LC.L(@"No password given"); } }
@@ -38,7 +38,7 @@ namespace Duplicati.Library.Backend.Strings
internal static class OneDriveV2
{
public static string Description(string mssadescription, string mssalink, string msopdescription, string msoplink) { return LC.L(@"Stores files in Microsoft OneDrive or Microsoft OneDrive for Business via the Microsoft Graph API. Usage of this backend requires that you agree to the terms in {0} ({1}) and {2} ({3}).", mssadescription, mssalink, msopdescription, msoplink); }
public static string Description(string mssadescription, string mssalink, string msopdescription, string msoplink) { return LC.L(@"Store files in Microsoft OneDrive or Microsoft OneDrive for Business via the Microsoft Graph API. Usage of this backend requires that you agree to the terms in {0} ({1}) and {2} ({3}).", mssadescription, mssalink, msopdescription, msoplink); }
public static string DisplayName { get { return LC.L(@"Microsoft OneDrive v2"); } }
public static string DriveIdLong(string defaultDrive) { return LC.L(@"ID of the drive to store data in. If no drive is specified, the default OneDrive or OneDrive for Business drive will be used via '{0}'.", defaultDrive); }
public static string DriveIdShort { get { return LC.L(@"Optional ID of the drive"); } }
@@ -46,7 +46,7 @@ namespace Duplicati.Library.Backend.Strings
internal static class SharePointV2
{
public static string Description(string mssadescription, string mssalink, string msopdescription, string msoplink) { return LC.L(@"Stores files in a Microsoft SharePoint site via the Microsoft Graph API. Usage of this backend requires that you agree to the terms in {0} ({1}) and {2} ({3}).", mssadescription, mssalink, msopdescription, msoplink); }
public static string Description(string mssadescription, string mssalink, string msopdescription, string msoplink) { return LC.L(@"Store files in a Microsoft SharePoint site via the Microsoft Graph API. Usage of this backend requires that you agree to the terms in {0} ({1}) and {2} ({3}).", mssadescription, mssalink, msopdescription, msoplink); }
public static string DisplayName { get { return LC.L(@"Microsoft SharePoint v2"); } }
public static string SiteIdLong { get { return LC.L(@"ID of the site to store data in."); } }
public static string SiteIdShort { get { return LC.L(@"ID of the site"); } }
@@ -56,7 +56,7 @@ namespace Duplicati.Library.Backend.Strings
internal static class MicrosoftGroup
{
public static string Description(string mssadescription, string mssalink, string msopdescription, string msoplink) { return LC.L(@"Stores files in a Microsoft Office 365 Group via the Microsoft Graph API. Allowed formats are ""sharepoint://tenant.sharepoint.com/{{PathToWeb}}//{{Documents}}/subfolder"" (with ""//"" being optionally used to indicate the root document folder) and ""sharepoint://subfolder"" (in which case you must also explicitly specify the SharePoint site's ID via --{0}). Usage of this backend requires that you agree to the terms in {1} ({2}) and {3} ({4}).", "site-id", mssadescription, mssalink, msopdescription, msoplink); }
public static string Description(string mssadescription, string mssalink, string msopdescription, string msoplink) { return LC.L(@"Store files in a Microsoft Office 365 Group via the Microsoft Graph API. Allowed formats are ""sharepoint://tenant.sharepoint.com/{{PathToWeb}}//{{Documents}}/subfolder"" (with ""//"" being optionally used to indicate the root document folder) and ""sharepoint://subfolder"" (in which case you must also explicitly specify the SharePoint site's ID via --{0}). Usage of this backend requires that you agree to the terms in {1} ({2}) and {3} ({4}).", "site-id", mssadescription, mssalink, msopdescription, msoplink); }
public static string DisplayName { get { return LC.L(@"Microsoft Office 365 Group"); } }
public static string GroupIdLong { get { return LC.L(@"ID of the group to store data in."); } }
public static string GroupIdShort { get { return LC.L(@"ID of the group"); } }
@@ -66,7 +66,7 @@ namespace Duplicati.Library.Backend.OpenStack
public string DisplayName { get { return LC.L("OpenStack configuration module"); } }
public string Description { get { return LC.L("Exposes OpenStack configuration as a web module"); } }
public string Description { get { return LC.L("Expose OpenStack configuration as a web module"); } }
public System.Collections.Generic.IList<ICommandLineArgument> SupportedCommands
@@ -74,11 +74,10 @@ namespace Duplicati.Library.Backend.OpenStack
get
{
return new List<ICommandLineArgument>([
new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provides different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provide different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
]);
}
}
}
}
@@ -28,21 +28,21 @@ namespace Duplicati.Library.Backend.Strings
public static string DisplayName { get { return LC.L(@"OpenStack Simple Storage"); } }
public static string MissingOptionError(string optionname) { return LC.L(@"Missing required option: {0}", optionname); }
public static string PasswordOptionLong(string tenantnameoption) { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD"". If the password is supplied, --{0} must also be set.", tenantnameoption); }
public static string PasswordOptionShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string PasswordOptionShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string DomainnameOptionLong { get { return LC.L(@"The domain name of the user used to connect to the server."); } }
public static string DomainnameOptionShort { get { return LC.L(@"Supplies the domain used to connect to the server"); } }
public static string DomainnameOptionShort { get { return LC.L(@"Supply the domain used to connect to the server"); } }
public static string UsernameOptionLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string UsernameOptionShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string UsernameOptionShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string TenantnameOptionLong { get { return LC.L(@"The Tenant Name is commonly the paying user account name. This option must be supplied when authenticating with a password, but is not required when using an API key."); } }
public static string TenantnameOptionShort { get { return LC.L(@"Supplies the Tenant Name used to connect to the server"); } }
public static string TenantnameOptionShort { get { return LC.L(@"Supply the Tenant Name used to connect to the server"); } }
public static string ApikeyOptionLong { get { return LC.L(@"The API key can be used to connect without supplying a password and tenant ID with some providers."); } }
public static string ApikeyOptionShort { get { return LC.L(@"Supplies the API key used to connect to the server"); } }
public static string ApikeyOptionShort { get { return LC.L(@"Supply the API key used to connect to the server"); } }
public static string AuthuriOptionLong(string providers) { return LC.L(@"The authentication URL is used to authenticate the user and find the storage service. The URL commonly ends with ""/v2.0"". Known providers are: {0}{1}", System.Environment.NewLine, providers); }
public static string AuthuriOptionShort { get { return LC.L(@"Supplies the authentication URL"); } }
public static string AuthuriOptionShort { get { return LC.L(@"Supply the authentication URL"); } }
public static string VersionOptionLong { get { return LC.L(@"The keystone API version to use. Valid values are 'v2' and 'v3'."); } }
public static string VersionOptionShort { get { return LC.L(@"The keystone API version to use"); } }
public static string RegionOptionLong { get { return LC.L(@"This option is only used when creating a container, and is used to indicate where the container should be placed. Consult your provider for a list of valid regions, or leave empty for the default region."); } }
public static string RegionOptionShort { get { return LC.L(@"Supplies the region used for creating a container"); } }
public static string RegionOptionShort { get { return LC.L(@"Supply the region used for creating a container"); } }
}
}
+2 -2
View File
@@ -74,14 +74,14 @@ namespace Duplicati.Library.Backend
public string DisplayName { get { return LC.L("S3 configuration module"); } }
public string Description { get { return LC.L("Exposes S3 configuration as a web module"); } }
public string Description { get { return LC.L("Expose S3 configuration as a web module"); } }
public IList<ICommandLineArgument> SupportedCommands
{
get
{
return new List<ICommandLineArgument>([
new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provides different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provide different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
]);
}
+2 -3
View File
@@ -70,7 +70,7 @@ namespace Duplicati.Library.Backend
public string DisplayName { get { return LC.L("S3 IAM support module"); } }
public string Description { get { return LC.L("Exposes S3 IAM manipulation as a web module"); } }
public string Description { get { return LC.L("Expose S3 IAM manipulation as a web module"); } }
public IList<ICommandLineArgument> SupportedCommands
@@ -78,7 +78,7 @@ namespace Duplicati.Library.Backend
get
{
return new List<ICommandLineArgument>([
new CommandLineArgument(KEY_OPERATION, CommandLineArgument.ArgumentType.Enumeration, LC.L("The operation to perform"), LC.L("Selects the operation to perform"), null, Enum.GetNames(typeof(Operation))),
new CommandLineArgument(KEY_OPERATION, CommandLineArgument.ArgumentType.Enumeration, LC.L("The operation to perform"), LC.L("Select the operation to perform"), null, Enum.GetNames(typeof(Operation))),
new CommandLineArgument(KEY_USERNAME, CommandLineArgument.ArgumentType.String, LC.L("The username"), LC.L("The Amazon Access Key ID")),
new CommandLineArgument(KEY_PASSWORD, CommandLineArgument.ArgumentType.String, LC.L("The password"), LC.L("The Amazon Secret Key")),
]);
@@ -235,4 +235,3 @@ namespace Duplicati.Library.Backend
}
}
}
+6 -6
View File
@@ -30,21 +30,21 @@ namespace Duplicati.Library.Backend.Strings
public static string AMZUserIDDescriptionLong { get { return LC.L(@"AWS Access Key ID can be obtained after logging into your AWS account. This can also be supplied through the option --{0}.", "auth-username"); } }
public static string AMZUserIDDescriptionShort { get { return LC.L(@"AWS Access Key ID"); } }
public static string AuthPasswordDescriptionLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string AuthPasswordDescriptionShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string AuthPasswordDescriptionShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string AuthUsernameDescriptionLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string AuthUsernameDescriptionShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string AuthUsernameDescriptionShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string NoAMZKeyError { get { return LC.L(@"No S3 secret key given"); } }
public static string NoAMZUserIDError { get { return LC.L(@"No S3 userID given"); } }
public static string S3LocationDescriptionLong(string regions) { return LC.L(@"This option is only used when creating new buckets. Use this option to change what region the data is stored in. Amazon charges slightly more for non-US buckets. Known bucket locations:
{0}", regions); }
public static string S3LocationDescriptionShort { get { return LC.L(@"Specifies S3 location constraints"); } }
public static string S3LocationDescriptionShort { get { return LC.L(@"Specify S3 location constraints"); } }
public static string S3ServerNameDescriptionLong(string providers) { return LC.L(@"Companies other than Amazon are now supporting the S3 API, meaning that this backend can read and write data to those providers as well. Use this option to set the hostname. Currently known providers are:
{0}", providers); }
public static string S3ServerNameDescriptionShort { get { return LC.L(@"Specifies an alternate S3 server name"); } }
public static string S3ServerNameDescriptionShort { get { return LC.L(@"Specify an alternate S3 server name"); } }
public static string S3ClientDescriptionLong { get { return LC.L(@"Set either to aws or minio. Then either the AWS SDK or Minio SDK will be used to communicate with S3 services."); } }
public static string S3ClientDescriptionShort { get { return LC.L(@"Specifies the S3 client library to use"); } }
public static string S3ClientDescriptionShort { get { return LC.L(@"Specify the S3 client library to use"); } }
public static string DescriptionUseSSLLong { get { return LC.L(@"Use this option to communicate using Secure Socket Layer (SSL) over http (https). Note that bucket names containing a period has problems with SSL connections."); } }
public static string DescriptionUseSSLShort { get { return LC.L(@"Instructs Duplicati to use an SSL (https) connection"); } }
public static string DescriptionUseSSLShort { get { return LC.L(@"Instruct Duplicati to use an SSL (https) connection"); } }
public static string DescriptionDisableChunkEncodingLong { get { return LC.L(@"This disables chunk encoding for the aws client, which is not supported by all S3 providers."); } }
public static string DescriptionDisableChunkEncodingShort { get { return LC.L(@"Disable chunk encoding (aws client only)"); } }
public static string S3StorageclassDescriptionLong { get { return LC.L(@"Use this option to specify a storage class. If this option is not used, the server will choose a default storage class."); } }
+9 -9
View File
@@ -46,21 +46,21 @@ namespace Duplicati.Library.Backend.Strings
public static string Description { get { return LC.L(@"This backend can read and write data to an SSH based backend, using SFTP. Allowed formats are ""ssh://hostname/folder"" and ""ssh://username:password@hostname/folder""."); } }
public static string DisplayName { get { return LC.L(@"SFTP (SSH)"); } }
public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string DescriptionFingerprintLong { get { return LC.L(@"The server fingerprint used for validation of server identity. Format is e.g. ""ssh-rsa 4096 11:22:33:44:55:66:77:88:99:00:11:22:33:44:55:66""."); } }
public static string DescriptionFingerprintShort { get { return LC.L(@"Supplies server fingerprint used for validation of server identity"); } }
public static string DescriptionFingerprintShort { get { return LC.L(@"Supply server fingerprint used for validation of server identity"); } }
public static string DescriptionAnyFingerprintLong { get { return LC.L(@"To guard against man-in-the-middle attacks, the server fingerprint is verified on connection. Use this option to disable host-key fingerprint verification. You should only use this option for testing."); } }
public static string DescriptionAnyFingerprintShort { get { return LC.L(@"Disables fingerprint validation"); } }
public static string DescriptionSshkeyfileLong { get { return LC.L(@"Points to a valid OpenSSH keyfile. If the file is encrypted, the password supplied is used to decrypt it. If the keyfile is specified, the password is not used to authenticate."); } }
public static string DescriptionSshkeyfileShort { get { return LC.L(@"Uses a SSH private key to authenticate"); } }
public static string DescriptionAnyFingerprintShort { get { return LC.L(@"Disable fingerprint validation"); } }
public static string DescriptionSshkeyfileLong { get { return LC.L(@"Point to a valid OpenSSH keyfile. If the file is encrypted, the password supplied is used to decrypt it. If the keyfile is specified, the password is not used to authenticate."); } }
public static string DescriptionSshkeyfileShort { get { return LC.L(@"Use a SSH private key to authenticate"); } }
public static string DescriptionSshkeyLong(string urlprefix) { return LC.L(@"An url-encoded SSH private key. The private key must be prefixed with {0}. If the key is encrypted, the password supplied is used to decrypt it. If the private key is specified, the password is not used to authenticate.", urlprefix); }
public static string DescriptionSshkeyShort { get { return LC.L(@"Uses a SSH private key to authenticate"); } }
public static string DescriptionSshkeyShort { get { return LC.L(@"Use a SSH private key to authenticate"); } }
public static string DescriptionSshtimeoutLong { get { return LC.L(@"Use this option to manage the internal timeout for SSH operations. If the value is set to zero, the operations will not time out."); } }
public static string DescriptionSshtimeoutShort { get { return LC.L(@"Sets the operation timeout value"); } }
public static string DescriptionSshtimeoutShort { get { return LC.L(@"Set the operation timeout value"); } }
public static string DescriptionSshkeepaliveLong { get { return LC.L(@"Use this option to enable the keep-alive interval for the SSH connection. If the connection is idle, aggressive firewalls might close the connection. Using keep-alive will keep the connection open in this scenario. If this value is set to zero, the keep-alive is disabled."); } }
public static string DescriptionSshkeepaliveShort { get { return LC.L(@"Sets a keepalive value"); } }
public static string DescriptionSshkeepaliveShort { get { return LC.L(@"Set a keepalive value"); } }
public static string FolderNotFoundManagedError(string foldername, string message) { return LC.L(@"Unable to set folder to {0}, error message: {1}", foldername, message); }
public static string FingerprintNotMatchManagedError(string fingerprint) { return LC.L(@"Validation of server fingerprint failed. Server returned fingerprint ""{0}"". Cause of this message is either not correct configuration or Man-in-the-middle attack!", fingerprint); }
public static string FingerprintNotSpecifiedManagedError(string fingerprint, string hostkeyoption, string allkeysoptions) { return LC.L(@"Please add --{1}=""{0}"" to trust this host. Optionally you can use --{2} (NOT SECURE) for testing!", fingerprint, hostkeyoption, allkeysoptions); }
@@ -26,9 +26,9 @@ namespace Duplicati.Library.Backend.Strings
public static string Description { get { return LC.L(@"This backend can read and write data to a SharePoint server (including OneDrive for Business). Allowed formats are ""mssp://tennant.sharepoint.com/PathToWeb//BaseDocLibrary/subfolder"" and ""mssp://username:password@tennant.sharepoint.com/PathToWeb//BaseDocLibrary/subfolder"". Use a double slash '//' in the path to denote the web from the documents library."); } }
public static string DisplayName { get { return LC.L(@"Microsoft SharePoint"); } }
public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string DescriptionIntegratedAuthenticationLong { get { return LC.L(@"If the server and client both supports integrated authentication, this option enables that authentication method. This is likely only available with windows servers and clients."); } }
public static string DescriptionIntegratedAuthenticationShort { get { return LC.L(@"Use windows integrated authentication to connect to the server"); } }
public static string DescriptionUseRecyclerLong { get { return LC.L(@"Use this option to have files moved to the recycle bin folder instead of removing them permanently when compacting or deleting backups."); } }
@@ -43,14 +43,14 @@ namespace Duplicati.Library.Backend.Storj
public string DisplayName { get { return LC.L("Storj DCS configuration module"); } }
public string Description { get { return LC.L("Exposes Storj DCS configuration as a web module"); } }
public string Description { get { return LC.L("Expose Storj DCS configuration as a web module"); } }
public IList<ICommandLineArgument> SupportedCommands
{
get
{
return new List<ICommandLineArgument>([
new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provides different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provide different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
]);
}
@@ -24,7 +24,7 @@ namespace Duplicati.Library.Backend.Strings {
public static string Description { get { return LC.L(@"This backend can read and write data to a Tahoe-LAFS based backend. Allowed format is ""tahoe://hostname:port/uri/$DIRCAP""."); } }
public static string Displayname { get { return LC.L(@"Tahoe-LAFS"); } }
public static string DescriptionUseSSLLong { get { return LC.L(@"Use this option to communicate using Secure Socket Layer (SSL) over http (https)."); } }
public static string DescriptionUseSSLShort { get { return LC.L(@"Instructs Duplicati to use an SSL (https) connection"); } }
public static string DescriptionUseSSLShort { get { return LC.L(@"Instruct Duplicati to use an SSL (https) connection"); } }
public static string MissingFolderError(string foldername, string message) { return LC.L(@"The folder {0} was not found, message: {1}", foldername, message); }
public static string UnrecognizedUriError { get { return LC.L(@"Unsupported URL format, must start with ""uri/URI:DIR2:"""); } }
}
@@ -35,7 +35,7 @@ namespace Duplicati.Library.Backend.Strings
public static string COSBucketDescriptionLong { get { return LC.L(@"Bucket name, format: BucketName-APPID"); } }
public static string COSBucketDescriptionShort { get { return LC.L(@"Bucket name"); } }
public static string COSLocationDescriptionLong { get { return LC.L(@"Region is the distribution area of ​​the Tencent cloud hosting machine room. The object storage COS data is stored in the storage buckets of these regions. https://intl.cloud.tencent.com/document/product/436/6224."); } }
public static string COSLocationDescriptionShort { get { return LC.L(@"Specifies COS location constraints"); } }
public static string COSLocationDescriptionShort { get { return LC.L(@"Specify COS location constraints"); } }
public static string COSStorageClassDescriptionLong { get { return LC.L(@"Storage class of the object; check enumerated values at https://intl.cloud.tencent.com/document/product/436/30925."); } }
public static string COSStorageClassDescriptionShort { get { return LC.L(@"Storage class of the object"); } }
}
+3 -3
View File
@@ -26,9 +26,9 @@ namespace Duplicati.Library.Backend.Strings {
public static string DescriptionForceDigestLong { get { return LC.L(@"Using the HTTP Digest authentication method allows the user to authenticate with the server, without sending the password in clear. However, a man-in-the-middle attack is easy, because the HTTP protocol specifies a fallback to Basic authentication, which will make the client send the password to the attacker. Using this option, the client does not accept this, and always uses Digest authentication or fails to connect."); } }
public static string DescriptionForceDigestShort { get { return LC.L(@"Force the use of the HTTP Digest authentication method"); } }
public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string DescriptionIntegratedAuthenticationLong { get { return LC.L(@"If the server and client both supports integrated authentication, this option enables that authentication method. This is likely only available with windows servers and clients."); } }
public static string DescriptionIntegratedAuthenticationShort { get { return LC.L(@"Use windows integrated authentication to connect to the server"); } }
public static string MethodNotAllowedError(System.Net.HttpStatusCode statuscode) { return LC.L(@"The server returned the error code {0} ({1}), indicating that the server does not support WebDAV connections", (int)statuscode, statuscode); }
@@ -37,7 +37,7 @@ namespace Duplicati.Library.Backend.Strings {
This can be because the file is deleted or unavailable, but it can also be because the file extension {2} is blocked by the web server. IIS blocks unknown extensions by default.
Error message: {3}", foldername, filename, extension, errormessage); }
public static string DescriptionUseSSLLong { get { return LC.L(@"Use this option to communicate using Secure Socket Layer (SSL) over http (https)."); } }
public static string DescriptionUseSSLShort { get { return LC.L(@"Instructs Duplicati to use an SSL (https) connection"); } }
public static string DescriptionUseSSLShort { get { return LC.L(@"Instruct Duplicati to use an SSL (https) connection"); } }
public static string DescriptionDebugPropfindLong { get { return LC.L(@"To aid in debugging issues, it is possible to set a path to a file that will be overwritten with the PROPFIND response."); } }
public static string DescriptionDebugPropfindShort { get { return LC.L(@"Dump the PROPFIND response"); } }
}
+5 -5
View File
@@ -25,11 +25,11 @@ namespace Duplicati.Library.Compression.Strings {
public static string DisplayName { get { return LC.L(@"ZIP compression"); } }
public static string CompressionlevelDeprecated(string optionname) { return LC.L(@"Use the option --{0} instead.", optionname); }
public static string CompressionlevelLong { get { return LC.L(@"This option controls the compression level used. A setting of zero gives no compression, and a setting of 9 gives maximum compression."); } }
public static string CompressionlevelShort { get { return LC.L(@"Sets the ZIP compression level"); } }
public static string CompressionlevelShort { get { return LC.L(@"Set the ZIP compression level"); } }
public static string CompressionmethodLong(string optionname) { return LC.L(@"Use this option to set an alternative compressor method, such as LZMA. Note that using another value than Deflate will cause the option --{0} to be ignored.", optionname); }
public static string CompressionmethodShort { get { return LC.L(@"Sets the ZIP compression method"); } }
public static string CompressionmethodShort { get { return LC.L(@"Set the ZIP compression method"); } }
public static string Compressionzip64Long { get { return LC.L(@"The ZIP64 format is required for files larger than 4GiB. Use this option to toggle it."); } }
public static string Compressionzip64Short { get { return LC.L(@"Toggles ZIP64 support"); } }
public static string Compressionzip64Short { get { return LC.L(@"Toggle ZIP64 support"); } }
public static string FileNotFoundError(string filename) { return LC.L(@"File not found: {0}", filename); }
}
internal static class SevenZipCompression {
@@ -41,8 +41,8 @@ namespace Duplicati.Library.Compression.Strings {
public static string ThreadcountLong { get { return LC.L(@"The number of threads used in LZMA 2 compression. Defaults to the number of processor cores."); } }
public static string ThreadcountShort { get { return LC.L(@"Number of threads used in compression"); } }
public static string CompressionlevelLong { get { return LC.L(@"This option controls the compression level used. A setting of zero gives no compression, and a setting of 9 gives maximum compression."); } }
public static string CompressionlevelShort { get { return LC.L(@"Sets the 7z compression level"); } }
public static string CompressionlevelShort { get { return LC.L(@"Set the 7z compression level"); } }
public static string FastalgoLong { get { return LC.L(@"This option controls the compression algorithm used. Enabling this option will cause 7z to use the fast algorithm, which produces slightly less compression."); } }
public static string FastalgoShort { get { return LC.L(@"Sets the 7z fast algorithm usage"); } }
public static string FastalgoShort { get { return LC.L(@"Set the 7z fast algorithm usage"); } }
}
}
+31 -6
View File
@@ -31,17 +31,31 @@ namespace Duplicati.Library.Encryption
/// </summary>
public class AESEncryption : EncryptionBase
{
/// <summary>
/// The key used to encrypt the data
/// </summary>
private string m_key;
private readonly string m_key;
/// <summary>
/// The cached value for size overhead
/// </summary>
private static long m_cachedsizeoverhead = -1;
/// <summary>
/// Cached set of options for minimal header
/// </summary>
private static readonly SharpAESCrypt.EncryptionOptions m_minimalHeaderOptions = new(InsertCreatedByIdentifier: false, InsertTimeStamp: false, InsertPlaceholder: false);
/// <summary>
/// Cached set of options for decryption
/// </summary>
private static readonly SharpAESCrypt.DecryptionOptions m_decryptionOptions = new(IgnorePaddingBytes: Environment.GetEnvironmentVariable("AES_IGNORE_PADDING_BYTES") == "1");
/// <summary>
/// Options to use for encryption
/// </summary>
private readonly SharpAESCrypt.EncryptionOptions m_encryptionOptions;
/// <summary>
/// Default constructor, used to read file extension and supported commands
/// </summary>
@@ -52,12 +66,23 @@ namespace Duplicati.Library.Encryption
/// <summary>
/// Constructs a new AES encryption/decyption instance
/// </summary>
public AESEncryption(string passphrase, Dictionary<string, string> options)
/// <param name="passphrase">The passphrase to use</param>
/// <param name="minimalheader">Flag controlling if the encryption is done with a minimal header</param>
public AESEncryption(string passphrase, bool minimalheader)
{
if (string.IsNullOrEmpty(passphrase))
throw new ArgumentException(Strings.AESEncryption.EmptyKeyError, nameof(passphrase));
m_key = passphrase;
m_encryptionOptions = minimalheader ? m_minimalHeaderOptions : default;
}
/// <summary>
/// Constructs a new AES encryption/decyption instance
/// </summary>
public AESEncryption(string passphrase, Dictionary<string, string> options)
: this(passphrase, false)
{
}
#region IEncryption Members
@@ -81,7 +106,7 @@ namespace Duplicati.Library.Encryption
/// Dispose the specified disposing.
/// </summary>
/// <param name="disposing">If set to <c>true</c> disposing.</param>
protected override void Dispose(bool disposing) { m_key = null; }
protected override void Dispose(bool disposing) { }
/// <summary>
/// Returns the size in bytes of the overhead that will be added to a file of the given size when encrypted
@@ -106,7 +131,7 @@ namespace Duplicati.Library.Encryption
/// <param name="input">The target stream</param>
/// <returns>An encrypted stream that can be written to</returns>
public override Stream Encrypt(Stream input)
=> new SharpAESCrypt.EncryptingStream(m_key, input);
=> new SharpAESCrypt.EncryptingStream(m_key, input, m_encryptionOptions);
/// <summary>
/// Decrypts the stream to the output stream
@@ -114,7 +139,7 @@ namespace Duplicati.Library.Encryption
/// <param name="input">The encrypted stream</param>
/// <returns>The unencrypted stream</returns>
public override Stream Decrypt(Stream input)
=> new SharpAESCrypt.DecryptingStream(m_key, input, new SharpAESCrypt.DecryptionOptions(IgnorePaddingBytes: Environment.GetEnvironmentVariable("AES_IGNORE_PADDING_BYTES") == "1"));
=> new SharpAESCrypt.DecryptingStream(m_key, input, m_decryptionOptions);
/// <summary>
/// Gets a list of supported commandline arguments
@@ -0,0 +1,66 @@
// Copyright (C) 2024, 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.IO;
using System.Text;
namespace Duplicati.Library.Encryption;
public static class AESStringEncryption
{
private enum Direction
{
Encryption,
Decryption
}
public static string EncryptToHex(string passphrase, string content)
{
return Transform(passphrase, content, Direction.Encryption);
}
public static string DecryptFromHex(string passphrase, string content)
{
return Transform(passphrase, content, Direction.Decryption);
}
private static string Transform(string passphrase, string content, Direction direction)
{
using var aesprovider = new AESEncryption(passphrase, minimalheader: true);
using var inputStream = new MemoryStream(direction == Direction.Encryption
? Encoding.UTF8.GetBytes(content)
: Utility.Utility.HexStringAsByteArray(content));
using var outputStream = new MemoryStream();
switch (direction)
{
case Direction.Encryption:
aesprovider.Encrypt(inputStream, outputStream);
return Utility.Utility.ByteArrayAsHexString(outputStream.ToArray());
case Direction.Decryption:
aesprovider.Decrypt(inputStream, outputStream);
return Encoding.UTF8.GetString(outputStream.ToArray());
default:
throw new NotImplementedException();
}
}
}
@@ -0,0 +1,205 @@
// Copyright (C) 2024, 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.
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Duplicati.Library.Interface;
using Duplicati.Library.Utility;
namespace Duplicati.Library.Encryption;
/// <summary>
/// Class used to encrypt and decrypt settings in a way that is backwards compatible
/// with previous versions of Duplicati.
/// </summary>
public static class EncryptedFieldHelper
{
/// <summary>
/// Key instance, isolating the current key and its hash
/// </summary>
/// <param name="Key">The key to use</param>
/// <param name="Hash">The key hash</param>
/// <param name="IsBlacklisted">If the key is blacklisted</param>
public sealed record KeyInstance(string Key, string Hash, bool IsBlacklisted)
{
/// <summary>
/// Creates a new key instance
/// </summary>
/// <param name="key">The key to use</param>
/// <returns>The key instance</returns>
public static KeyInstance CreateKey(string key)
{
if (string.IsNullOrWhiteSpace(key))
throw new ArgumentNullException(nameof(key));
if (key.Length < 8)
throw new ArgumentException("Key must be at least 8 characters long", nameof(key));
using var hasher = HashFactory.CreateHasher(HashFactory.SHA256);
return new KeyInstance(key, key.ComputeHashToHex(hasher), IsKeyBlacklisted(key));
}
}
/// <summary>
/// Checks if a key is blacklisted
/// </summary>
/// <param name="key">The key to check</param>
/// <returns><c>true</c> if the key is blacklisted; <c>false</c> otherwise</returns>
public static bool IsKeyBlacklisted(string key)
=> DeviceIDHelper.EMPTY_DEVICE_ID_HASHES.Contains(key);
/// <summary>
/// The key supplied by the user
/// </summary>
private static string? SUPPLIED_KEY = Environment.GetEnvironmentVariable(ENVIROMENT_VARIABLE_NAME);
/// <summary>
/// The default key to be used for encryption
/// </summary>
private static readonly KeyInstance DefaultKey = KeyInstance.CreateKey(
string.IsNullOrWhiteSpace(SUPPLIED_KEY)
? DeviceIDHelper.GetDeviceIDHash()
: SUPPLIED_KEY
);
/// <summary>
/// Returns a value indicating if the default key is blacklisted and cannot be used
/// </summary>
public static bool IsCurrentKeyBlacklisted => DefaultKey.IsBlacklisted;
/// <summary>
/// Prefix used to identify an encrypted field
/// </summary>
public const string HEADER_PREFIX = "enc-v1:";
/// <summary>
/// The name of the enviroment variable that holds the encryption key
/// </summary>
public const string ENVIROMENT_VARIABLE_NAME = "SETTINGS_ENCRYPTION_KEY";
/// <summary>
/// Checks if a value is an encrypted string
/// </summary>
/// <param name="value">The value to decrypt</param>
/// <returns><c>true</c> if the string is encrypted; <c>false</c> otherwise</returns>
public static bool IsEncryptedString(string value)
=> !string.IsNullOrWhiteSpace(value) && value.StartsWith(HEADER_PREFIX);
/// <summary>
/// Decrypts a value from the database, if it is not encrypted, it will be returned as is.
///
/// If the value is encrypted, it will be decrypted using the key obtained from ActiveKey.
///
/// The check for encryption is done by checking the prefix of the string.
/// An additional check is done by hashing the content and comparing it to the hash
/// </summary>
/// <param name="value">data from the field</param>
/// <returns>Unencrypted data of the field</returns>
[return: NotNullIfNotNull("value")]
public static string? Decrypt(string? value)
=> Decrypt(value, DefaultKey);
/// <summary>
/// Decrypts a value from the database, if it is not encrypted, it will be returned as is.
///
/// If the value is encrypted, it will be decrypted using the key obtained from ActiveKey.
///
/// The check for encryption is done by checking the prefix of the string.
/// An additional check is done by hashing the content and comparing it to the hash
/// </summary>
/// <param name="value">data from the field</param>
/// <param name="key">The key to use for decryption</param>
/// <returns>Unencrypted data of the field</returns>
[return: NotNullIfNotNull("value")]
public static string? Decrypt(string? value, KeyInstance key)
{
// If the value is not encrypted, it will be returned as is.
if (string.IsNullOrEmpty(value) || !value.StartsWith(HEADER_PREFIX))
return value;
value = value.Substring(HEADER_PREFIX.Length);
using var hasher = HashFactory.CreateHasher(HashFactory.SHA256);
// For clarity, HashSize is size in bits / 8 for bytes, then times two because an encrypted field
// is prefixed with two hashes before
var hashSizeInBytes = hasher.HashSize / 8 * 2;
// Value may be encrypted, to ensure, we will parse everything after
// the mark of hashesCombinedSize as content, hash it and check if matches prefix.
var contentHash = value.Substring(0, hashSizeInBytes);
var keyHash = value.Substring(hashSizeInBytes, hashSizeInBytes);
var content = value.Substring(hashSizeInBytes * 2);
if (contentHash == content.ComputeHashToHex(hasher))
{
// Content hashes match therefore it is probed as encrypted, the next
// step is to verify the encryption keys hashes match.
if (keyHash != key.Hash)
throw new SettingsEncryptionKeyMismatchException();
// Lets then decrypt it.
return AESStringEncryption.DecryptFromHex(key.Key, content);
}
// if the hashes don't match, the lenght criteria can be ignored,
// and it will be returned as is.
return value;
}
/// <summary>
/// Encrypts a value to be stored in the database.
/// </summary>
/// <param name="value"></param>
/// <returns>The encrypted string</returns>
public static string Encrypt(string value)
=> Encrypt(value, DefaultKey);
/// <summary>
/// Encrypts a value to be stored in the database.
/// </summary>
/// <param name="value"></param>
/// <param name="key">The key to use for encryption</param>
/// <returns>The encrypted string</returns>
public static string Encrypt(string value, KeyInstance key)
{
if (key.IsBlacklisted)
throw new InvalidOperationException("Refusing to encrypt with a blacklisted key");
using var hasher = HashFactory.CreateHasher(HashFactory.SHA256);
var encrypted = AESStringEncryption.EncryptToHex(key.Key, value);
var sb = new StringBuilder();
sb.Append(HEADER_PREFIX);
sb.Append(encrypted.ComputeHashToHex(hasher));
sb.Append(key.Hash);
sb.Append(encrypted);
return sb.ToString();
}
}
+2 -2
View File
@@ -49,9 +49,9 @@ namespace Duplicati.Library.Encryption.Strings
public static string GpgprogrampathShort { get { return LC.L(@"The path to GnuPG"); } }
public static string GpgencryptionenablearmorLong { get { return LC.L(@"Use this option to supply the --armor option to GPG. The files will be larger but can be sent as pure text files."); } }
public static string GpgencryptionenablearmorShort { get { return LC.L(@"Use GPG Armor"); } }
public static string GpgencryptiondecryptioncommandLong { get { return LC.L(@"Overrides the GPG command supplied for decryption."); } }
public static string GpgencryptiondecryptioncommandLong { get { return LC.L(@"Override the GPG command supplied for decryption."); } }
public static string GpgencryptiondecryptioncommandShort { get { return LC.L(@"The GPG decryption command"); } }
public static string GpgencryptionencryptioncommandLong(string commandname, string optionvalue) { return LC.L(@"Overrides the default GPG encryption command ""{0}"". Normal usage is to request asymetric encryption with the setting {1}.", commandname, optionvalue); }
public static string GpgencryptionencryptioncommandLong(string commandname, string optionvalue) { return LC.L(@"Override the default GPG encryption command ""{0}"". Normal usage is to request asymetric encryption with the setting {1}.", commandname, optionvalue); }
public static string GpgencryptionencryptioncommandShort { get { return LC.L(@"The GPG encryption command"); } }
}
internal static class GPGStreamWrapper
@@ -201,4 +201,18 @@ namespace Duplicati.Library.Interface
: base(message, helpId, innerException)
{}
}
/// <summary>
/// An exception indicating that the current encryption key does not match the key
/// used to encrypt the settings.
/// </summary>
[Serializable]
public class SettingsEncryptionKeyMismatchException : UserInformationException
{
public SettingsEncryptionKeyMismatchException()
: base(Strings.Common.SettingsKeyMismatchExceptionError, "SettingsKeyMismatch")
{}
}
}
+2
View File
@@ -41,5 +41,7 @@ namespace Duplicati.Library.Interface.Strings {
public static string FolderAlreadyExistsError { get { return LC.L(@"The folder cannot be created because it already exists"); } }
public static string FolderMissingError { get { return LC.L(@"The requested folder does not exist"); } }
public static string CancelExceptionError { get { return LC.L(@"Cancelled"); } }
public static string SettingsKeyMismatchExceptionError { get { return LC.L(@"Encryption key used to encrypt target settings does not match current key."); } }
}
}
@@ -1,27 +1,25 @@
// Copyright (C) 2024, 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.
// Copyright (C) 2024, 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 System.Text;
namespace Duplicati.Library.Logging
{
@@ -72,7 +70,7 @@ namespace Duplicati.Library.Logging
m_stream.WriteLine(entry.AsString(true));
}
#endregion
#region IDisposable Members
@@ -21,6 +21,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Duplicati.Library.Logging;
namespace Duplicati.Library.Main
@@ -56,7 +57,7 @@ namespace Duplicati.Library.Main
{
if (target == null)
return;
m_targets.Add(new Tuple<ILogDestination, LogMessageType, Library.Utility.IFilter>(target, loglevel, filter ?? new Library.Utility.FilterExpression()));
}
@@ -76,11 +77,17 @@ namespace Duplicati.Library.Main
m_targets.Clear();
}
/// <summary>
/// Gets the minimum log level of all the targets
/// </summary>
public LogMessageType MinimumLevel
=> m_targets.Select(x => x.Item2).DefaultIfEmpty(LogMessageType.Error).Min();
/// <summary>
/// Writes the message to all the destinations.
/// </summary>
/// <param name="entry">Entry.</param>
public void WriteMessage(LogEntry entry)
public void WriteMessage(LogEntry entry)
{
foreach (var e in m_targets)
{
+117 -74
View File
@@ -22,8 +22,6 @@
using System;
using System.Linq;
using System.Collections.Generic;
using Newtonsoft.Json.Serialization;
using Duplicati.Library.Common;
namespace Duplicati.Library.Main
{
@@ -38,10 +36,83 @@ namespace Duplicati.Library.Main
public string Username;
//public string Passwordhash;
public int Port;
public string Databasepath;
public string Databasepath;
public string ParameterFile;
}
/// <summary>
/// The filename of the file with database configurations
/// </summary>
private const string CONFIG_FILE = "dbconfig.json";
/// <summary>
/// Finds a default storage folder, using the operating system specific locations.
/// The targetfilename is used to detect locations that are used in previous versions.
/// If the targetfilename is found in an old location, but not the current, the old location is used.
/// If running with DEBUG defined, the storage folder is placed in the same folder as the executable
/// </summary>
/// <param name="targetfilename">The filename to look for</param>
/// <param name="appName">The name of the application</param>
/// <returns>The default storage folder</returns>
public static string GetDefaultStorageFolderWithDebugSupport(string targetfilename, string appName = "Duplicati")
{
#if DEBUG
return System.IO.Path.GetDirectoryName(typeof(DatabaseLocator).Assembly.Location) ?? string.Empty;
#else
return GetDefaultStorageFolder(targetfilename, appName);
#endif
}
/// <summary>
/// Finds a default storage folder, using the operating system specific locations.
/// The targetfilename is used to detect locations that are used in previous versions.
/// If the targetfilename is found in an old location, but not the current, the old location is used.
/// </summary>
/// <param name="targetfilename">The filename to look for</param>
/// <param name="appName">The name of the application</param>
/// <returns>The default storage folder</returns>
public static string GetDefaultStorageFolder(string targetfilename, string appName = "Duplicati")
{
//Normal mode uses the systems "(Local) Application Data" folder
// %LOCALAPPDATA% on Windows, ~/.config on Linux
var folder = System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), appName);
if (OperatingSystem.IsWindows())
{
// Special handling for Windows:
// - Older versions use %APPDATA%
// - but new versions use %LOCALAPPDATA%
var newlocation = System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), appName);
var prevfile = System.IO.Path.Combine(folder, targetfilename);
var curfile = System.IO.Path.Combine(newlocation, targetfilename);
// If the new file exists, we use that
// If the new file does not exist, and the old file exists we use the old
// Otherwise we use the new location
if (System.IO.File.Exists(curfile) || !System.IO.File.Exists(prevfile))
folder = newlocation;
}
if (OperatingSystem.IsMacOS())
{
// Special handling for MacOS:
// - Older versions use ~/.config/
// - but new versions use ~/Library/Application\ Support/
var configfolder = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", appName);
var prevfile = System.IO.Path.Combine(configfolder, targetfilename);
var curfile = System.IO.Path.Combine(folder, targetfilename);
// If the old file exists, and not the new file, we use the old
// Otherwise we use the new location
if (!System.IO.File.Exists(curfile) && System.IO.File.Exists(prevfile))
folder = configfolder;
}
return folder;
}
public static string GetDatabasePath(string backend, Options options, bool autoCreate = true, bool anyUsername = false)
{
if (options == null)
@@ -50,40 +121,15 @@ namespace Duplicati.Library.Main
if (!string.IsNullOrEmpty(options.Dbpath))
return options.Dbpath;
//Normal mode uses the systems "(Local) Application Data" folder
// %LOCALAPPDATA% on Windows, ~/.config on Linux
var folder = GetDefaultStorageFolderWithDebugSupport(CONFIG_FILE);
// Special handling for Windows:
// - Older versions use %APPDATA%
// - but new versions use %LOCALAPPDATA%
//
// If we find a new version, lets use that
// otherwise use the older location
//
var folder = System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Duplicati");
if (OperatingSystem.IsWindows())
{
var newlocation = System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Duplicati");
var prevfile = System.IO.Path.Combine(folder, "dbconfig.json");
var curfile = System.IO.Path.Combine(newlocation, "dbconfig.json");
// If the new file exists, we use that
// If the new file does not exist, and the old file exists we use the old
// Otherwise we use the new location
if (System.IO.File.Exists(curfile) || !System.IO.File.Exists(prevfile))
folder = newlocation;
}
var file = System.IO.Path.Combine(folder, "dbconfig.json");
var file = System.IO.Path.Combine(folder, CONFIG_FILE);
List<BackendEntry> configs;
if (!System.IO.File.Exists(file))
configs = new List<BackendEntry>();
else
configs = Newtonsoft.Json.JsonConvert.DeserializeObject<List<BackendEntry>>(System.IO.File.ReadAllText(file, System.Text.Encoding.UTF8));
var uri = new Library.Utility.Uri(backend);
string server = uri.Host;
string path = uri.Path;
@@ -91,64 +137,64 @@ namespace Duplicati.Library.Main
int port = uri.Port;
string username = uri.Username;
string prefix = options.Prefix;
if (username == null || uri.Password == null)
{
var sopts = DynamicLoader.BackendLoader.GetSupportedCommands(backend);
var ropts = new Dictionary<string, string>(options.RawOptions);
foreach(var k in uri.QueryParameters.AllKeys)
foreach (var k in uri.QueryParameters.AllKeys)
ropts[k] = uri.QueryParameters[k];
if (sopts != null)
{
foreach(var o in sopts)
foreach (var o in sopts)
{
if (username == null && o.Aliases != null && o.Aliases.Contains("auth-username", StringComparer.OrdinalIgnoreCase) && ropts.ContainsKey(o.Name))
username = ropts[o.Name];
}
foreach(var o in sopts)
foreach (var o in sopts)
{
if (username == null && o.Name.Equals("auth-username", StringComparison.OrdinalIgnoreCase) && ropts.ContainsKey("auth-username"))
username = ropts["auth-username"];
}
}
}
//Now find the one that matches :)
var matches = (from n in configs
where
n.Type == type &&
//n.Passwordhash == password &&
n.Username == username &&
n.Port == port &&
n.Server == server &&
n.Path == path &&
n.Prefix == prefix
select n).ToList();
where
n.Type == type &&
//n.Passwordhash == password &&
n.Username == username &&
n.Port == port &&
n.Server == server &&
n.Path == path &&
n.Prefix == prefix
select n).ToList();
if (matches.Count > 1)
throw new Duplicati.Library.Interface.UserInformationException(string.Format("Multiple sources found for: {0}", backend), "MultipleLocalDatabaseSourcesFound");
// Re-select
if (matches.Count == 0 && anyUsername && string.IsNullOrEmpty(username))
{
matches = (from n in configs
where
n.Type == type &&
n.Port == port &&
n.Server == server &&
n.Path == path &&
n.Prefix == prefix
select n).ToList();
where
n.Type == type &&
n.Port == port &&
n.Server == server &&
n.Path == path &&
n.Prefix == prefix
select n).ToList();
if (matches.Count > 1)
throw new Duplicati.Library.Interface.UserInformationException(String.Format("Multiple sources found for \"{0}\", try supplying --{1}", backend, "auth-username"), "MultipleLocalDatabaseSourcesFound");
}
if (matches.Count == 0 && !autoCreate)
return null;
if (matches.Count == 0)
{
var backupname = options.BackupName;
@@ -156,20 +202,21 @@ namespace Duplicati.Library.Main
backupname = GenerateRandomName();
else
{
foreach(var c in System.IO.Path.GetInvalidFileNameChars())
foreach (var c in System.IO.Path.GetInvalidFileNameChars())
backupname = backupname.Replace(c.ToString(), "");
}
var newpath = System.IO.Path.Combine(folder, backupname + ".sqlite");
int max_tries = 100;
while (System.IO.File.Exists(newpath) && max_tries-- > 0)
newpath = System.IO.Path.Combine(folder, GenerateRandomName());
if (System.IO.File.Exists(newpath))
throw new Duplicati.Library.Interface.UserInformationException("Unable to find a unique name for the database, please use --dbpath", "CannotCreateRandomName");
//Create a new one, add it to the list, and save it
configs.Add(new BackendEntry() {
configs.Add(new BackendEntry()
{
Type = type,
Server = server,
Path = path,
@@ -177,7 +224,7 @@ namespace Duplicati.Library.Main
Username = username,
//Passwordhash = password,
Port = port,
Databasepath = newpath,
Databasepath = newpath,
ParameterFile = null
});
@@ -187,16 +234,16 @@ namespace Duplicati.Library.Main
var settings = new Newtonsoft.Json.JsonSerializerSettings();
settings.Formatting = Newtonsoft.Json.Formatting.Indented;
System.IO.File.WriteAllText(file, Newtonsoft.Json.JsonConvert.SerializeObject(configs, settings), System.Text.Encoding.UTF8);
return newpath;
}
else
{
return matches[0].Databasepath;
}
}
public static string GenerateRandomName()
{
var rnd = new Random();
@@ -210,11 +257,7 @@ namespace Duplicati.Library.Main
public static bool IsDatabasePathInUse(string path)
{
var folder = System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Duplicati");
if (!System.IO.Directory.Exists(folder))
return false;
var file = System.IO.Path.Combine(folder, "dbconfig.json");
var file = System.IO.Path.Combine(GetDefaultStorageFolderWithDebugSupport(CONFIG_FILE), CONFIG_FILE);
if (!System.IO.File.Exists(file))
return false;
+32 -32
View File
@@ -62,7 +62,7 @@ namespace Duplicati.Library.Main.Strings
public static string RestorepathLong { get { return LC.L(@"By default, files will be restored in the source folders. Use this option to restore to another folder."); } }
public static string RestorepathShort { get { return LC.L(@"Restore to another folder"); } }
public static string AllowsleepLong { get { return LC.L(@"Allow system to enter sleep power modes for inactivity during backup/restore operations (Windows/OSX only)"); } }
public static string AllowsleepShort { get { return LC.L(@"Toggles system sleep mode"); } }
public static string AllowsleepShort { get { return LC.L(@"Toggle system sleep mode"); } }
public static string ThrottledownloadLong { get { return LC.L(@"By setting this value you can limit how much bandwidth Duplicati consumes for downloads. Setting this limit can make the backups take longer, but will make Duplicati less intrusive."); } }
public static string ThrottledownloadShort { get { return LC.L(@"Max number of kilobytes to download pr. second"); } }
public static string ThrottleuploadLong { get { return LC.L(@"By setting this value you can limit how much bandwidth Duplicati consumes for uploads. Setting this limit can make the backups take longer, but will make Duplicati less intrusive."); } }
@@ -95,24 +95,24 @@ namespace Duplicati.Library.Main.Strings
public static string SkipfileslargerthanShort { get { return LC.L(@"Limit the size of files being backed up"); } }
public static string TempdirLong { get { return LC.L(@"Use this option to supply an alternative folder for temporary storage. By default the system default temporary folder is used. Note that also SQLite will put temporary files in this temporary folder."); } }
public static string TempdirShort { get { return LC.L(@"Temporary storage folder"); } }
public static string ThreadpriorityLong { get { return LC.L(@"Selects another thread priority for the process. Use this to set Duplicati to be more or less CPU intensive."); } }
public static string ThreadpriorityLong { get { return LC.L(@"Select another thread priority for the process. Use this to set Duplicati to be more or less CPU intensive."); } }
public static string ThreadpriorityShort { get { return LC.L(@"Thread priority"); } }
public static string DblocksizeLong { get { return LC.L(@"This option can change the maximum size of dblock files. Changing the size can be useful if the backend has a limit on the size of each individual file."); } }
public static string DblocksizeShort { get { return LC.L(@"Limit the size of the volumes"); } }
public static string DisableStreamingLong { get { return LC.L(@"Enabling this option will disallow usage of the streaming interface, which means that transfer progress bars will not show, and bandwidth throttle settings will be ignored."); } }
public static string DisableStreamingShort { get { return LC.L(@"Disables use of the streaming transfer method"); } }
public static string DisableStreamingLong { get { return LC.L(@"Use this option to disallow usage of the streaming interface, which means that transfer progress bars will not show, and bandwidth throttle settings will be ignored."); } }
public static string DisableStreamingShort { get { return LC.L(@"Disable use of the streaming transfer method"); } }
public static string DontreadmanifestsLong { get { return LC.L(@"Use this option to make sure the contents of the manifest file are not read. This also implies that file hashes are not checked either. Use only for disaster recovery."); } }
public static string DontreadmanifestsShort { get { return LC.L(@"Disables manifests verification"); } }
public static string DontreadmanifestsShort { get { return LC.L(@"Disable manifests verification"); } }
public static string CompressionmoduleLong { get { return LC.L(@"Duplicati supports pluggable compression modules. Use this option to select a module to use for compression. This is only applied when creating new volumes, when reading an existing file, the filename is used to select the compression module."); } }
public static string CompressionmoduleShort { get { return LC.L(@"Select what module to use for compression"); } }
public static string EncryptionmoduleLong { get { return LC.L(@"Duplicati supports pluggable encryption modules. Use this option to select a module to use for encryption. This is only applied when creating new volumes, when reading an existing file, the filename is used to select the encryption module."); } }
public static string EncryptionmoduleShort { get { return LC.L(@"Select what module to use for encryption"); } }
public static string DisablemoduleLong { get { return LC.L(@"Supply one or more module names, separated by commas to unload them."); } }
public static string DisablemoduleShort { get { return LC.L(@"Disables one or more modules"); } }
public static string DisablemoduleShort { get { return LC.L(@"Disable one or more modules"); } }
public static string EnablemoduleLong { get { return LC.L(@"Supply one or more module names, separated by commas to load them."); } }
public static string EnablemoduleShort { get { return LC.L(@"Enables one or more modules"); } }
public static string EnablemoduleShort { get { return LC.L(@"Enable one or more modules"); } }
public static string SnapshotpolicyLong { get { return LC.L(@"This setting controls the usage of snapshots, which allows Duplicati to backup files that are locked by other programs. If this is set to ""off"", Duplicati will not attempt to create a disk snapshot. Setting this to ""auto"" makes Duplicati attempt to create a snapshot, and fail silently if that was not allowed or supported (note that the OS may still log system warnings). A setting of ""on"" will also make Duplicati attempt to create a snapshot, but will produce a warning message in the log if it fails. Setting it to ""required"" will make Duplicati abort the backup if the snapshot creation fails. On windows this uses the Volume Shadow Copy Services (VSS) and requires administrative privileges. On Linux this uses Logical Volume Management (LVM) and requires root privileges."); } }
public static string SnapshotpolicyShort { get { return LC.L(@"Controls the use of disk snapshots"); } }
public static string SnapshotpolicyShort { get { return LC.L(@"Control the use of disk snapshots"); } }
public static string AsynchronousuploadfolderLong { get { return LC.L(@"The pre-generated volumes will be placed into the temporary folder by default. This option can set a different folder for placing the temporary volumes. Despite the name, this also works for synchronous runs."); } }
public static string AsynchronousuploadfolderShort { get { return LC.L(@"The path where ready volumes are placed until uploaded"); } }
public static string AsynchronousuploadlimitLong { get { return LC.L(@"When performing asynchronous uploads, Duplicati will create volumes that can be uploaded. To prevent Duplicati from generating too many volumes, this option limits the number of pending uploads. Set to zero to disable the limit."); } }
@@ -120,20 +120,20 @@ namespace Duplicati.Library.Main.Strings
public static string AsynchronousconcurrentuploadlimitLong { get { return LC.L(@"When performing asynchronous uploads, the maximum number of concurrent uploads allowed. Set to zero to disable the limit."); } }
public static string AsynchronousconcurrentuploadlimitShort { get { return LC.L(@"The number of concurrent uploads allowed"); } }
public static string DebugoutputLong { get { return LC.L(@"Activate this option to make some error messages more verbose, which may help you track down a particular issue."); } }
public static string DebugoutputShort { get { return LC.L(@"Enables debugging output"); } }
public static string LogfileLong { get { return LC.L(@"Logs information to the file specified."); } }
public static string DebugoutputShort { get { return LC.L(@"Enable debugging output"); } }
public static string LogfileLong { get { return LC.L(@"Log information to the file specified."); } }
public static string LogfileShort { get { return LC.L(@"Log internal information to a file"); } }
public static string LoglevelLong { get { return LC.L(@"Specifies the amount of log information to write into the file specified by the option --{0}.", "log-file"); } }
public static string LoglevelLong { get { return LC.L(@"Specify the amount of log information to write into the file specified by the option --{0}.", "log-file"); } }
public static string LoglevelShort { get { return LC.L(@"Log information level"); } }
public static string LogLevelDeprecated(string option1, string option2) { return LC.L("Use the options --{0} and --{1} instead.", option1, option2); }
public static string DisableautocreatefolderLong { get { return LC.L(@"If Duplicati detects that the target folder is missing, it will create it automatically. Activate this option to prevent automatic folder creation."); } }
public static string DisableautocreatefolderShort { get { return LC.L(@"Disables automatic folder creation"); } }
public static string DisableautocreatefolderShort { get { return LC.L(@"Disable automatic folder creation"); } }
public static string VssexcludewritersLong { get { return LC.L(@"Use this option to exclude faulty writers from a snapshot. This is equivalent to the -wx flag of the vshadow.exe tool, except that it only accepts writer class GUIDs, and not component names or instance GUIDs. Multiple GUIDs must be separated with a semicolon, and most forms of GUIDs are allowed, including with and without curly braces."); } }
public static string VssexcludewritersShort { get { return LC.L(@"A semicolon separated list of guids of VSS writers to exclude (Windows only)"); } }
public static string UsnpolicyLong { get { return LC.L(@"This setting controls the usage of NTFS USN numbers, which allows Duplicati to obtain a list of files and folders much faster. If this is set to ""off"", Duplicati will not attempt to use USN. Setting this to ""auto"" makes Duplicati attempt to use USN, and fail silently if that was not allowed or supported. A setting of ""on"" will also make Duplicati attempt to use USN, but will produce a warning message in the log if it fails. Setting it to ""required"" will make Duplicati abort the backup if the USN usage fails. This feature is only supported on Windows and requires administrative privileges."); } }
public static string UsnpolicyShort { get { return LC.L(@"Controls the use of NTFS Update Sequence Numbers"); } }
public static string UsnpolicyShort { get { return LC.L(@"Control the use of NTFS Update Sequence Numbers"); } }
public static string DisabletimetoleranceLong { get { return LC.L(@"When matching timestamps, Duplicati will adjust the times by a small fraction to ensure that minor time differences do not cause unexpected updates. If the option --{0} is set to keep a week of backups, and the backup is made the same time each week, it is possible that the clock drifts slightly, such that full week has just passed, causing Duplicati to delete the older backup earlier than expected. To avoid this, Duplicati inserts a 1% tolerance (max 1 hour). Use this option to disable the tolerance, and use strict time checking.", "keep-time"); } }
public static string DisabletimetoleranceShort { get { return LC.L(@"Deactivates tolerance when comparing times"); } }
public static string DisabletimetoleranceShort { get { return LC.L(@"Deactivate tolerance when comparing times"); } }
public static string ListverifyuploadsLong { get { return LC.L(@"Use this option to verify uploads by listing contents."); } }
public static string ListverifyuploadsShort { get { return LC.L(@"Verify uploads by listing contents"); } }
public static string SynchronousuploadLong { get { return LC.L(@"Duplicati will upload files while scanning the disk and producing volumes, which usually makes the backup faster. Use this option to turn the behavior off, so that Duplicati will wait for each volume to complete."); } }
@@ -146,7 +146,7 @@ namespace Duplicati.Library.Main.Strings
public static string UploadUnchangedBackupsShort { get { return LC.L(@"Upload empty backup files"); } }
public static string QuotasizeLong { get { return LC.L(@"Set a limit to the amount of storage used on the backend (by this backup). This is in addition to the full backend quota, if available. Note: Backups will continue past the quota. This only creates warnings and error messages."); } }
public static string QuotasizeShort { get { return LC.L(@"Limit storage use"); } }
public static string QuotaWarningThresholdLong { get { return LC.L(@"Sets a threshold for when to warn about the backend quota being nearly exceeded. It is given as a percentage, and a warning is generated if the amount of available quota is less than this percentage of the total backup size. If the backend does not report the quota information, this value will be ignored."); } }
public static string QuotaWarningThresholdLong { get { return LC.L(@"Set a threshold for when to warn about the backend quota being nearly exceeded. It is given as a percentage, and a warning is generated if the amount of available quota is less than this percentage of the total backup size. If the backend does not report the quota information, this value will be ignored."); } }
public static string QuotaWarningThresholdShort { get { return LC.L(@"Threshold for warning about low quota"); } }
public static string QuotaDisableLong(string optionname) { return LC.L(@"Disable the quota reported by the backend. The option --{0} can still be used to set a manual quota", optionname); }
public static string QuotaDisableShort { get { return LC.L(@"Disable backend quota"); } }
@@ -182,11 +182,11 @@ namespace Duplicati.Library.Main.Strings
public static string NobackendverificationLong { get { return LC.L(@"If this option is set, the local database is not compared to the remote filelist on startup. The intended usage for this option is to work correctly in cases where the filelisting is broken or unavailable."); } }
public static string NobackendverificationShort { get { return LC.L(@"Do not query backend at startup"); } }
public static string IndexfilepolicyLong { get { return LC.L(@"The index files are used to limit the need for downloading dblock files when there is no local database present. The more information is recorded in the index files, the faster operations can proceed without the database. The tradeoff is that larger index files take up more remote space and which may never be used."); } }
public static string IndexfilepolicyShort { get { return LC.L(@"Determines usage of index files"); } }
public static string IndexfilepolicyShort { get { return LC.L(@"Determine usage of index files"); } }
public static string ThresholdLong { get { return LC.L(@"As files are changed, some data stored at the remote destination may not be required. This option controls how much wasted space the destination can contain before being reclaimed. This value is a percentage used on each volume and the total storage."); } }
public static string ThresholdShort { get { return LC.L(@"The maximum wasted space in percent"); } }
public static string DryrunLong { get { return LC.L(@"Use this option to experiment with different settings and observe the outcome without changing actual files."); } }
public static string DryrunShort { get { return LC.L(@"Does not perform any modifications"); } }
public static string DryrunShort { get { return LC.L(@"Do not perform any modifications"); } }
public static string BlockhashalgorithmLong { get { return LC.L(@"This is a very advanced option! Use this option to select a block hash algorithm with smaller or larger hash size, for performance or storage space reasons."); } }
public static string BlockhashalgorithmShort { get { return LC.L(@"The hash algorithm used on blocks"); } }
public static string FilehashalgorithmLong { get { return LC.L(@"This is a very advanced option! Use this option to select a file hash algorithm with smaller or larger hash size, for performance or storage space reasons."); } }
@@ -201,7 +201,7 @@ namespace Duplicati.Library.Main.Strings
public static string PatchwithlocalblocksShort { get { return LC.L(@"Use local file data when restoring"); } }
public static string PatchwithlocalblocksDeprecated(string optionname) { return LC.L(@"Use the option --{0} instead.", optionname); }
public static string NolocaldbLong { get { return LC.L(@"When listing contents or when restoring files, the local database can be skipped. This is usually slower, but can be used to verify the actual contents of the remote store."); } }
public static string NolocaldbShort { get { return LC.L(@"Disables the local database"); } }
public static string NolocaldbShort { get { return LC.L(@"Disable the local database"); } }
public static string KeepversionsLong { get { return LC.L(@"Use this option to set number of versions to keep. Supply -1 to keep all versions."); } }
public static string KeepversionsShort { get { return LC.L(@"Keep a number of versions"); } }
public static string KeeptimeLong { get { return LC.L(@"Use this option to set the timespan in which backups are kept."); } }
@@ -224,7 +224,7 @@ namespace Duplicati.Library.Main.Strings
public static string BackendtestpercentageLong { get { return LC.L(@"After a backup is completed, some (dblock, dindex, dlist) files from the remote backend are selected for verification. Use this option to specify the percentage (between 0 and 100) of files to test. If the option --{0} is also provided, the number of samples tested is the maximum implied by the two options. If the option --{1} is provided, no remote files are verified.", "backup-test-samples", "no-backend-verification"); } }
public static string BackendtestpercentageShort { get { return LC.L(@"The percentage of samples to test after a backup"); } }
public static string FullremoteverificationLong(string optionname) { return LC.L(@"After a backup is completed, some (dblock, dindex, dlist) files from the remote backend are selected for verification. Use this option to turn on full verification, which will decrypt the files and examine the insides of each volume, instead of simply verifying the external hash. If the option --{0} is set, no remote files are verified. This option is automatically set when then verification is performed directly. ListAndIndexes is like True but only dlist and index volumes are handled.", optionname); }
public static string FullremoteverificationShort { get { return LC.L(@"Activates in-depth verification of files"); } }
public static string FullremoteverificationShort { get { return LC.L(@"Activate in-depth verification of files"); } }
public static string FilereadbuffersizeLong { get { return LC.L(@"Use this size to control how many bytes are read from a file before processing."); } }
public static string FilereadbuffersizeShort { get { return LC.L(@"Size of the file read buffer"); } }
public static string FilereadbuffersizeDeprecated { get { return LC.L(@"The option --{0} is no longer used and has been deprecated.", "file-read-buffer-size"); } }
@@ -234,7 +234,7 @@ namespace Duplicati.Library.Main.Strings
public static string ListsetsonlyShort { get { return LC.L(@"List only filesets"); } }
public static string SkipmetadataLong { get { return LC.L(@"Use this option to disable the storage of metadata, such as file timestamps. Disabling metadata storage will speed up the backup and restore operations, but does not affect file size much."); } }
public static string SkipmetadataShort { get { return LC.L(@"Don't store metadata"); } }
public static string SkipmetadataShort { get { return LC.L(@"Do not store metadata"); } }
public static string RestorepermissionsLong { get { return LC.L(@"By default permissions are not restored as they might prevent you from accessing your files. Use this option to restore the permissions as well."); } }
public static string RestorepermissionsShort { get { return LC.L(@"Restore file permissions"); } }
public static string SkiprestoreverificationLong { get { return LC.L(@"After restoring files, the file hash of all restored files are checked to verify that the restore was successful. Use this option to disable the check and avoid waiting for the verification."); } }
@@ -254,7 +254,7 @@ namespace Duplicati.Library.Main.Strings
public static string ForcelocaleLong { get { return LC.L(@"By default, your system locale and culture settings will be used. In some cases you may prefer to run with another locale, for example to get messages in another language. Use this option to set the locale. Supply a blank string to choose the ""Invariant Culture""."); } }
public static string ForcelocaleShort { get { return LC.L(@"Force the locale setting"); } }
public static string ForceActualDateLong { get { return LC.L(@"By default, dates are displayed in the calendar format, meaning ""Today"" or ""Last Thursday"". By setting this option, only the actual dates are displayed, ""Nov 12, 2018, 8:01 AM"" for example."); } }
public static string ForceActualDateShort { get { return LC.L(@"Forces the display of the actual date instead of calendar date"); } }
public static string ForceActualDateShort { get { return LC.L(@"Force the display of the actual date instead of calendar date"); } }
public static string DisablepipingLong { get { return LC.L(@"Use this option to disable multithreaded handling of up- and downloads. That can significantly speed up backend operations depending on the hardware you're running on and the transfer rate of your backend."); } }
public static string DisablepipingShort { get { return LC.L(@"Handle file communication with backend using threaded pipes"); } }
public static string ConcurrencymaxthreadsLong { get { return LC.L(@"Use this option to set the maximum number of threads used. Setting this value to zero or less will dynamically balance the number of active threads to fit the hardware."); } }
@@ -264,11 +264,11 @@ namespace Duplicati.Library.Main.Strings
public static string ConcurrencycompressorsLong { get { return LC.L(@"Use this option to set the number of processes that perform compression of output data."); } }
public static string ConcurrencycompressorsShort { get { return LC.L(@"Specify the number of concurrent compression processes"); } }
public static string DisablesyntehticfilelistLong { get { return LC.L(@"If Duplicati detects that the previous backup did not complete, it will generate a filelist that is a merge of the last completed backup and the contents that were uploaded in the incomplete backup session."); } }
public static string DisablesyntheticfilelistShort { get { return LC.L(@"Disables synthetic filelist"); } }
public static string DisablesyntheticfilelistShort { get { return LC.L(@"Disable synthetic filelist"); } }
public static string CheckfiletimeonlyLong { get { return LC.L(@"This option instructs Duplicati to not look at metadata or filesize when deciding to scan a file for changes. Use this option if you have a large number of files and notice that the scanning takes a long time with unmodified files."); } }
public static string CheckfiletimeonlyShort { get { return LC.L(@"Checks only file lastmodified"); } }
public static string CheckfiletimeonlyShort { get { return LC.L(@"Check only file lastmodified"); } }
public static string DontcompressrestorepathsLong { get { return LC.L(@"When restore a subset of a backup into a new folder, the shortest possible path is used to avoid generating deep paths with empty folders. Use this option to skip this compression, such that the entire original folder structure is preserved, including upper level empty folders."); } }
public static string DontcompressrestorepathsShort { get { return LC.L(@"Disables path compression on restore"); } }
public static string DontcompressrestorepathsShort { get { return LC.L(@"Disable path compression on restore"); } }
public static string AllowfullremovalLong { get { return LC.L(@"By default, the last fileset cannot be removed. This is a safeguard to make sure that all remote data is not deleted by a configuration mistake. Use this option to disable that protection, such that all filesets can be deleted."); } }
public static string AllowfullremovalShort { get { return LC.L(@"Allow removing all filesets"); } }
public static string AutoVacuumLong { get { return LC.L(@"Some operations that manipulate the local database leave unused entries behind. These entries are not deleted from a hard drive until a VACUUM operation is run. This operation saves disk space in the long run but needs to temporarily create a copy of all valid entries in the database. Setting this to true will allow Duplicati to perform VACUUM operations at its discretion."); } }
@@ -277,23 +277,23 @@ namespace Duplicati.Library.Main.Strings
public static string DisablefilescannerShort { get { return LC.L(@"Disable the read-ahead scanner"); } }
public static string DisablefilelistconsistencychecksLong { get { return LC.L(@"In backups with a large number of filesets, the verification can take up a large part of the backup time. If you disable the checks, make sure you run regular check commands to ensure that everything is working as expected."); } }
public static string DisablefilelistconsistencychecksShort { get { return LC.L(@"Disable filelist consistency checks"); } }
public static string DisableOnBatteryLong { get { return LC.L("Use this option to run a scheduled backup if the system is detected to be running on battery power (manual or command line backups will still be run). If the detected power source is mains (e.g., AC) or unknown, then scheduled backups will proceed as normal."); } }
public static string DisableOnBatteryLong { get { return LC.L("Use this option to disable a scheduled backup if the system is detected to be running on battery power (manual or command line backups will still be run). If the detected power source is mains (e.g., AC) or unknown, then scheduled backups will proceed as normal."); } }
public static string DisableOnBatteryShort { get { return LC.L("Disable the backup when on battery power"); } }
public static string LogfileloglevelLong { get { return LC.L(@"Specifies the amount of log information to write into the file specified by the option --{0}.", "log-file"); } }
public static string LogfileloglevelLong { get { return LC.L(@"Specify the amount of log information to write into the file specified by the option --{0}.", "log-file"); } }
public static string LogfileloglevelShort { get { return LC.L(@"Log file information level"); } }
public static string LogfilelogfiltersLong(string delimiter) { return LC.L(@"This option accepts filters that removes or includes messages regardless of their log level. Multiple filters are supported by separating with {0}. Filters are matched against the log tag and assumed to be including, unless they start with '-'. Regular expressions are supported within hard braces. Example: ""+Path*{0}+*Mail*{0}-[.*DNS]"" ", delimiter); }
public static string LogfilelogfiltersShort { get { return LC.L(@"Applies filters to the file log data"); } }
public static string ConsoleloglevelLong { get { return LC.L(@"Specifies the amount of log information to output to the console."); } }
public static string LogfilelogfiltersShort { get { return LC.L(@"Apply filters to the file log data"); } }
public static string ConsoleloglevelLong { get { return LC.L(@"Specify the amount of log information to output to the console."); } }
public static string ConsoleloglevelShort { get { return LC.L(@"Console information level"); } }
public static string ConsolelogfiltersLong(string delimiter) { return LogfilelogfiltersLong(delimiter); }
public static string ConsolelogfiltersShort { get { return LC.L(@"Applies filters to the console log data"); } }
public static string ConsolelogfiltersShort { get { return LC.L(@"Apply filters to the console log data"); } }
public static string UsebackgroundiopriorityLong { get { return LC.L("This option instructs the operating system to set the current process to use the lowest IO priority level, which can make operations run slower but will interfere less with other operations running at the same time."); } }
public static string UsebackgroundiopriorityShort { get { return LC.L("Sets the process to use low IO priority"); } }
public static string UsebackgroundiopriorityShort { get { return LC.L("Set the process to use low IO priority"); } }
public static string ExcludeemptyfoldersLong { get { return LC.L("Use this option to remove all empty folders from a backup."); } }
public static string ExcludeemptyfoldersShort { get { return LC.L("Excludes empty folders"); } }
public static string ExcludeemptyfoldersShort { get { return LC.L("Exclude empty folders"); } }
public static string IgnorefilenamesLong { get { return LC.L("Use this option to set a filename, or list of filenames, that indicate exclusion of a folder which contains it. A common use would be to have a file named something like \".nobackup\" and place this file into folders that should not be backed up."); } }
public static string IgnorefilenamesShort { get { return LC.L("List of filenames that exclude folders"); } }
public static string RestoresymlinkmetadataLong { get { return LC.L("If symlink metadata is applied, it will usually mean changing the symlink target, instead of the symlink itself. For this reason, metadata is not applied to symlinks, but this option can be used to override this, such that metadata is applied to symlinks as well."); } }
@@ -302,7 +302,7 @@ namespace Duplicati.Library.Main.Strings
public static string UnittestmodeShort { get { return LC.L("Activate unittest mode"); } }
public static string ProfilealldatabasequeriesLong { get { return LC.L("To improve performance of the backups, frequent database queries are not logged by default. Enable this option to log all database queries, and remember to set either --{0}={2} or --{1}={2} to report the additional log data", "console-log-level", "log-file-log-level", nameof(Logging.LogMessageType.Profiling)); } }
public static string ProfilealldatabasequeriesShort { get { return LC.L("Activates logging of all database queries"); } }
public static string ProfilealldatabasequeriesShort { get { return LC.L("Activate logging of all database queries"); } }
public static string RebuildmissingdblockfilesLong { get { return LC.L("If dblock files are missing from the destination, you can attempt to rebuild them using local source data. However, since the local data may have changed, it may not be possible to retrieve all the required data and the process may be slow. Use this option to attempt to rebuild missing dblock files."); } }
public static string RebuildmissingdblockfilesShort { get { return LC.L("Rebuild dblock files when missing"); } }
@@ -37,6 +37,7 @@ public static class GenericModules
new RunScript(),
new SendHttpMessage(),
new SendJabberMessage(),
new SendTelegramMessage(),
new SendMail(),
];
}
@@ -212,7 +212,7 @@ namespace Duplicati.Library.Modules.Builtin
if (!commandlineOptions.TryGetValue(OPTION_RESULT_FORMAT, out var format))
format = ResultExportFormat.Duplicati.ToString();
if (!Enum.TryParse<ResultExportFormat>(format, out var exportFormat))
if (!Enum.TryParse<ResultExportFormat>(format, true, out var exportFormat))
exportFormat = ResultExportFormat.Duplicati;
commandlineOptions.TryGetValue(OPTION_VERB, out var verb);
@@ -0,0 +1,200 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using Duplicati.Library.Interface;
namespace Duplicati.Library.Modules.Builtin;
public class SendTelegramMessage : ReportHelper
{
/// <summary>
/// The tag used for log messages
/// </summary>
private static readonly string LOGTAG = Logging.Log.LogTagFromType<SendTelegramMessage>();
/// <summary>
/// The timeout for the HTTP request
/// </summary>
private static readonly TimeSpan REQUEST_TIMEOUT = TimeSpan.FromSeconds(10);
#region Option names
/// <summary>
/// Option used to specify Telegram bot ID
/// </summary>
private const string OPTION_BOTID = "send-telegram-bot-id";
/// <summary>
/// Option used to specify Telegram bot API key
/// </summary>
private const string OPTION_APIKEY = "send-telegram-api-key";
/// <summary>
/// Option used to specify channel to send to
/// </summary>
private const string OPTION_CHANNEL = "send-telegram-channel-id";
/// <summary>
/// Option used to specify report body
/// </summary>
private const string OPTION_MESSAGE = "send-telegram-message";
/// <summary>
/// Option used to specify report level
/// </summary>
private const string OPTION_SENDLEVEL = "send-telegram-level";
/// <summary>
/// Option used to specify if reports are sent for other operations than backups
/// </summary>
private const string OPTION_SENDALL = "send-telegram-any-operation";
/// <summary>
/// Option used to specify what format the result is sent in.
/// </summary>
private const string OPTION_RESULT_FORMAT = "send-telegram-result-output-format";
/// <summary>
/// Option used to set the log level
/// </summary>
private const string OPTION_LOG_LEVEL = "send-telegram-log-level";
/// <summary>
/// Option used to set the log level
/// </summary>
private const string OPTION_LOG_FILTER = "send-telegram-log-filter";
/// <summary>
/// Option used to set the maximum number of log lines
/// </summary>
private const string OPTION_MAX_LOG_LINES = "send-telegram-max-log-lines";
#endregion
#region Option defaults
/// <summary>
/// The default message body
/// </summary>
protected override string DEFAULT_BODY => string.Format("Duplicati %OPERATIONNAME% report for %backup-name%{0}{0} %RESULT%", Environment.NewLine);
/// <summary>
/// Don't use the subject for telegram
/// </summary>
protected override string DEFAULT_SUBJECT => string.Empty;
#endregion
#region Implementation of IGenericModule
/// <summary>
/// The module key, used to activate or deactivate the module on the commandline
/// </summary>
public override string Key => "sendtelegram";
/// <summary>
/// A localized string describing the module with a friendly name
/// </summary>
public override string DisplayName => Strings.SendTelegramMessage.DisplayName;
/// <summary>
/// A localized description of the module
/// </summary>
public override string Description => Strings.SendTelegramMessage.Description;
/// <summary>
/// A boolean value that indicates if the module should always be loaded.
/// If true, the user can choose to not load the module by entering the appropriate commandline option.
/// If false, the user can choose to load the module by entering the appropriate commandline option.
/// </summary>
public override bool LoadAsDefault => true;
/// <summary>
/// Gets a list of supported commandline arguments
/// </summary>
public override IList<ICommandLineArgument> SupportedCommands
=> [
new CommandLineArgument(OPTION_CHANNEL, CommandLineArgument.ArgumentType.String, Strings.SendTelegramMessage.SendtelegramchannelShort, Strings.SendTelegramMessage.SendtelegramchannelLong),
new CommandLineArgument(OPTION_MESSAGE, CommandLineArgument.ArgumentType.String, Strings.SendTelegramMessage.SendtelegrammessageShort, Strings.SendTelegramMessage.SendtelegrammessageLong, DEFAULT_BODY),
new CommandLineArgument(OPTION_BOTID, CommandLineArgument.ArgumentType.String, Strings.SendTelegramMessage.SendtelegrambotidShort, Strings.SendTelegramMessage.SendtelegrambotidLong),
new CommandLineArgument(OPTION_APIKEY, CommandLineArgument.ArgumentType.String, Strings.SendTelegramMessage.SendtelegramapikeyShort, Strings.SendTelegramMessage.SendtelegramapikeyLong),
new CommandLineArgument(OPTION_SENDLEVEL, CommandLineArgument.ArgumentType.String, Strings.SendTelegramMessage.SendtelegramlevelShort, Strings.SendTelegramMessage.SendtelegramlevelLong(ParsedResultType.Success.ToString(), ParsedResultType.Warning.ToString(), ParsedResultType.Error.ToString(), ParsedResultType.Fatal.ToString(), "All"), DEFAULT_LEVEL, null, Enum.GetNames(typeof(ParsedResultType)).Union(new string[] { "All" } ).ToArray()),
new CommandLineArgument(OPTION_SENDALL, CommandLineArgument.ArgumentType.Boolean, Strings.SendTelegramMessage.SendtelegramanyoperationShort, Strings.SendTelegramMessage.SendtelegramanyoperationLong),
new CommandLineArgument(OPTION_LOG_LEVEL, CommandLineArgument.ArgumentType.Enumeration, Strings.ReportHelper.OptionLoglevelShort, Strings.ReportHelper.OptionLoglevelLong, DEFAULT_LOG_LEVEL.ToString(), null, Enum.GetNames(typeof(Logging.LogMessageType))),
new CommandLineArgument(OPTION_LOG_FILTER, CommandLineArgument.ArgumentType.String, Strings.ReportHelper.OptionLogfilterShort, Strings.ReportHelper.OptionLogfilterLong),
new CommandLineArgument(OPTION_MAX_LOG_LINES, CommandLineArgument.ArgumentType.Integer, Strings.ReportHelper.OptionmaxloglinesShort, Strings.ReportHelper.OptionmaxloglinesLong, DEFAULT_LOGLINES.ToString()),
new CommandLineArgument(OPTION_RESULT_FORMAT, CommandLineArgument.ArgumentType.Enumeration, Strings.ReportHelper.ResultFormatShort, Strings.ReportHelper.ResultFormatLong(Enum.GetNames(typeof(ResultExportFormat))), DEFAULT_EXPORT_FORMAT.ToString(), null, Enum.GetNames(typeof(ResultExportFormat))),
];
protected override string SubjectOptionName => OPTION_MESSAGE;
protected override string BodyOptionName => OPTION_MESSAGE;
protected override string ActionLevelOptionName => OPTION_SENDLEVEL;
protected override string ActionOnAnyOperationOptionName => OPTION_SENDALL;
protected override string LogLevelOptionName => OPTION_LOG_LEVEL;
protected override string LogFilterOptionName => OPTION_LOG_FILTER;
protected override string LogLinesOptionName => OPTION_MAX_LOG_LINES;
protected override string ResultFormatOptionName => OPTION_RESULT_FORMAT;
/// <summary>
/// The server username
/// </summary>
private string m_botid;
/// <summary>
/// The server password
/// </summary>
private string m_apikey;
/// <summary>
/// The Telegram ChannelID
/// </summary>
private string m_channelId;
/// <summary>
/// This method is the interception where the module can interact with the execution environment and modify the settings.
/// </summary>
/// <param name="commandlineOptions">A set of commandline options passed to Duplicati</param>
protected override bool ConfigureModule(IDictionary<string, string> commandlineOptions)
{
//We need at least a recipient
commandlineOptions.TryGetValue(OPTION_CHANNEL, out m_channelId);
if (string.IsNullOrEmpty(m_channelId))
return false;
commandlineOptions.TryGetValue(OPTION_BOTID, out m_botid);
commandlineOptions.TryGetValue(OPTION_APIKEY, out m_apikey);
return true;
}
#endregion
protected override string ReplaceTemplate(string input, object result, Exception exception, bool subjectline)
{
// No need to do the expansion as we throw away the result
if (subjectline)
return string.Empty;
return base.ReplaceTemplate(input, result, exception, subjectline);
}
protected override async void SendMessage(string subject, string body)
{
try
{
var p = new
{
chat_id = Uri.EscapeDataString(m_channelId),
parse_mode = "Text",
text = Uri.EscapeDataString(body),
botId = Uri.EscapeDataString(m_botid),
apiKey = Uri.EscapeDataString(m_apikey)
};
var url = $"https://api.telegram.org/bot{p.botId}:{p.apiKey}/sendMessage?chat_id={p.chat_id}&parse_mode={p.parse_mode}&text={p.text}";
using var client = new HttpClient { Timeout = REQUEST_TIMEOUT };
var response = await client.GetAsync(url);
var responseContent = await response.Content.ReadAsStringAsync();
if (responseContent.Contains("\"ok\":true"))
return;
Logging.Log.WriteWarningMessage(LOGTAG, "telegramSendError", null, "Failed to send to telegram messages: {0}", responseContent);
}
catch (Exception e)
{
Logging.Log.WriteWarningMessage(LOGTAG, "telegramSendError", e, "Failed to send to telegram messages: {0}", e.Message);
}
}
}
+45 -16
View File
@@ -50,13 +50,13 @@ namespace Duplicati.Library.Modules.Builtin.Strings
public static string OauthurlLong { get { return LC.L(@"Duplicati uses an external server to support the OAuth authentication flow. If you have set up your own Duplicati OAuth server, you can supply the refresh URL."); } }
public static string OauthurlShort { get { return LC.L(@"Alternate OAuth URL"); } }
public static string SslversionsLong { get { return LC.L(@"This option changes the default SSL versions allowed. This is an advanced option and should only be used if you want to enhance security or work around an issue with a particular SSL protocol."); } }
public static string SslversionsShort { get { return LC.L(@"Sets allowed SSL versions"); } }
public static string SslversionsShort { get { return LC.L(@"Set allowed SSL versions"); } }
public static string OperationtimeoutLong { get { return LC.L(@"This option changes the default timeout for any HTTP request, the time covers the entire operation from initial packet to shutdown."); } }
public static string OperationtimeoutShort { get { return LC.L(@"Sets the default operation timeout"); } }
public static string OperationtimeoutShort { get { return LC.L(@"Set the default operation timeout"); } }
public static string ReadwritetimeoutLong { get { return LC.L(@"This option changes the default read-write timeout. Read-write timeouts are used to detect a stalled requests, and this option configures the maximum time between activity on a connection."); } }
public static string ReadwritetimeoutShort { get { return LC.L(@"Sets readwrite"); } }
public static string ReadwritetimeoutShort { get { return LC.L(@"Set readwrite"); } }
public static string BufferrequestsLong { get { return LC.L(@"This option sets the HTTP buffering. Setting this to ""{0}"" can cause memory leaks, but can also improve performance in some cases.", "true"); } }
public static string BufferrequestsShort { get { return LC.L(@"Sets HTTP buffering"); } }
public static string BufferrequestsShort { get { return LC.L(@"Set HTTP buffering"); } }
}
internal static class HyperVOptions
{
@@ -70,23 +70,23 @@ namespace Duplicati.Library.Modules.Builtin.Strings
}
internal static class RunScript
{
public static string Description { get { return LC.L(@"Executes a script before starting an operation, and again on completion"); } }
public static string Description { get { return LC.L(@"Execute a script before starting an operation, and again on completion"); } }
public static string DisplayName { get { return LC.L(@"Run script"); } }
public static string FinishoptionLong { get { return LC.L(@"Executes a script after performing an operation. The script will receive the operation results written to stdout."); } }
public static string FinishoptionLong { get { return LC.L(@"Execute a script after performing an operation. The script will receive the operation results written to stdout."); } }
public static string FinishoptionShort { get { return LC.L(@"Run a script on exit"); } }
public static string InvalidExitCodeError(string script, int exitcode) { return LC.L(@"The script ""{0}"" returned with exit code {1}", script, exitcode); }
public static string ExitCodeError(string script, int exitcode, string message) { return LC.L(@"The script ""{0}"" returned with exit code {1}{2}", script, exitcode, string.IsNullOrWhiteSpace(message) ? string.Empty : string.Format(": {0}", message)); }
public static string RequiredoptionLong { get { return LC.L(@"Executes a script before performing an operation. The operation will block until the script has completed or timed out. If the script returns a non-zero error code or times out, the operation will be aborted."); } }
public static string RequiredoptionLong { get { return LC.L(@"Execute a script before performing an operation. The operation will block until the script has completed or timed out. If the script returns a non-zero error code or times out, the operation will be aborted."); } }
public static string RequiredoptionShort { get { return LC.L(@"Run a required script on startup"); } }
public static string ResultFormatLong(IEnumerable<string> options) { return LC.L(@"Use this option to select the output format for results. Available formats: {0}", string.Join(", ", options)); }
public static string ResultFormatShort { get { return LC.L(@"Selects the output format for results"); } }
public static string ResultFormatShort { get { return LC.L(@"Select the output format for results"); } }
public static string ScriptExecuteError(string script, string message) { return LC.L(@"Error while executing script ""{0}"": {1}", script, message); }
public static string ScriptTimeoutError(string script) { return LC.L(@"Execution of the script ""{0}"" timed out", script); }
public static string StartupoptionLong { get { return LC.L(@"Executes a script before performing an operation. The operation will block until the script has completed or timed out."); } }
public static string StartupoptionLong { get { return LC.L(@"Execute a script before performing an operation. The operation will block until the script has completed or timed out."); } }
public static string StartupoptionShort { get { return LC.L(@"Run a script on startup"); } }
public static string StdErrorReport(string script, string message) { return LC.L(@"The script ""{0}"" reported error messages: {1}", script, message); }
public static string TimeoutoptionLong { get { return LC.L(@"Sets the maximum time a script is allowed to execute. If the script has not completed within this time, it will continue to execute but the operation will continue too, and no script output will be processed."); } }
public static string TimeoutoptionShort { get { return LC.L(@"Sets the script timeout"); } }
public static string TimeoutoptionLong { get { return LC.L(@"Set the maximum time a script is allowed to execute. If the script has not completed within this time, it will continue to execute but the operation will continue too, and no script output will be processed."); } }
public static string TimeoutoptionShort { get { return LC.L(@"Set the script timeout"); } }
public static string EnableArgumentsLong { get { return LC.L(@"This option enables the use of script arguments. If this option is set, the script arguments are treated as commandline strings. Use single or double quotes to separate arguments."); } }
public static string EnableArgumentsShort { get { return LC.L(@"Enable script arguments"); } }
}
@@ -94,7 +94,7 @@ namespace Duplicati.Library.Modules.Builtin.Strings
{
public static string Description { get { return LC.L(@"This module can send email after an operation completes"); } }
public static string Displayname { get { return LC.L(@"Send mail"); } }
public static string FailedToLookupMXServer(string optionname) { return LC.L(@"Unable to find the destination mail server through MX lookup. Please use the option {0} to specify what smtp server to use.", optionname); }
public static string FailedToLookupMXServer(string optionname) { return LC.L(@"Unable to find the destination mail server through MX lookup. Please use the option --{0} to specify what SMTP server to use.", optionname); }
public static string OptionBodyLong { get { return LC.L(@"This value can be a filename. If the file exists, the file contents will be used as the message body.
In the message body, certain tokens are replaced:
@@ -163,6 +163,35 @@ You can supply multiple options with a comma separator, e.g. ""{0},{1}"". The sp
public static string LoginTimeoutError { get { return LC.L(@"Timeout occurred while logging in to jabber server"); } }
}
internal static class SendTelegramMessage
{
public static string Description { get { return LC.L(@"This module provides support for sending status reports via Telegram messages"); } }
public static string DisplayName { get { return LC.L(@"Telegram report module"); } }
public static string SendtelegramchannelLong { get { return LC.L(@"Use this option to set the channel ID."); } }
public static string SendtelegramchannelShort { get { return LC.L(@"Telegram channel ID"); } }
public static string SendtelegrammessageLong { get { return LC.L(@"This value can be a filename. If the file exists, the file contents will be used as the message.
In the message, certain tokens are replaced:
%OPERATIONNAME% - The name of the operation, normally ""Backup""
%REMOTEURL% - Remote server URL
%LOCALPATH% - The path to the local files or folders involved in the operation (if any)
%PARSEDRESULT% - The parsed result, if the operation is a backup. Possible values are: Error, Warning, Success
All command line options are also reported within %value%, e.g. %volsize%. Any unknown/unset value is removed."); } }
public static string SendtelegrammessageShort { get { return LC.L(@"The message template"); } }
public static string SendtelegrambotidLong { get { return LC.L(@"Use this option to set a bot ID for the bot that will send the message."); } }
public static string SendtelegrambotidShort { get { return LC.L(@"The Telegram bot ID"); } }
public static string SendtelegramapikeyLong { get { return LC.L(@"Use this option to set a API key for the bot that will send the message."); } }
public static string SendtelegramapikeyShort { get { return LC.L(@"The Telegram API key"); } }
public static string SendtelegramlevelLong(string success, string warning, string error, string fatal, string all) { return LC.L(@"You can specify one of ""{0}"", ""{1}"", ""{2}"", ""{3}"".
You can supply multiple options with a comma separator, e.g. ""{0},{1}"". The special value ""{4}"" is a shorthand for ""{0},{1},{2},{3}"" and will cause all backup operations to send a message.", success, warning, error, fatal, all); }
public static string SendtelegramlevelShort { get { return LC.L(@"The messages to send"); } }
public static string SendtelegramanyoperationLong { get { return LC.L(@"By default, messages will only be sent after a backup operation. Use this option to send messages for all operations."); } }
public static string SendtelegramanyoperationShort { get { return LC.L(@"Send messages for all operations"); } }
public static string LoginTimeoutError { get { return LC.L(@"Timeout occurred while sending to Telegram server"); } }
}
internal static class SendHttpMessage
{
public static string Description { get { return LC.L(@"This module provides support for sending status reports via HTTP messages"); } }
@@ -189,7 +218,7 @@ You can supply multiple options with a comma separator, e.g. ""{0},{1}"". The sp
public static string SendhttpanyoperationLong { get { return LC.L(@"By default, messages will only be sent after a backup operation. Use this option to send messages for all operations."); } }
public static string SendhttpanyoperationShort { get { return LC.L(@"Send messages for all operations"); } }
public static string HttpverbLong { get { return LC.L(@"Use this option to change the default HTTP verb used to submit a report."); } }
public static string HttpverbShort { get { return LC.L(@"Sets the HTTP verb to use"); } }
public static string HttpverbShort { get { return LC.L(@"Set the HTTP verb to use"); } }
public static string SendhttpurlsformLong { get { return LC.L(@"Use this option to set HTTP report URLs for sending form-encoded data. This option accepts multiple URLs, seperated by a semi-colon. All URLs will receive the same data. Note that this option ignores the format and verb settings."); } }
public static string SendhttpurlsformShort { get { return LC.L(@"HTTP report URLs for sending form data"); } }
public static string SendhttpurlsjsonLong { get { return LC.L(@"Use this option to set HTTP report URLs for sending JSON data. This option accepts multiple URLs, seperated by a semi-colon. All URLs will receive the same data. Note that this option ignores the format and verb settings."); } }
@@ -200,12 +229,12 @@ You can supply multiple options with a comma separator, e.g. ""{0},{1}"". The sp
{
public static string SendMessageFailedError(string message) { return LC.L(@"Failed to send message: {0}", message); }
public static string OptionLoglevelLong { get { return LC.L("Use this option to set the log level for messages to include in the report."); } }
public static string OptionLoglevelShort { get { return LC.L("Defines a log level for messages"); } }
public static string OptionLoglevelShort { get { return LC.L("Define a log level for messages"); } }
public static string OptionLogfilterLong { get { return LC.L("Use this option to set a filter expression that defines what options are included in the report."); } }
public static string OptionLogfilterShort { get { return LC.L("Log message filter"); } }
public static string OptionmaxloglinesLong { get { return LC.L("Use this option to set the maximum number of log lines to include in the report. Zero or negative values means unlimited."); } }
public static string OptionmaxloglinesShort { get { return LC.L("Limits log lines"); } }
public static string OptionmaxloglinesShort { get { return LC.L("Limit log lines"); } }
public static string ResultFormatLong(IEnumerable<string> options) { return LC.L(@"Use this option to select the output format for results. Available formats: {0}", string.Join(", ", options)); }
public static string ResultFormatShort { get { return LC.L(@"Selects the output format for results"); } }
public static string ResultFormatShort { get { return LC.L(@"Select the output format for results"); } }
}
}
@@ -48,29 +48,25 @@ public static class BackupImportExportHandler
return data;
}
public static Server.Serializable.ImportExportStructure ImportBackup(string configurationFile, bool importMetadata, Func<string> getPassword, Dictionary<string, string> advancedOptions)
public static Server.Serializable.ImportExportStructure ImportBackup(Connection connection, string configurationFile, bool importMetadata, Func<string> getPassword)
{
// This removes the ID and DBPath from the backup configuration.
Server.Serializable.ImportExportStructure importedStructure = LoadConfiguration(configurationFile, importMetadata, getPassword);
// This will create the Duplicati-server.sqlite database file if it doesn't exist.
using (Duplicati.Server.Database.Connection connection = FIXMEGlobal.GetDatabaseConnection(advancedOptions))
if (connection.Backups.Any(x => x.Name.Equals(importedStructure.Backup.Name, StringComparison.OrdinalIgnoreCase)))
{
if (connection.Backups.Any(x => x.Name.Equals(importedStructure.Backup.Name, StringComparison.OrdinalIgnoreCase)))
{
throw new InvalidOperationException($"A backup with the name {importedStructure.Backup.Name} already exists.");
}
string error = connection.ValidateBackup(importedStructure.Backup, importedStructure.Schedule);
if (!string.IsNullOrWhiteSpace(error))
{
throw new InvalidOperationException(error);
}
// This creates a new ID and DBPath.
connection.AddOrUpdateBackupAndSchedule(importedStructure.Backup, importedStructure.Schedule);
throw new InvalidOperationException($"A backup with the name {importedStructure.Backup.Name} already exists.");
}
string error = connection.ValidateBackup(importedStructure.Backup, importedStructure.Schedule);
if (!string.IsNullOrWhiteSpace(error))
{
throw new InvalidOperationException(error);
}
// This creates a new ID and DBPath.
connection.AddOrUpdateBackupAndSchedule(importedStructure.Backup, importedStructure.Schedule);
return importedStructure;
}
@@ -25,20 +25,40 @@ using System.Linq;
using Duplicati.Server.Serialization.Interface;
using System.Text;
using Duplicati.Library.RestAPI;
using Duplicati.Library.Encryption;
using Duplicati.Library.DynamicLoader;
using Duplicati.Library.Main;
namespace Duplicati.Server.Database
{
public class Connection : IDisposable
{
private readonly System.Data.IDbConnection m_connection;
private System.Data.IDbCommand m_errorcmd;
private readonly System.Data.IDbCommand m_errorcmd;
public readonly object m_lock = new object();
public const int ANY_BACKUP_ID = -1;
public const int SERVER_SETTINGS_ID = -2;
private readonly Dictionary<string, Backup> m_temporaryBackups = new Dictionary<string, Backup>();
private readonly bool m_encryptSensitiveFields;
private static readonly HashSet<string> _encryptedFields =
BackendLoader.Backends.SelectMany(x => x.SupportedCommands ?? [])
.Concat(EncryptionLoader.Modules.SelectMany(x => x.SupportedCommands ?? []))
.Concat(CompressionLoader.Modules.SelectMany(x => x.SupportedCommands ?? []))
.Concat(GenericLoader.Modules.SelectMany(x => x.SupportedCommands ?? []))
.Concat(WebLoader.Modules.SelectMany(x => x.SupportedCommands ?? []))
.Concat(new Options(new Dictionary<string, string>()).SupportedCommands)
.Where(x => x.Type == Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Password)
.SelectMany(x => new string[] { x.Name }.Concat(x.Aliases ?? []))
.SelectMany(x => new string[] { x, $"--{x}" })
.Concat([
ServerSettings.CONST.JWT_CONFIG,
ServerSettings.CONST.PBKDF_CONFIG
])
.ToHashSet(StringComparer.OrdinalIgnoreCase);
public Connection(System.Data.IDbConnection connection)
public Connection(System.Data.IDbConnection connection, bool disableFieldEncryption)
{
m_encryptSensitiveFields = !disableFieldEncryption;
m_connection = connection;
m_errorcmd = m_connection.CreateCommand();
m_errorcmd.CommandText = @"INSERT INTO ""ErrorLog"" (""BackupID"", ""Message"", ""Exception"", ""Timestamp"") VALUES (?,?,?,?)";
@@ -48,6 +68,53 @@ namespace Duplicati.Server.Database
this.ApplicationSettings = new ServerSettings(this);
}
public void ReWriteAllFieldsIfEncryptionChanged()
{
// The token is automatically decrypted when the settings are loaded
// In case the password has changed, this will fail and return the encrypted
// hex-string, but will crash before reaching this point
if (this.ApplicationSettings.EncryptedFields != m_encryptSensitiveFields)
{
var backups = this.Backups;
foreach (var b in backups)
{
((Backup)b).LoadChildren(this);
AddOrUpdateBackup(b, false, null);
}
this.SetSettings(this.GetSettings(ANY_BACKUP_ID), ANY_BACKUP_ID);
this.ApplicationSettings.EncryptedFields = m_encryptSensitiveFields;
}
}
public void SetPreloadSettingsIfChanged(Dictionary<string, string> newsettings)
{
if (newsettings == null || newsettings.Count == 0)
return;
var settingsHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(newsettings.OrderBy(x => x.Key)))));
if (settingsHash == this.ApplicationSettings.PreloadSettingsHash)
return;
newsettings = newsettings
.ToDictionary(x => x.Key.StartsWith("--") ? x.Key : $"--{x.Key}", x => x.Value);
var currentSettings = this.Settings;
var filters = currentSettings.Where(x => x.Filter != null).ToDictionary(x => x.Name, x => x.Filter);
var updatedSettings = currentSettings
.Where(x => !newsettings.ContainsKey(x.Name))
.Concat(newsettings.Where(x => x.Value != null).Select(x => new Setting
{
Name = x.Key,
Value = x.Value,
Filter = filters.GetValueOrDefault(x.Key) ?? ""
}));
this.Settings = updatedSettings.ToArray();
this.ApplicationSettings.PreloadSettingsHash = settingsHash;
}
public void LogError(string backupid, string message, Exception ex)
{
lock (m_lock)
@@ -194,7 +261,7 @@ namespace Duplicati.Server.Database
{
Filter = ConvertToString(rd, 0) ?? "",
Name = ConvertToString(rd, 1) ?? "",
Value = ConvertToString(rd, 2) ?? ""
Value = DecryptSensitiveFields(ConvertToString(rd, 2) ?? "")
//TODO: Attach the argument information
},
@"SELECT ""Filter"", ""Name"", ""Value"" FROM ""Option"" WHERE ""BackupID"" = ?", id)
@@ -206,6 +273,14 @@ namespace Duplicati.Server.Database
lock (m_lock)
using (var tr = transaction == null ? m_connection.BeginTransaction() : null)
{
if (m_encryptSensitiveFields)
values = values.Select(x => new Setting
{
Filter = x.Filter,
Name = x.Name,
Value = EncryptSensitiveFields(x.Name, x.Value)
}).ToList();
OverwriteAndUpdateDb(
tr,
@"DELETE FROM ""Option"" WHERE ""BackupID"" = ?", new object[] { id },
@@ -300,7 +375,7 @@ namespace Duplicati.Server.Database
Name = ConvertToString(rd, 1),
Description = ConvertToString(rd, 2),
Tags = (ConvertToString(rd, 3) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
TargetURL = ConvertToString(rd, 4),
TargetURL = EncryptedFieldHelper.Decrypt(ConvertToString(rd, 4)),
DBPath = ConvertToString(rd, 5),
},
@"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" WHERE ID = ?", id)
@@ -558,7 +633,7 @@ namespace Duplicati.Server.Database
n.Name,
n.Description ?? "" , // Description is optional but the column is set to NOT NULL, an additional check is welcome
string.Join(",", n.Tags ?? new string[0]),
n.TargetURL,
m_encryptSensitiveFields ? EncryptedFieldHelper.Encrypt(n.TargetURL) : n.TargetURL,
update ? item.ID : n.DBPath
};
});
@@ -728,7 +803,7 @@ namespace Duplicati.Server.Database
Name = ConvertToString(rd, 1),
Description = ConvertToString(rd, 2),
Tags = (ConvertToString(rd, 3) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
TargetURL = ConvertToString(rd, 4),
TargetURL = EncryptedFieldHelper.Decrypt(ConvertToString(rd, 4)),
DBPath = ConvertToString(rd, 5),
},
@"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" ")
@@ -1254,23 +1329,45 @@ namespace Duplicati.Server.Database
}
}
/// <summary>
/// Encrypts sensitive fields
/// </summary>
/// <param name="fieldName">The fieldname used to determine if it will be encrypted</param>
/// <param name="fieldValue">The field value</param>
/// <returns>The encrypted string or the original value</returns>
private static string EncryptSensitiveFields(string fieldName, string fieldValue)
{
if (fieldValue != null)
return _encryptedFields.Contains(fieldName)
? EncryptedFieldHelper.Encrypt(fieldValue)
: fieldValue;
return null;
}
/// <summary>
/// Decrypts sensitive fields
/// </summary>
/// <param name="fieldValue">The field value</param>
/// <returns>The decrypted string</returns>
private static string DecryptSensitiveFields(string fieldValue)
{
if (fieldValue != null)
return EncryptedFieldHelper.IsEncryptedString(fieldValue)
? EncryptedFieldHelper.Decrypt(fieldValue)
: fieldValue;
return null;
}
#region IDisposable implementation
public void Dispose()
{
if (m_errorcmd != null)
try { if (m_errorcmd != null) m_errorcmd.Dispose(); }
catch { }
finally { m_errorcmd = null; }
try { m_errorcmd?.Dispose(); }
catch { }
try
{
if (m_connection != null)
m_connection.Dispose();
}
catch
{
}
try { m_connection?.Dispose(); }
catch { }
}
#endregion
}
@@ -0,0 +1,6 @@
/*
This update does nothing but the user cannot really downgrade,
because the fields that are encrypted cannot be read by the previous version.
*/
SELECT COUNT(*) FROM "Notification";
@@ -164,5 +164,5 @@ CREATE TABLE "TokenFamily" (
"LastUpdated" INTEGER NOT NULL
);
INSERT INTO "Version" ("Version") VALUES (7);
INSERT INTO "Version" ("Version") VALUES (8);
@@ -58,6 +58,9 @@ namespace Duplicati.Server.Database
public const string JWT_CONFIG = "jwt-config";
public const string PBKDF_CONFIG = "pbkdf-config";
public const string AUTOGENERATED_PASSPHRASE = "autogenerated-passphrase";
public const string DISABLE_VISUAL_CAPTCHA = "disable-visual-captcha";
public const string ENCRYPTED_FIELDS = "encrypted-fields";
public const string PRELOAD_SETTINGS_HASH = "preload-settings-hash";
}
private readonly Dictionary<string, string> settings;
@@ -273,6 +276,20 @@ namespace Duplicati.Server.Database
}
}
public bool DisableVisualCaptcha
{
get
{
return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.DISABLE_VISUAL_CAPTCHA], false);
}
set
{
lock (databaseConnection.m_lock)
settings[CONST.DISABLE_VISUAL_CAPTCHA] = value.ToString();
SaveSettings();
}
}
public int LastWebserverPort
{
get
@@ -636,6 +653,34 @@ namespace Duplicati.Server.Database
SaveSettings();
}
}
public bool EncryptedFields
{
get
{
return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.ENCRYPTED_FIELDS], false);
}
set
{
lock (databaseConnection.m_lock)
settings[CONST.ENCRYPTED_FIELDS] = value.ToString();
SaveSettings();
}
}
public string PreloadSettingsHash
{
get
{
return settings[CONST.PRELOAD_SETTINGS_HASH];
}
set
{
lock (databaseConnection.m_lock)
settings[CONST.PRELOAD_SETTINGS_HASH] = value;
SaveSettings();
}
}
}
}
@@ -18,13 +18,13 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Duplicati\CommandLine\CLI\Duplicati.CommandLine.csproj" />
<ProjectReference Include="..\Duplicati\Library\Common\Duplicati.Library.Common.csproj" />
<ProjectReference Include="..\Duplicati\Library\Encryption\Duplicati.Library.Encryption.csproj" />
<ProjectReference Include="..\Duplicati\Library\Main\Duplicati.Library.Main.csproj" />
<ProjectReference Include="..\Duplicati\Library\Snapshots\Duplicati.Library.Snapshots.csproj" />
<ProjectReference Include="..\Duplicati\Library\SQLiteHelper\Duplicati.Library.SQLiteHelper.csproj" />
<ProjectReference Include="..\Duplicati\Server\Duplicati.Server.Serialization\Duplicati.Server.Serialization.csproj" />
<ProjectReference Include="..\..\CommandLine\CLI\Duplicati.CommandLine.csproj" />
<ProjectReference Include="..\Common\Duplicati.Library.Common.csproj" />
<ProjectReference Include="..\Encryption\Duplicati.Library.Encryption.csproj" />
<ProjectReference Include="..\Main\Duplicati.Library.Main.csproj" />
<ProjectReference Include="..\Snapshots\Duplicati.Library.Snapshots.csproj" />
<ProjectReference Include="..\SQLiteHelper\Duplicati.Library.SQLiteHelper.csproj" />
<ProjectReference Include="..\..\Server\Duplicati.Server.Serialization\Duplicati.Server.Serialization.csproj" />
</ItemGroup>
<ItemGroup>
@@ -72,8 +72,6 @@ namespace Duplicati.Library.RestAPI
/// </summary>
public static readonly LogWriteHandler LogHandler = new LogWriteHandler();
public static Func<Dictionary<string, string>, Server.Database.Connection> GetDatabaseConnection;
/// <summary>
/// The update poll thread.
/// </summary>
@@ -24,6 +24,7 @@ using System.Linq;
using Duplicati.Library.Logging;
using System.Collections.Generic;
using Duplicati.Library.Interface;
using Duplicati.Library.Main;
namespace Duplicati.Server
{
@@ -209,8 +210,8 @@ namespace Duplicati.Server
private volatile bool m_anytimeouts = false;
private RingBuffer<LogEntry> m_buffer;
private ILogDestination m_serverfile;
private LogMessageType m_serverloglevel;
private readonly ControllerMultiLogTarget m_target = new ControllerMultiLogTarget(null, LogMessageType.Warning, null);
private LogMessageType m_logLevel;
public LogWriteHandler()
@@ -237,9 +238,13 @@ namespace Duplicati.Server
if (!System.IO.Directory.Exists(dir))
System.IO.Directory.CreateDirectory(dir);
m_serverfile = new StreamLogDestination(path);
m_serverloglevel = level;
m_target.AddTarget(new StreamLogDestination(path), level, null);
UpdateLogLevel();
}
public void AppendLogDestination(ILogDestination destination, LogMessageType level)
{
m_target.AddTarget(destination, level, null);
UpdateLogLevel();
}
@@ -294,7 +299,7 @@ namespace Duplicati.Server
private void UpdateLogLevel()
{
m_logLevel =
(LogMessageType)(GetActiveTimeouts().Union(new int[] { (int)m_serverloglevel }).Min());
(LogMessageType)GetActiveTimeouts().Append((int)m_target.MinimumLevel).Min();
}
@@ -305,14 +310,13 @@ namespace Duplicati.Server
if (entry.Level < m_logLevel)
return;
if (m_serverfile != null && entry.Level >= m_serverloglevel)
try
{
m_serverfile.WriteMessage(entry);
}
catch
{
}
try
{
m_target.WriteMessage(entry);
}
catch
{
}
lock (m_lock)
{
@@ -342,13 +346,7 @@ namespace Duplicati.Server
public void Dispose()
{
if (m_serverfile != null)
{
var sf = m_serverfile;
m_serverfile = null;
if (sf is IDisposable disposable)
disposable.Dispose();
}
m_target.Dispose();
}
#endregion

Some files were not shown because too many files have changed in this diff Show More