Merge pull request #6404 from carljohnsen/bugfix/rsync-fixes

Bugfix/Fixes to the argument parsing of the remote synchronization tool
This commit is contained in:
Kenneth Skovhede
2025-07-08 18:05:01 +02:00
committed by GitHub
2 changed files with 53 additions and 19 deletions
+22 -9
View File
@@ -42,7 +42,7 @@ namespace Duplicati.UnitTest
/// Tests that the remote synchronization tool doesn't do anything when the dry run option is used.
/// </summary>
[Test]
[Category("Tools")]
[Category("Tools/RemoteSynchronization")]
public void TestDryRun()
{
var l1 = Path.Combine(TARGETFOLDER, "l1");
@@ -67,7 +67,7 @@ namespace Duplicati.UnitTest
/// Tests that the remote synchronization tool works with an empty source to an empty destination.
/// </summary>
[Test]
[Category("Tools")]
[Category("Tools/RemoteSynchronization")]
public void TestEmptySourceAndDestination()
{
var l1 = Path.Combine(TARGETFOLDER, "empty_src");
@@ -89,7 +89,7 @@ namespace Duplicati.UnitTest
/// Test that remote synchronizing an empty source to a non-empty destination deletes the destination files.
/// </summary>
[Test]
[Category("Tools")]
[Category("Tools/RemoteSynchronization")]
public void TestEmptySourceDeletesDestination()
{
var l1 = Path.Combine(TARGETFOLDER, "empty_src");
@@ -113,7 +113,7 @@ namespace Duplicati.UnitTest
/// Test that remote synchronizing an empty source to a non-empty destination renames the destination files when the `--retention` option is used.
/// </summary>
[Test]
[Category("Tools")]
[Category("Tools/RemoteSynchronization")]
public void TestEmptySourceRenamesDestination()
{
var l1 = Path.Combine(TARGETFOLDER, "empty_src");
@@ -147,6 +147,7 @@ namespace Duplicati.UnitTest
/// Tests passing all arguments to the main method of the remote synchronization tool.
/// </summary>
[Test]
[Category("Tools/RemoteSynchronization")]
public void TestMainMethodParsesArgumentsCorrectly()
{
string[][] testCases =
@@ -176,6 +177,18 @@ namespace Duplicati.UnitTest
"--src-options", "somesrckey=somesrcvalue", "anothersrckey=anothersrcvalue",
"--dst-options", "somedstkey=somedstvalue", "anotherdstkey=anotherdstvalue"
],
[
"source", "destination", "--parse-arguments-only",
"--global-options", "somekey=somevalue=with=extra=equals", "anotherkey=anothervalue",
"--src-options", "somekey=somevalue=with=extra=equals", "anotherkey=anothervalue",
"--dst-options", "somekey=somevalue=with=extra=equals", "anotherkey=anothervalue"
],
[
"source", "destination", "--parse-arguments-only",
"--global-options", "somekey=\"some value with spaces\"", "anotherkey=anothervalue",
"--src-options", "somekey=\"some value with spaces\"", "anotherkey=anothervalue",
"--dst-options", "somekey=\"some value with spaces\"", "anotherkey=anothervalue"
]
};
foreach (var args in testCases)
@@ -192,7 +205,7 @@ namespace Duplicati.UnitTest
/// Tests the original inded use of the remote synchronization tool on an empty destination.
/// </summary>
[Test]
[Category("Tools")]
[Category("Tools/RemoteSynchronization")]
public void TestRemoteSynchronization()
{
var l1 = Path.Combine(TARGETFOLDER, "l1");
@@ -203,7 +216,7 @@ namespace Duplicati.UnitTest
var options = TestOptions;
var now = DateTime.Now;
GenerateTestData(DATAFOLDER, 10, 3, 3, 1024 * 1024).Wait();
GenerateTestData(DATAFOLDER, 5, 2, 2, 1024).Wait();
Console.WriteLine($"Generated test data in {DATAFOLDER} in {DateTime.Now - now}");
// Create the directories if they do not exist
@@ -247,7 +260,7 @@ namespace Duplicati.UnitTest
using (var c = new Controller($"file://{l1}", options, null))
{
now = DateTime.Now;
var results = c.Restore([Path.Combine(DATAFOLDER, "*")]);
var results = c.Restore([]);
Assert.AreEqual(0, results.Errors.Count());
Assert.AreEqual(0, results.Warnings.Count());
Console.WriteLine($"Restored {results.RestoredFiles} files to {options["restore-path"]} in {DateTime.Now - now}");
@@ -309,7 +322,7 @@ namespace Duplicati.UnitTest
Assert.IsTrue(DirectoriesAndContentsAreEqual(DATAFOLDER, l2r), "Restored second level files is not equal to original files");
// Add some more files to the source
GenerateTestData(Path.Combine(DATAFOLDER, "brand_new_files"), 5, 2, 2, 1024 * 1024).Wait();
GenerateTestData(Path.Combine(DATAFOLDER, "brand_new_files"), 5, 2, 2, 1024).Wait();
// Backup the new files to l1
using (var c = new Controller($"file://{l1}", options, null))
@@ -413,7 +426,7 @@ namespace Duplicati.UnitTest
/// Tests that the remote synchronization tool verifies the contents of the files.
/// </summary>
[Test]
[Category("Tools")]
[Category("Tools/RemoteSynchronization")]
public void TestVerifies()
{
var l1 = Path.Combine(TARGETFOLDER, "l1");
+31 -10
View File
@@ -143,9 +143,7 @@ destination will be verified before being overwritten (if they seemingly match).
private static async Task<int> Run(Config config)
{
// Unpack and parse the multi token options
Dictionary<string, string> global_options = config.GlobalOptions
.Select(x => x.Split("="))
.ToDictionary(x => x[0], x => x[1]);
var global_options = ParseOptions(config.GlobalOptions);
// Parse the log level
var log_level_parsed = Enum.TryParse<Duplicati.Library.Logging.LogMessageType>(config.LogLevel, true, out var log_level_enum);
@@ -169,13 +167,8 @@ destination will be verified before being overwritten (if they seemingly match).
// Start the logging scope
using var _ = Duplicati.Library.Logging.Log.StartScope(multi_sink, log_level_enum);
Dictionary<string, string> src_opts = config.SrcOptions
.Select(x => x.Split("="))
.ToDictionary(x => x[0], x => x[1]);
Dictionary<string, string> dst_opts = config.DstOptions
.Select(x => x.Split("="))
.ToDictionary(x => x[0], x => x[1]);
var src_opts = ParseOptions(config.SrcOptions);
var dst_opts = ParseOptions(config.DstOptions);
// Merge the global options into the source and destination options. The global options will be overridden by the source and destination options.
foreach (var x in global_options)
@@ -529,6 +522,34 @@ destination will be verified before being overwritten (if they seemingly match).
};
}
/// <summary>
/// Parses the options from a list of strings.
/// Each option should be in the format "key=value". If the value contains spaces,
/// it should be enclosed in quotes, e.g. "key=\"value with spaces\"".
/// </summary>
/// <param name="options">The list of string options to parse</param>
/// <returns>A dictionary with the parsed options, where the key is the option name and the value is the option value.</returns>
/// <exception cref="ArgumentException">If an option was not parsed correctly.</exception>
private static Dictionary<string, string> ParseOptions(IEnumerable<string> options)
{
var result = options
.Select(x => x.Split('='))
.ToDictionary(x => x[0], x => string.Join("=", x.Skip(1)));
// Double check that the options are valid by reconstructing them from the dictionary
foreach (var opt in result.Select(x => $"{x.Key}={x.Value}"))
{
if (!options.Contains(opt))
{
Duplicati.Library.Logging.Log.WriteErrorMessage(LOGTAG, "rsync", null,
"The source option '{0}' is not valid. Please check the syntax.", opt);
throw new ArgumentException($"The source option '{opt}' has not been parsed correctly.");
}
}
return result;
}
/// <summary>
/// Prepares the lists of files to copy, delete and verify.
/// The files to copy are the files that are not in the destination, have a different size or have a more recent modification date.