diff --git a/Duplicati/Library/Interface/ResultInterfaces.cs b/Duplicati/Library/Interface/ResultInterfaces.cs index 94af622f6..ecf26ea17 100644 --- a/Duplicati/Library/Interface/ResultInterfaces.cs +++ b/Duplicati/Library/Interface/ResultInterfaces.cs @@ -579,8 +579,64 @@ namespace Duplicati.Library.Interface { } + /// + /// Results of a sync operation. + /// public interface ISyncResults : IBasicResults { + /// + /// The number of folders created on the remote destination. + /// + long FoldersCreated { get; } + + /// + /// The number of folders deleted from the remote destination. + /// + long FoldersDeleted { get; } + + /// + /// The number of files uploaded to the remote destination (new files plus + /// updated files whose content changed). + /// + long FilesUploaded { get; } + + /// + /// The number of files that were examined but left unchanged because the + /// local and remote content already matched (size/mtime, or hash when + /// --sync-verify-hash is set). + /// + long UnchangedFiles { get; } + + /// + /// The number of files deleted from the remote destination (unknown remote + /// files removed when --sync-then-delete is set). + /// + long FilesDeleted { get; } + + /// + /// The total number of local source files encountered during enumeration + /// across all folders, regardless of whether they were uploaded, unchanged, + /// or skipped. + /// + long SourceFiles { get; } + + /// + /// The total size in bytes of all local source files encountered during + /// enumeration, regardless of whether they were uploaded or left unchanged. + /// + long SizeOfSourceFiles { get; } + + /// + /// The total size in bytes of the files uploaded to the remote destination + /// (new files plus updated files whose content changed). + /// + long SizeOfUploadedFiles { get; } + + /// + /// The total size in bytes of the files deleted from the remote destination + /// (unknown remote files removed when --sync-then-delete is set). + /// + long SizeOfDeletedFiles { get; } } public interface ISetLockResults : IBasicResults diff --git a/Duplicati/Library/Main/IPC/Dto/ResultsDto.cs b/Duplicati/Library/Main/IPC/Dto/ResultsDto.cs index 51ddf6082..057fd6111 100644 --- a/Duplicati/Library/Main/IPC/Dto/ResultsDto.cs +++ b/Duplicati/Library/Main/IPC/Dto/ResultsDto.cs @@ -266,6 +266,16 @@ public class VacuumResultsDto : BasicResultsDto [Serializable] public class SyncResultsDto : BasicResultsDto { + public long FoldersCreated { get; set; } + public long FoldersDeleted { get; set; } + public long FilesUploaded { get; set; } + public long UnchangedFiles { get; set; } + public long FilesDeleted { get; set; } + public long SourceFiles { get; set; } + public long SizeOfSourceFiles { get; set; } + public long SizeOfUploadedFiles { get; set; } + public long SizeOfDeletedFiles { get; set; } + public static SyncResultsDto FromResults(ISyncResults results) { if (results == null) return null; @@ -278,7 +288,16 @@ public class SyncResultsDto : BasicResultsDto Warnings = results.Warnings?.ToList() ?? new List(), Messages = results.Messages?.ToList() ?? new List(), ParsedResult = results.ParsedResult, - Interrupted = results.Interrupted + Interrupted = results.Interrupted, + FoldersCreated = results.FoldersCreated, + FoldersDeleted = results.FoldersDeleted, + FilesUploaded = results.FilesUploaded, + UnchangedFiles = results.UnchangedFiles, + FilesDeleted = results.FilesDeleted, + SourceFiles = results.SourceFiles, + SizeOfSourceFiles = results.SizeOfSourceFiles, + SizeOfUploadedFiles = results.SizeOfUploadedFiles, + SizeOfDeletedFiles = results.SizeOfDeletedFiles }; } } diff --git a/Duplicati/Library/Main/IPC/ResultWrappers.cs b/Duplicati/Library/Main/IPC/ResultWrappers.cs index 747dd1d85..341d4b878 100644 --- a/Duplicati/Library/Main/IPC/ResultWrappers.cs +++ b/Duplicati/Library/Main/IPC/ResultWrappers.cs @@ -162,6 +162,18 @@ public class VacuumResultsWrapper : BasicResultsWrapper, IVacuumResults public class SyncResultsWrapper : BasicResultsWrapper, ISyncResults { public SyncResultsWrapper(SyncResultsDto dto) : base(dto) { } + + protected new SyncResultsDto _dto => (SyncResultsDto)base._dto; + + public long FoldersCreated => _dto.FoldersCreated; + public long FoldersDeleted => _dto.FoldersDeleted; + public long FilesUploaded => _dto.FilesUploaded; + public long UnchangedFiles => _dto.UnchangedFiles; + public long FilesDeleted => _dto.FilesDeleted; + public long SourceFiles => _dto.SourceFiles; + public long SizeOfSourceFiles => _dto.SizeOfSourceFiles; + public long SizeOfUploadedFiles => _dto.SizeOfUploadedFiles; + public long SizeOfDeletedFiles => _dto.SizeOfDeletedFiles; } /// diff --git a/Duplicati/Library/Main/Operation/Sync/SyncHandler.cs b/Duplicati/Library/Main/Operation/Sync/SyncHandler.cs index 681a1db25..ae3b7307e 100644 --- a/Duplicati/Library/Main/Operation/Sync/SyncHandler.cs +++ b/Duplicati/Library/Main/Operation/Sync/SyncHandler.cs @@ -168,6 +168,24 @@ internal class SyncHandler } Logging.Log.WriteInformationMessage(LOGTAG, "SyncPlan", "Plan: Upload {0}, Update {1}, Delete {2}", planSummary.Upload, planSummary.Update, planSummary.Delete); + + // Flush the running counters into the results so callers (CLI, IPC, web UI) + // see folders created, files uploaded/unchanged/deleted, and the total + // files/size encountered. Upload+Update is the combined "files uploaded" + // count; Delete maps to files deleted from the remote. + m_results.FoldersCreated = planSummary.FoldersCreated; + // The handler deletes unknown remote files only, never remote folders, so + // the folders-deleted count is always zero today; the field is exposed for + // completeness and future folder-deletion support. + m_results.FoldersDeleted = 0; + m_results.FilesUploaded = planSummary.Upload + planSummary.Update; + m_results.UnchangedFiles = planSummary.UnchangedFiles; + m_results.FilesDeleted = planSummary.Delete; + m_results.SourceFiles = planSummary.SourceFiles; + m_results.SizeOfSourceFiles = planSummary.SizeOfSourceFiles; + m_results.SizeOfUploadedFiles = planSummary.SizeOfUploadedFiles; + m_results.SizeOfDeletedFiles = planSummary.SizeOfDeletedFiles; + Logging.Log.WriteInformationMessage(LOGTAG, "SyncFinished", "Sync completed successfully."); } @@ -241,6 +259,12 @@ internal class SyncHandler } else { + // Count every local source file encountered (regardless of whether it + // ends up uploaded, unchanged, or de-duplicated) so SourceFiles and + // SizeOfSourceFiles reflect the full local inventory scanned this run. + planSummary.SourceFiles++; + planSummary.SizeOfSourceFiles += Math.Max(child.Size, 0); + // INSERT OR IGNORE semantics: the first entry seen for a path wins, so a // later duplicate in the same folder is ignored (paths are unique within // a single folder in practice). @@ -403,6 +427,7 @@ internal class SyncHandler Logging.Log.WriteDryrunMessage(LOGTAG, "DryRun", "Would create folder: {0}", subRelPath); else await backendManager.EnsureFolderAsync(subRelPath, ct).ConfigureAwait(false); + planSummary.FoldersCreated++; } folderQueue.Enqueue(new FolderWorkItem(subEntry, subRelPath)); @@ -416,8 +441,14 @@ internal class SyncHandler // either already has files (so it exists) or is brand new, in which case the // sub-folder ensure above created it when it was enqueued as a child. The // backend root (folderRelPath == "") is assumed to exist for all modes. - if (stateMode == SyncRemoteState.BlindlyUpload && !string.IsNullOrEmpty(folderRelPath) && !isDryRun) - await backendManager.EnsureFolderAsync(folderRelPath, ct).ConfigureAwait(false); + // BlindlyUpload always treats the current folder as new (we have no listing + // to prove it exists), so it counts as a created folder when not a dry run. + if (stateMode == SyncRemoteState.BlindlyUpload && !string.IsNullOrEmpty(folderRelPath)) + { + if (!isDryRun) + await backendManager.EnsureFolderAsync(folderRelPath, ct).ConfigureAwait(false); + planSummary.FoldersCreated++; + } // 5. Upload all new or changed files in this folder. The caller guarantees the // current folder exists, so UploadOrUpdateAsync does not call EnsureFolderAsync. @@ -459,7 +490,10 @@ internal class SyncHandler } if (!needsUpload) + { + planSummary.UnchangedFiles++; continue; + } if (hashRecheck) { @@ -470,7 +504,10 @@ internal class SyncHandler { var hash = Convert.ToBase64String(hasher.ComputeHash(s)); if (hash == remoteHash) + { + planSummary.UnchangedFiles++; continue; // Content unchanged; no upload needed. + } } } catch (Exception ex) @@ -530,6 +567,7 @@ internal class SyncHandler { Logging.Log.WriteDryrunMessage(LOGTAG, "DryRun", "Would delete: {0}", relPath); planSummary.Delete++; + planSummary.SizeOfDeletedFiles += Math.Max(rc.Size, 0); continue; } @@ -542,6 +580,7 @@ internal class SyncHandler await backendManager.DeleteAsync(relPath, 0, true, ct).ConfigureAwait(false); deletedPaths.Add(relPath); planSummary.Delete++; + planSummary.SizeOfDeletedFiles += Math.Max(rc.Size, 0); } // Commit the inventory removals and intent clearances for all files deleted @@ -613,6 +652,7 @@ internal class SyncHandler planSummary.Upload++; else planSummary.Update++; + planSummary.SizeOfUploadedFiles += Math.Max(lf.Size, 0); return; } @@ -655,6 +695,7 @@ internal class SyncHandler planSummary.Upload++; else planSummary.Update++; + planSummary.SizeOfUploadedFiles += Math.Max(lf.Size, 0); } /// @@ -759,12 +800,19 @@ internal class SyncHandler /// /// Running counters for the plan summary log, bumped per operation as it is - /// planned/performed so the final log line reflects the whole run. + /// planned/performed so the final log line reflects the whole run. The same + /// counters are flushed into at the end of the run. /// private sealed class PlanSummary { public long Upload; public long Update; public long Delete; + public long FoldersCreated; + public long UnchangedFiles; + public long SourceFiles; + public long SizeOfSourceFiles; + public long SizeOfUploadedFiles; + public long SizeOfDeletedFiles; } } diff --git a/Duplicati/Library/Main/ResultClasses.cs b/Duplicati/Library/Main/ResultClasses.cs index d0f7f902c..5d97acaa8 100644 --- a/Duplicati/Library/Main/ResultClasses.cs +++ b/Duplicati/Library/Main/ResultClasses.cs @@ -1058,5 +1058,24 @@ namespace Duplicati.Library.Main public SyncResults(BasicResults p) : base(p) { } public override OperationMode MainOperation { get { return OperationMode.Sync; } } + + /// + public long FoldersCreated { get; internal set; } + /// + public long FoldersDeleted { get; internal set; } + /// + public long FilesUploaded { get; internal set; } + /// + public long UnchangedFiles { get; internal set; } + /// + public long FilesDeleted { get; internal set; } + /// + public long SourceFiles { get; internal set; } + /// + public long SizeOfSourceFiles { get; internal set; } + /// + public long SizeOfUploadedFiles { get; internal set; } + /// + public long SizeOfDeletedFiles { get; internal set; } } }