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();