diff --git a/Tools/RemoteSynchronization/Program.cs b/Tools/RemoteSynchronization/Program.cs index f31721228..56672127d 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 @@ -47,6 +47,18 @@ namespace RemoteSynchronization private const string DEFAULT_LOG_LEVEL = "Information"; private const string DEFAULT_LOG_FILE = ""; + /// + /// Global configuration for the tool. Should be set after parsing the commandline arguments. + /// + private class GlobalConfig + { + internal static bool DryRun = DEFAULT_DRY_RUN; + internal static bool Force = DEFAULT_FORCE; + internal static string LogLevel = DEFAULT_LOG_LEVEL; + internal static string LogFile = DEFAULT_LOG_FILE; + internal static bool Verify = DEFAULT_VERIFY; + } + /// /// The log tag for this tool. /// @@ -128,10 +140,12 @@ destination will be verified before being overwritten (if they seemingly match). private static async Task Run(string src, string dst, Dictionary options) { // Parse the known options for this tool - var dry_run = options["dry-run"] as bool? ?? DEFAULT_DRY_RUN; - var verify = options["verify"] as bool? ?? DEFAULT_VERIFY; + GlobalConfig.Force = options["force"] as bool? ?? DEFAULT_FORCE; + GlobalConfig.DryRun = options["dry-run"] as bool? ?? DEFAULT_DRY_RUN; + GlobalConfig.LogLevel = options["log-level"] as string ?? DEFAULT_LOG_LEVEL; + GlobalConfig.LogFile = options["log-file"] as string ?? DEFAULT_LOG_FILE; + GlobalConfig.Verify = options["verify"] as bool? ?? DEFAULT_VERIFY; var retries = options["retry"] as int? ?? DEFAULT_RETRY; - var force = options["force"] as bool? ?? DEFAULT_FORCE; var retention = options["retention"] as bool? ?? DEFAULT_RETENTION; var confirm = options["confirm"] as bool? ?? DEFAULT_CONFIRM; @@ -179,10 +193,10 @@ destination will be verified before being overwritten (if they seemingly match). System.Diagnostics.Debug.Assert(b2s != null); // Prepare the operations - var (to_copy, to_delete, to_verify) = PrepareFileLists(b1s, b2s, force); + var (to_copy, to_delete, to_verify) = PrepareFileLists(b1s, b2s); // Verify the files if requested. If the files are not verified, they will be deleted and copied again. - if (verify) + if (GlobalConfig.Verify) { // As this is a potentially slow operation, ask for confirmation of the verification) if (!confirm) @@ -227,17 +241,17 @@ destination will be verified before being overwritten (if they seemingly match). // Delete or rename the files that are not needed if (retention) { - var renamed = await RenameAsync(b2, to_delete, dry_run); + var renamed = await RenameAsync(b2, to_delete); Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", "Renamed {0} files in {1}", renamed, b2s.DisplayName); } else { - var deleted = await DeleteAsync(b2s, to_delete, dry_run); + var deleted = await DeleteAsync(b2s, to_delete); Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", "Deleted {0} files from {1}", deleted, b2s.DisplayName); } // Copy the files - var (copied, copy_errors) = await CopyAsync(b1s, b2s, to_copy, dry_run, verify); + var (copied, copy_errors) = await CopyAsync(b1s, b2s, to_copy); Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", "Copied {0} files from {1} to {2}", copied, b1s, b2s); // If there are still errors, retry a few times @@ -250,7 +264,7 @@ destination will be verified before being overwritten (if they seemingly match). for (int i = 0; i < retries; i++) { Thread.Sleep(5000); // Wait 5 seconds before retrying - (copied, copy_errors) = await CopyAsync(b1s, b2s, copy_errors, dry_run, verify); + (copied, copy_errors) = await CopyAsync(b1s, b2s, copy_errors); Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", "Copied {0} files from {1} to {2}", copied, b1s, b2s); if (!copy_errors.Any()) break; @@ -278,6 +292,8 @@ destination will be verified before being overwritten (if they seemingly match). // TODO Progress reporting // TODO Duplicati Results + // TODO Should the "get after put" verification be extracted out to be its own flag, as the "verify destination matches source" check is a desiredly separate operation? + /// /// Copies the files from one backend to another. /// The files are copied one by one, and each file is verified after uploading if the verify flag is set. @@ -288,7 +304,7 @@ destination will be verified before being overwritten (if they seemingly match). /// Flag for whether destructive actions (writes) should be printed rather than performed. Downloads (reads) are still being performed. /// Flag for whether to verify each upload to the destination backend. /// A tuple holding the number of succesful copies and a List of the files that failed. - private static async Task<(long, IEnumerable)> CopyAsync(IStreamingBackend b_src, IStreamingBackend b_dst, IEnumerable files, bool dry_run, bool verify) + private static async Task<(long, IEnumerable)> CopyAsync(IStreamingBackend b_src, IStreamingBackend b_dst, IEnumerable files) { long successful_copies = 0; List errors = []; @@ -299,14 +315,14 @@ destination will be verified before being overwritten (if they seemingly match). try { await b_src.GetAsync(f.Name, s_src, CancellationToken.None); - if (dry_run) + if (GlobalConfig.DryRun) { Duplicati.Library.Logging.Log.WriteDryrunMessage(LOGTAG, "rsync", "Would write {0} bytes of {1} to {2}", s_src.Length, f.Name, b_dst.DisplayName); } else { await b_dst.PutAsync(f.Name, s_src, CancellationToken.None); - if (verify) + if (GlobalConfig.Verify) { await b_dst.GetAsync(f.Name, s_dst, CancellationToken.None); @@ -350,14 +366,14 @@ destination will be verified before being overwritten (if they seemingly match). /// The files to delete. /// Flag for whether the deletion should be printed rather than performed. /// The number of successful deletions. - private static async Task DeleteAsync(IStreamingBackend b, IEnumerable files, bool dry_run) + private static async Task DeleteAsync(IStreamingBackend b, IEnumerable files) { long successful_deletes = 0; foreach (var f in files) { try { - if (dry_run) + if (GlobalConfig.DryRun) { Duplicati.Library.Logging.Log.WriteDryrunMessage(LOGTAG, "rsync", "Would delete {0} from {1}", f.Name, b.DisplayName); } @@ -400,13 +416,13 @@ destination will be verified before being overwritten (if they seemingly match). /// The destination backend. /// Flag for whether to force the synchronization. /// A tuple of Lists each holding the files to copy, delete and verify. - private static (IEnumerable, IEnumerable, IEnumerable) PrepareFileLists(IStreamingBackend b_src, IStreamingBackend b_dst, bool force) + private static (IEnumerable, IEnumerable, IEnumerable) PrepareFileLists(IStreamingBackend b_src, IStreamingBackend b_dst) { var files_src = b_src.List(); var files_dst = b_dst.List(); // Shortcut for force - if (force) + if (GlobalConfig.Force) { return (files_src, files_dst, []); } @@ -471,7 +487,7 @@ destination will be verified before being overwritten (if they seemingly match). /// The files to rename. /// Flag for whether the renaming should be printed rather than performed. /// The number of successful renames. - private static async Task RenameAsync(IBackend b, IEnumerable files, bool dry_run) + private static async Task RenameAsync(IBackend b, IEnumerable files) { long successful_renames = 0; string suffix = $"{System.DateTime.Now:yyyyMMddHHmmss}.old"; @@ -485,7 +501,7 @@ destination will be verified before being overwritten (if they seemingly match). try { await sb.GetAsync(f.Name, downloaded, CancellationToken.None); - if (dry_run) + if (GlobalConfig.DryRun) { Duplicati.Library.Logging.Log.WriteDryrunMessage(LOGTAG, "rsync", "Would rename {0} to {0}.{1} by deleting and re-uploading {2} bytes to {3}", f.Name, suffix, downloaded.Length, sb.DisplayName); } @@ -514,7 +530,7 @@ destination will be verified before being overwritten (if they seemingly match). { try { - if (dry_run) + if (GlobalConfig.DryRun) { Duplicati.Library.Logging.Log.WriteDryrunMessage(LOGTAG, "rsync", "Would rename {0} to {0}.{1} by calling Rename on {2}", f.Name, suffix, rb.DisplayName); }