feat(fs): delta savings in sync stats + exclude interrupted-sync temp files

- Add SyncStats.file_bytes: total uncompressed size of transferred files;
  lets CLI compute CDC delta savings (e.g. "1.2 MB file bytes, 45 KB wire, 96% savings")
- Update cmd_sync summary to show "X file bytes (Y wire, Z% delta savings)" when
  delta savings are non-trivial; falls back to plain "X bytes" for cold copies
- Exclude *.tmp.clawsync from FsManifest::build() unconditionally so leftover
  temp files from interrupted syncs never appear in manifests or get transferred
- Add manifest unit test confirming temp file exclusion

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
osobh
2026-04-05 12:09:36 -05:00
co-authored by Claude Sonnet 4.6
parent b8365a4d41
commit e2aa5a96a5
3 changed files with 51 additions and 4 deletions
+12 -1
View File
@@ -1215,12 +1215,23 @@ async fn cmd_sync(
mods.len(), mods.len(),
rems.len() rems.len()
); );
} else {
let wire = stats.bytes_transferred;
let files = stats.file_bytes;
if files > 0 && wire < files {
let savings_pct = 100u64 - wire * 100 / files;
println!(
"Sync complete: {} added, {} modified, {} removed, {} file bytes ({} wire, {}% delta savings).",
stats.files_added, stats.files_modified, stats.files_removed,
files, wire, savings_pct
);
} else { } else {
println!( println!(
"Sync complete: {} added, {} modified, {} removed, {} bytes.", "Sync complete: {} added, {} modified, {} removed, {} bytes.",
stats.files_added, stats.files_modified, stats.files_removed, stats.bytes_transferred stats.files_added, stats.files_modified, stats.files_removed, wire
); );
} }
}
Ok(()) Ok(())
} }
+20
View File
@@ -60,6 +60,12 @@ impl FsManifest {
if !rel.is_empty() && excludes.is_match(&rel) { if !rel.is_empty() && excludes.is_match(&rel) {
continue; continue;
} }
// Always exclude clawsync temp files regardless of caller excludes.
// These are leftover from interrupted syncs and must never appear
// in manifests or be transferred to peers.
if rel.ends_with(".tmp.clawsync") {
continue;
}
candidates.push((abs, rel)); candidates.push((abs, rel));
} }
@@ -192,4 +198,18 @@ mod tests {
.collect(); .collect();
assert_eq!(paths, vec!["a.bin", "m.bin", "z.bin"]); assert_eq!(paths, vec!["a.bin", "m.bin", "z.bin"]);
} }
#[test]
fn build_always_excludes_clawsync_temp_files() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("real.bin"), b"real content").unwrap();
// Simulate a leftover temp file from an interrupted sync.
fs::write(dir.path().join("real.tmp.clawsync"), b"incomplete").unwrap();
fs::write(dir.path().join("other.bin.tmp.clawsync"), b"also incomplete").unwrap();
let manifest = FsManifest::build(dir.path(), &empty_excludes()).unwrap();
let paths: Vec<&str> = manifest.entries.iter().map(|e| e.rel_path.as_str()).collect();
assert_eq!(paths, vec!["real.bin"], "temp files must be excluded from manifest");
}
} }
+16
View File
@@ -59,7 +59,14 @@ pub struct SyncStats {
pub files_added: u32, pub files_added: u32,
pub files_modified: u32, pub files_modified: u32,
pub files_removed: u32, pub files_removed: u32,
/// Actual bytes sent over the wire (compressed literal chunks only).
pub bytes_transferred: u64, pub bytes_transferred: u64,
/// Total uncompressed size of every file that was added or modified.
///
/// Comparing `file_bytes` to `bytes_transferred` shows the CDC delta
/// savings (e.g. 96% of file_bytes skipped because blocks already exist
/// on the server). Zero when no files are transferred.
pub file_bytes: u64,
/// Populated only in dry-run mode: paths that would be added. /// Populated only in dry-run mode: paths that would be added.
pub would_add: Option<Vec<String>>, pub would_add: Option<Vec<String>>,
/// Populated only in dry-run mode: paths that would be modified. /// Populated only in dry-run mode: paths that would be modified.
@@ -149,6 +156,7 @@ impl FsSyncClient {
files_modified: would_modify.len() as u32, files_modified: would_modify.len() as u32,
files_removed: would_remove.len() as u32, files_removed: would_remove.len() as u32,
bytes_transferred: 0, bytes_transferred: 0,
file_bytes: 0,
would_add: Some(would_add), would_add: Some(would_add),
would_modify: Some(would_modify), would_modify: Some(would_modify),
would_remove: Some(would_remove), would_remove: Some(would_remove),
@@ -197,6 +205,13 @@ impl FsSyncClient {
// The semaphore (W=16) limits in-flight FsCdcData messages. // The semaphore (W=16) limits in-flight FsCdcData messages.
let total = needed_files.len(); let total = needed_files.len();
// Sum up the uncompressed sizes of files we'll transfer (from the manifest).
// This lets us report delta savings alongside the wire byte count.
let file_bytes: u64 = needed_files
.iter()
.filter_map(|f| manifest.get(&f.path))
.map(|e| e.size)
.sum();
let progress_tx = self.progress_tx.take(); let progress_tx = self.progress_tx.take();
if let Some(tx) = &progress_tx { if let Some(tx) = &progress_tx {
let _ = tx.send(ProgressEvent::TotalFiles(total)); let _ = tx.send(ProgressEvent::TotalFiles(total));
@@ -297,6 +312,7 @@ impl FsSyncClient {
files_modified, files_modified,
files_removed, files_removed,
bytes_transferred, bytes_transferred,
file_bytes,
..Default::default() ..Default::default()
}, },
other => { other => {