Implement dry run option in the Remote Synchronization Tool

This commit is contained in:
Carl Johnsen
2025-01-27 21:26:06 +01:00
parent b025c9af01
commit 6993969dde
+42 -16
View File
@@ -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<string>(name: "backend_src", description: "The source backend string");
var dst_arg = new Argument<string>(name: "backend_dst", description: "The destination backend string");
var dry_run_opt = new Option<bool>(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<int> Run(string src, string dst, Dictionary<string, object?> options)
{
var options = new Dictionary<string, string>();
var dry_run = options["dry-run"] as bool? ?? false;
var duplicati_options = new Dictionary<string, string>()
{
["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<long> CopyAsync(IStreamingBackend b_src, IStreamingBackend b_dst, IEnumerable<IFileEntry> files)
private static async Task<long> CopyAsync(IStreamingBackend b_src, IStreamingBackend b_dst, IEnumerable<IFileEntry> 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<long> DeleteAsync(IStreamingBackend b, IEnumerable<IFileEntry> files)
private static async Task<long> DeleteAsync(IStreamingBackend b, IEnumerable<IFileEntry> 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<long> RenameAsync(IStreamingBackend b, IEnumerable<FileEntry> files)
private static async Task<long> RenameAsync(IStreamingBackend b, IEnumerable<FileEntry> 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++;
}