diff --git a/Duplicati/Library/Modules/Builtin/RemoteSynchronizationModule.cs b/Duplicati/Library/Modules/Builtin/RemoteSynchronizationModule.cs index 7346c2ce2..4a75df5d4 100644 --- a/Duplicati/Library/Modules/Builtin/RemoteSynchronizationModule.cs +++ b/Duplicati/Library/Modules/Builtin/RemoteSynchronizationModule.cs @@ -375,7 +375,7 @@ public class RemoteSynchronizationModule : IGenericCallbackModuleWithProgress progressUpdater?.UpdatePhase(OperationPhase.Backup_RemoteSynchronization); var config = dest.Config with { Src = m_source! }; - var exitCode = RemoteSynchronizationRunner.Run(config, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); + var exitCode = RemoteSynchronizationRunner.Run(config, CancellationToken.None, progressUpdater).ConfigureAwait(false).GetAwaiter().GetResult(); if (exitCode == 0) RecordSyncOperation(i); diff --git a/Duplicati/Library/RemoteSynchronization/RemoteSynchronizationRunner.cs b/Duplicati/Library/RemoteSynchronization/RemoteSynchronizationRunner.cs index c2437fdc6..deb384d19 100644 --- a/Duplicati/Library/RemoteSynchronization/RemoteSynchronizationRunner.cs +++ b/Duplicati/Library/RemoteSynchronization/RemoteSynchronizationRunner.cs @@ -23,6 +23,7 @@ using Duplicati.Library.Backend; using Duplicati.Library.Common.IO; using Duplicati.Library.Interface; using Duplicati.Library.Localization.Short; +using Duplicati.Library.Main; using Duplicati.Library.Main.Backend; using Duplicati.Library.Logging; using Duplicati.Library.Utility; @@ -135,8 +136,9 @@ namespace RemoteSynchronization /// /// The parsed configuration for the tool. /// The cancellation token to use for the asynchronous operations. + /// Optional progress updater for reporting file count and transfer progress to the UI. /// The return code for the main entry; 0 on success. - public static async Task Run(Config config, CancellationToken token) + public static async Task Run(Config config, CancellationToken token, IOperationProgressUpdater? progressUpdater = null) { // Parse the log level var log_level_parsed = Enum.TryParse(config.LogLevel, true, out var log_level_enum); @@ -160,11 +162,11 @@ namespace RemoteSynchronization // Start the logging scope using var _ = Duplicati.Library.Logging.Log.StartScope(multi_sink, log_level_enum); - return await RunCore(config, token).ConfigureAwait(false); + return await RunCore(config, token, progressUpdater).ConfigureAwait(false); } else { - return await RunCore(config, token).ConfigureAwait(false); + return await RunCore(config, token, progressUpdater).ConfigureAwait(false); } } @@ -173,8 +175,9 @@ namespace RemoteSynchronization /// /// The parsed configuration for the tool. /// The cancellation token to use for the asynchronous operations. + /// Optional progress updater for reporting file count and transfer progress to the UI. /// The return code for the main entry; 0 on success. - private static async Task RunCore(Config config, CancellationToken token) + private static async Task RunCore(Config config, CancellationToken token, IOperationProgressUpdater? progressUpdater = null) { // Unpack and parse the multi token options var global_options = ParseOptions(config.GlobalOptions); @@ -257,6 +260,15 @@ namespace RemoteSynchronization } } + // Report total file count to the progress updater (delete + copy operations) + var totalFileCount = to_delete.Count() + to_copy.Count(); + var totalFileSize = to_copy.Sum(f => f.Size); + if (progressUpdater != null) + { + progressUpdater.UpdatefileCount(totalFileCount, totalFileSize, true); + progressUpdater.UpdatefilesProcessed(0, 0); + } + // Delete or rename the files that are not needed long renamed = 0, deleted = 0; if (config.Retention) @@ -272,8 +284,12 @@ namespace RemoteSynchronization "Deleted {0} files from {1}", deleted, b2m.DisplayName); } + // Update progress after delete/rename phase + var deletedOrRenamed = Math.Max(deleted, renamed); + progressUpdater?.UpdatefilesProcessed(deletedOrRenamed, 0); + // Copy the files - var (copied, copy_errors) = await CopyAsync(b1m, b2m, to_copy, config, token).ConfigureAwait(false); + var (copied, copy_errors) = await CopyAsync(b1m, b2m, to_copy, config, deletedOrRenamed, token, progressUpdater).ConfigureAwait(false); Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", "Copied {0} files from {1} to {2}", copied, b1m.DisplayName, b2m.DisplayName); @@ -290,7 +306,7 @@ namespace RemoteSynchronization for (int i = 0; i < config.Retry; i++) { await Task.Delay(5000).ConfigureAwait(false); // Wait 5 seconds before retrying - (copied, copy_errors) = await CopyAsync(b1m, b2m, copy_errors, config, token).ConfigureAwait(false); + (copied, copy_errors) = await CopyAsync(b1m, b2m, copy_errors, config, deletedOrRenamed, token, progressUpdater).ConfigureAwait(false); Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", "Copied {0} files from {1} to {2}", copied, b1m.DisplayName, b2m.DisplayName); if (!copy_errors.Any()) @@ -345,11 +361,14 @@ namespace RemoteSynchronization /// The destination backend. /// The files that will be copied. /// The parsed configuration for the tool. + /// The number of files already processed before this copy phase (e.g., from delete/rename), used for progress reporting. /// The cancellation token to use for the asynchronous operations. + /// Optional progress updater for reporting file-level progress to the UI. /// A tuple holding the number of succesful copies and a List of the files that failed. - private static async Task<(long, IEnumerable)> CopyAsync(LightWeightBackendManager b_src, LightWeightBackendManager b_dst, IEnumerable files, Config config, CancellationToken token) + private static async Task<(long, IEnumerable)> CopyAsync(LightWeightBackendManager b_src, LightWeightBackendManager b_dst, IEnumerable files, Config config, long filesProcessedOffset, CancellationToken token, IOperationProgressUpdater? progressUpdater = null) { long successful_copies = 0; + long sizeProcessed = 0; List errors = []; long i = 0, n = files.Count(); var sw_get_src = new System.Diagnostics.Stopwatch(); @@ -362,6 +381,9 @@ namespace RemoteSynchronization if (config.Progress) Console.Write($"\rCopying: {i}/{n}"); + // Report current file to the progress updater + progressUpdater?.StartFile(f.Name, f.Size); + Duplicati.Library.Logging.Log.WriteVerboseMessage(LOGTAG, "rsync", "Copying {0} from {1} to {2}", f.Name, b_src.DisplayName, b_dst.DisplayName); @@ -373,6 +395,10 @@ namespace RemoteSynchronization await b_src.GetAsync(f.Name, s_src, token).ConfigureAwait(false); s_src.Position = 0; sw_get_src.Stop(); + + // Report download progress (half of the file transfer) + progressUpdater?.UpdateFileProgress(f.Size / 2); + if (config.DryRun) { Duplicati.Library.Logging.Log.WriteDryrunMessage(LOGTAG, "rsync", @@ -394,6 +420,9 @@ namespace RemoteSynchronization await b_dst.PutAsync(f.Name, s_src, token).ConfigureAwait(false); sw_put_dst.Stop(); + // Report upload complete (full file transfer done) + progressUpdater?.UpdateFileProgress(f.Size); + if (config.VerifyGetAfterPut) { using var s_dst = Duplicati.Library.Utility.TempFileStream.Create(); @@ -427,6 +456,7 @@ namespace RemoteSynchronization } successful_copies++; + sizeProcessed += f.Size; } catch (Exception e) { @@ -438,6 +468,10 @@ namespace RemoteSynchronization { i++; + // Report overall progress: files processed so far (offset + current index) + progressUpdater?.UpdatefilesProcessed(filesProcessedOffset + i, sizeProcessed); + progressUpdater?.UpdateProgress(n > 0 ? (float)i / n : 1.0f); + // Stop any running timers sw_get_src.Stop(); sw_put_dst.Stop();