From 6993969dde1147d70bdde66dc71f01028c3411d2 Mon Sep 17 00:00:00 2001 From: Carl Johnsen Date: Mon, 27 Jan 2025 21:26:06 +0100 Subject: [PATCH] Implement dry run option in the Remote Synchronization Tool --- Tools/RemoteSynchronization/Program.cs | 58 +++++++++++++++++++------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/Tools/RemoteSynchronization/Program.cs b/Tools/RemoteSynchronization/Program.cs index 7e4d6c8dc..471d2fac7 100644 --- a/Tools/RemoteSynchronization/Program.cs +++ b/Tools/RemoteSynchronization/Program.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2025, The Duplicati Team +// Copyright (C) 2025, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -39,10 +39,12 @@ namespace RemoteSynchronization { var src_arg = new Argument(name: "backend_src", description: "The source backend string"); var dst_arg = new Argument(name: "backend_dst", description: "The destination backend string"); + var dry_run_opt = new Option(aliases: ["--dry-run", "-d"], description: "Do not actually write or delete files"); var root_cmd = new RootCommand("Remote Synchronization Tool"); root_cmd.AddArgument(src_arg); root_cmd.AddArgument(dst_arg); + root_cmd.AddOption(dry_run_opt); root_cmd.SetHandler((InvocationContext ctx) => { @@ -58,20 +60,24 @@ namespace RemoteSynchronization private static async Task Run(string src, string dst, Dictionary options) { - var options = new Dictionary(); + var dry_run = options["dry-run"] as bool? ?? false; + var duplicati_options = new Dictionary() + { + ["dry-run"] = dry_run.ToString() + }; - using var b1 = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(src, options); + using var b1 = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(src, duplicati_options); var b1s = b1 as IStreamingBackend; System.Diagnostics.Debug.Assert(b1s != null); - using var b2 = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(dst, options); + using var b2 = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(dst, duplicati_options); var b2s = b2 as IStreamingBackend; System.Diagnostics.Debug.Assert(b2s != null); var (to_copy, to_delete) = PrepareFileLists(b1s, b2s); - var deleted = await DeleteAsync(b2s, to_delete); + var deleted = await DeleteAsync(b2s, to_delete, dry_run); Console.WriteLine($"Deleted {deleted} files from {dst}"); - var copied = await CopyAsync(b1s, b2s, to_copy); + var copied = await CopyAsync(b1s, b2s, to_copy, dry_run); Console.WriteLine($"Copied {copied} files from {src} to {dst}"); return 0; @@ -82,15 +88,14 @@ namespace RemoteSynchronization // TODO low memory mode, where things aren't kept in memory. Maybe utilize SQLite? // TODO Force parameter // TODO Progress reporting - // TODO Dry run // TODO Logging - // TODO check database consistency. I.e. have both databases, check that the block, volume, files, etc match up. - // TODO introduce these checks as a post processing step? Especially the database consistency check, as that is often recreated from the index files. + // check database consistency. I.e. have both databases, check that the block, volume, files, etc match up. + // introduce these checks as a post processing step? Especially the database consistency check, as that is often recreated from the index files. // TODO This tool shouldn't handle it, but for convenience, it should support making the seperate call to the regular Duplicati on the destination backend, which alread carries this functionality. // Forcefully synchronize the remote backends - private static async Task CopyAsync(IStreamingBackend b_src, IStreamingBackend b_dst, IEnumerable files) + private static async Task CopyAsync(IStreamingBackend b_src, IStreamingBackend b_dst, IEnumerable files, bool dry_run) { long successful_copies = 0; using var s = new MemoryStream(); @@ -99,7 +104,14 @@ namespace RemoteSynchronization try { await b_src.GetAsync(f.Name, s, CancellationToken.None); - await b_dst.PutAsync(f.Name, s, CancellationToken.None); + if (dry_run) + { + Console.WriteLine($"Would write {s.Length} bytes of {f.Name} to {b_dst.DisplayName}"); + } + else + { + await b_dst.PutAsync(f.Name, s, CancellationToken.None); + } s.SetLength(0); successful_copies++; } @@ -111,14 +123,21 @@ namespace RemoteSynchronization return successful_copies; } - private static async Task DeleteAsync(IStreamingBackend b, IEnumerable files) + private static async Task DeleteAsync(IStreamingBackend b, IEnumerable files, bool dry_run) { long successful_deletes = 0; foreach (var f in files) { try { - await b.DeleteAsync(f.Name, CancellationToken.None); + if (dry_run) + { + Console.WriteLine($"Would delete {f.Name} from {b.DisplayName}"); + } + else + { + await b.DeleteAsync(f.Name, CancellationToken.None); + } successful_deletes++; } catch (Exception e) @@ -178,7 +197,7 @@ namespace RemoteSynchronization return (to_copy, to_delete.Select(x => lookup_dst[x])); } - private static async Task RenameAsync(IStreamingBackend b, IEnumerable files) + private static async Task RenameAsync(IStreamingBackend b, IEnumerable files, bool dry_run) { long successful_renames = 0; string suffix = $"{DateTime.Now:yyyyMMddHHmmss}.old"; @@ -188,8 +207,15 @@ namespace RemoteSynchronization try { await b.GetAsync(f.Name, downloaded, CancellationToken.None); - await b.PutAsync($"{f.Name}.{suffix}", downloaded, CancellationToken.None); - await b.DeleteAsync(f.Name, CancellationToken.None); + if (dry_run) + { + Console.WriteLine($"Would rename {f.Name} to {f.Name}.{suffix} by deleting and re-uploading {downloaded.Length} bytes to {b.DisplayName}"); + } + else + { + await b.PutAsync($"{f.Name}.{suffix}", downloaded, CancellationToken.None); + await b.DeleteAsync(f.Name, CancellationToken.None); + } downloaded.SetLength(0); successful_renames++; }