The remote synchronization runner now reports its progress to the UI

This commit is contained in:
Carl Johnsen
2026-03-20 04:49:14 +01:00
parent 5046edef49
commit 407dcbaa59
2 changed files with 42 additions and 8 deletions
@@ -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);
@@ -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
/// </summary>
/// <param name="config">The parsed configuration for the tool.</param>
/// <param name="token">The cancellation token to use for the asynchronous operations.</param>
/// <param name="progressUpdater">Optional progress updater for reporting file count and transfer progress to the UI.</param>
/// <returns>The return code for the main entry; 0 on success.</returns>
public static async Task<int> Run(Config config, CancellationToken token)
public static async Task<int> Run(Config config, CancellationToken token, IOperationProgressUpdater? progressUpdater = null)
{
// Parse the log level
var log_level_parsed = Enum.TryParse<Duplicati.Library.Logging.LogMessageType>(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
/// </summary>
/// <param name="config">The parsed configuration for the tool.</param>
/// <param name="token">The cancellation token to use for the asynchronous operations.</param>
/// <param name="progressUpdater">Optional progress updater for reporting file count and transfer progress to the UI.</param>
/// <returns>The return code for the main entry; 0 on success.</returns>
private static async Task<int> RunCore(Config config, CancellationToken token)
private static async Task<int> 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
/// <param name="b_dst">The destination backend.</param>
/// <param name="files">The files that will be copied.</param>
/// <param name="config">The parsed configuration for the tool.</param>
/// <param name="filesProcessedOffset">The number of files already processed before this copy phase (e.g., from delete/rename), used for progress reporting.</param>
/// <param name="token">The cancellation token to use for the asynchronous operations.</param>
/// <param name="progressUpdater">Optional progress updater for reporting file-level progress to the UI.</param>
/// <returns>A tuple holding the number of succesful copies and a List of the files that failed.</returns>
private static async Task<(long, IEnumerable<IFileEntry>)> CopyAsync(LightWeightBackendManager b_src, LightWeightBackendManager b_dst, IEnumerable<IFileEntry> files, Config config, CancellationToken token)
private static async Task<(long, IEnumerable<IFileEntry>)> CopyAsync(LightWeightBackendManager b_src, LightWeightBackendManager b_dst, IEnumerable<IFileEntry> files, Config config, long filesProcessedOffset, CancellationToken token, IOperationProgressUpdater? progressUpdater = null)
{
long successful_copies = 0;
long sizeProcessed = 0;
List<IFileEntry> 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();