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; }
}
}