diff --git a/Duplicati/Library/Main/ResultClasses.cs b/Duplicati/Library/Main/ResultClasses.cs index f70399d6e..3c91f5650 100644 --- a/Duplicati/Library/Main/ResultClasses.cs +++ b/Duplicati/Library/Main/ResultClasses.cs @@ -33,6 +33,7 @@ using Newtonsoft.Json; // Expose internal classes to Builtin modules, so they can create result objects [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Duplicati.Library.Modules.Builtin")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Duplicati.Library.RemoteSynchronization")] namespace Duplicati.Library.Main { diff --git a/Duplicati/Library/Modules/Builtin/RemoteSynchronizationModule.cs b/Duplicati/Library/Modules/Builtin/RemoteSynchronizationModule.cs index 39281c3d4..baad4a14e 100644 --- a/Duplicati/Library/Modules/Builtin/RemoteSynchronizationModule.cs +++ b/Duplicati/Library/Modules/Builtin/RemoteSynchronizationModule.cs @@ -388,22 +388,14 @@ public class RemoteSynchronizationModule : IGenericCallbackModuleWithProgress var config = dest.Config with { Src = m_source! }; var date_start = DateTime.UtcNow; - var (exitCode, syncResults) = RemoteSynchronizationRunner.Run(config, CancellationToken.None, progressUpdater, backendProgressUpdater).ConfigureAwait(false).GetAwaiter().GetResult(); - var date_end = DateTime.UtcNow; - - // Create result object for this destination var syncResult = new RemoteSynchronizationResults { Destination = Duplicati.Library.Utility.Utility.GetUrlWithoutCredentials(config.Dst), - DeletedFileCount = syncResults.DeletedFileCount, - RenamedFileCount = syncResults.RenamedFileCount, - CopiedFileCount = syncResults.CopiedFileCount, - VerifiedFileCount = syncResults.VerifiedFileCount, - FailedVerificationCount = syncResults.FailedVerificationCount, - CopiedFileSize = syncResults.CopiedFileSize, - BeginTime = date_start, - EndTime = date_end }; + var exitCode = RemoteSynchronizationRunner.Run(config, CancellationToken.None, progressUpdater, backendProgressUpdater, syncResult).ConfigureAwait(false).GetAwaiter().GetResult(); + var date_end = DateTime.UtcNow; + syncResult.BeginTime = date_start; + syncResult.EndTime = date_end; syncResultsList.Add(syncResult); if (exitCode == 0) diff --git a/Duplicati/Library/RemoteSynchronization/RemoteSynchronizationRunner.cs b/Duplicati/Library/RemoteSynchronization/RemoteSynchronizationRunner.cs index 3dfe71ffb..4d32206e0 100644 --- a/Duplicati/Library/RemoteSynchronization/RemoteSynchronizationRunner.cs +++ b/Duplicati/Library/RemoteSynchronization/RemoteSynchronizationRunner.cs @@ -151,12 +151,13 @@ namespace RemoteSynchronization /// The cancellation token to use for the asynchronous operations. /// Optional progress updater for reporting file count and transfer progress to the UI. /// Optional backend progress updater for reporting transfer speed to the UI. - /// A tuple containing the return code (0 on success) and the synchronization results. - public static async Task<(int, SyncResults)> Run(Config config, CancellationToken token, IOperationProgressUpdater? progressUpdater = null, IBackendProgressUpdater? backendProgressUpdater = null) + /// The return code (0 on success). + public static async Task Run(Config config, CancellationToken token, IOperationProgressUpdater? progressUpdater = null, IBackendProgressUpdater? backendProgressUpdater = null, IBasicResults? results = null) { // Parse the log level var log_level_parsed = Enum.TryParse(config.LogLevel, true, out var log_level_enum); log_level_enum = log_level_parsed ? log_level_enum : Duplicati.Library.Logging.LogMessageType.Information; + var rsyncResults = results as RemoteSynchronizationResults; if (!string.IsNullOrEmpty(config.LogFile)) { @@ -171,16 +172,42 @@ namespace RemoteSynchronization SystemIO.IO_OS.DirectoryCreate(log_file_dir); log_file_sink = new StreamLogDestination(config.LogFile); } - using var multi_sink = new MultiLogDestination([console_sink, log_file_sink]); - // Start the logging scope - using var _ = Duplicati.Library.Logging.Log.StartScope(multi_sink, log_level_enum); + if (rsyncResults is not null) + { + using var multi_log_target = new ControllerMultiLogTarget(rsyncResults, log_level_enum, null, null); + multi_log_target.AddTarget(console_sink, log_level_enum, null); + multi_log_target.AddTarget(log_file_sink, log_level_enum, null); - return await RunCore(config, token, progressUpdater, backendProgressUpdater).ConfigureAwait(false); + // Start the logging scope with both file logging and message capture + using var scope = Duplicati.Library.Logging.Log.StartScope(multi_log_target.WriteMessage); + + return await RunCore(config, token, progressUpdater, backendProgressUpdater, rsyncResults).ConfigureAwait(false); + } + else + { + using var multi_sink = new MultiLogDestination([console_sink, log_file_sink]); + + // Start the logging scope + using var _ = Duplicati.Library.Logging.Log.StartScope(multi_sink, log_level_enum); + + return await RunCore(config, token, progressUpdater, backendProgressUpdater).ConfigureAwait(false); + } } else { - return await RunCore(config, token, progressUpdater, backendProgressUpdater).ConfigureAwait(false); + if (rsyncResults is not null) + { + // Set up logging to capture messages to the results even without a log file + using var log_target = new ControllerMultiLogTarget(rsyncResults, log_level_enum, null, null); + using var scope = Duplicati.Library.Logging.Log.StartScope(log_target.WriteMessage); + + return await RunCore(config, token, progressUpdater, backendProgressUpdater, rsyncResults).ConfigureAwait(false); + } + else + { + return await RunCore(config, token, progressUpdater, backendProgressUpdater).ConfigureAwait(false); + } } } @@ -191,8 +218,8 @@ namespace RemoteSynchronization /// The cancellation token to use for the asynchronous operations. /// Optional progress updater for reporting file count and transfer progress to the UI. /// Optional backend progress updater for reporting transfer speed to the UI. - /// A tuple containing the return code (0 on success) and the synchronization results. - private static async Task<(int, SyncResults)> RunCore(Config config, CancellationToken token, IOperationProgressUpdater? progressUpdater = null, IBackendProgressUpdater? backendProgressUpdater = null) + /// The return code (0 on success). + private static async Task RunCore(Config config, CancellationToken token, IOperationProgressUpdater? progressUpdater = null, IBackendProgressUpdater? backendProgressUpdater = null, RemoteSynchronizationResults? results = null) { // Unpack and parse the multi token options var global_options = ParseOptions(config.GlobalOptions); @@ -213,7 +240,7 @@ namespace RemoteSynchronization if (config.ParseArgumentsOnly) { Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", "Arguments parsed successfully; {0}; exiting.", config); - return (0, new SyncResults(0, 0, 0, 0, 0, 0)); + return 0; } using var b1m = new LightWeightBackendManager(config.Src, src_opts, config.BackendRetries, config.BackendRetryDelay, config.BackendRetryWithExponentialBackoff, progressUpdater: progressUpdater, backendProgressUpdater: backendProgressUpdater); @@ -234,7 +261,7 @@ namespace RemoteSynchronization if (!response?.Equals("y", StringComparison.CurrentCultureIgnoreCase) ?? true) { Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", "Aborted"); - return (-1, new SyncResults(0, 0, 0, 0, 0, 0)); + return -1; } } @@ -271,7 +298,7 @@ namespace RemoteSynchronization if (!response?.Equals("y", StringComparison.CurrentCultureIgnoreCase) ?? true) { Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", "Aborted"); - return (-1, new SyncResults(0, 0, 0, 0, 0, 0)); + return -1; } } @@ -333,40 +360,55 @@ namespace RemoteSynchronization { Duplicati.Library.Logging.Log.WriteErrorMessage(LOGTAG, "rsync", null, "Could not copy {0} files. Not retrying any more.", copy_errors.Count()); - return (copy_errors.Count(), new SyncResults(deleted, renamed, 0, verified, failed_verify, totalFileSize)); + results?.DeletedFileCount = deleted; + results?.RenamedFileCount = renamed; + results?.CopiedFileCount = copied; + results?.VerifiedFileCount = verified; + results?.FailedVerificationCount = failed_verify; + results?.CopiedFileSize = totalFileSize; + return copy_errors.Count(); } } // Results reporting - if (verified > 0) - Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", - "Verified {0} files in {1} that didn't need to be copied", - verified, b2m.DisplayName); - if (failed_verify > 0) - Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", - "Failed to verify {0} files in {1}, which were then attempted to be copied", - failed_verify, b2m.DisplayName); - if (copied > 0) - Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", - "Copied {0} files from {1} to {2}", copied, b1m.DisplayName, b2m.DisplayName); - if (deleted > 0) - Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", - "Deleted {0} files from {1}", deleted, b2m.DisplayName); - if (renamed > 0) - Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", - "Renamed {0} files in {1}", renamed, b2m.DisplayName); + if (results is not null) + { + if (verified > 0) + Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", + "Verified {0} files in {1} that didn't need to be copied", + verified, b2m.DisplayName); + if (failed_verify > 0) + Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", + "Failed to verify {0} files in {1}, which were then attempted to be copied", + failed_verify, b2m.DisplayName); + if (copied > 0) + Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", + "Copied {0} files from {1} to {2}", copied, b1m.DisplayName, b2m.DisplayName); + if (deleted > 0) + Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", + "Deleted {0} files from {1}", deleted, b2m.DisplayName); + if (renamed > 0) + Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", + "Renamed {0} files in {1}", renamed, b2m.DisplayName); + } Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "rsync", "Remote synchronization completed successfully"); - return (0, new SyncResults(deleted, renamed, copied, verified, failed_verify, totalFileSize)); + results?.DeletedFileCount = deleted; + results?.RenamedFileCount = renamed; + results?.CopiedFileCount = copied; + results?.VerifiedFileCount = verified; + results?.FailedVerificationCount = failed_verify; + results?.CopiedFileSize = totalFileSize; + + return 0; } // TODO have concurrency parameters: uploaders, downloaders // TODO low memory mode, where things aren't kept in memory. Maybe utilize SQLite? // TODO For convenience, have the option to launch a "duplicati test" on the destination backend after the synchronization // TODO Save hash to minimize redownload - // TODO Duplicati Results /// /// Copies the files from one backend to another.