diff --git a/Duplicati/UnitTest/ToolTests.cs b/Duplicati/UnitTest/ToolTests.cs index 8d8a8935c..b83929495 100644 --- a/Duplicati/UnitTest/ToolTests.cs +++ b/Duplicati/UnitTest/ToolTests.cs @@ -42,7 +42,7 @@ namespace Duplicati.UnitTest /// Tests that the remote synchronization tool doesn't do anything when the dry run option is used. /// [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. /// [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. /// [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. /// [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. /// [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. /// [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. /// [Test] - [Category("Tools")] + [Category("Tools/RemoteSynchronization")] public void TestVerifies() { var l1 = Path.Combine(TARGETFOLDER, "l1"); diff --git a/Tools/RemoteSynchronization/Program.cs b/Tools/RemoteSynchronization/Program.cs index 9c553a5c4..c713e1443 100644 --- a/Tools/RemoteSynchronization/Program.cs +++ b/Tools/RemoteSynchronization/Program.cs @@ -143,9 +143,7 @@ destination will be verified before being overwritten (if they seemingly match). private static async Task Run(Config config) { // Unpack and parse the multi token options - Dictionary 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(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 src_opts = config.SrcOptions - .Select(x => x.Split("=")) - .ToDictionary(x => x[0], x => x[1]); - - Dictionary 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). }; } + /// + /// 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\"". + /// + /// The list of string options to parse + /// A dictionary with the parsed options, where the key is the option name and the value is the option value. + /// If an option was not parsed correctly. + private static Dictionary ParseOptions(IEnumerable 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; + } + /// /// 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.