fix(chunker): remove empty parent dirs on download delete (#21) - #23
Conversation
Addresses issue #21: parent directories left behind on receiving devices when a synced file is deleted. Design walks parents upward from Chunker::delete using strict-empty semantics.
Closes #21. When the syncer downloads a delete event, walk up from the removed file and remove empty parent directories using fs::remove_dir (which natively rejects non-empty dirs, giving us strict-empty semantics). Stop at the storage root and on any error so a sibling file or permissions issue never fails the download.
…eanups Address review of #21 fix: the `dir == self.base_path` stop guard is lexical, which is safe under the current trusted-server contract (paths from WalkDir don't contain `..`) but worth documenting so a future weakening of that contract doesn't silently regress. Also surface skipped cleanups via trace! — the dominant cause is ENOTEMPTY (normal) but field reports of leftover folders need a signal beyond "directory still on disk" to diagnose.
Code ReviewOverviewThis PR correctly fixes issue #21 by walking parent directories after Issues to address before merging1. Use The SAFETY comment itself flags this risk: if
while let Some(dir) = parent {
if !dir.starts_with(&self.base_path) || dir == self.base_path {
break;
}
// ...
}Or simply: if dir == self.base_path || !dir.starts_with(&self.base_path) {
break;
}This aligns with the SAFETY comment's own recommendation and removes the need for the trust-assumption paragraph. 2. The
Consider dropping both files from this PR (the relevant design rationale is already captured in the SAFETY comment and the PR description). Suggestions (non-blocking)3. Trim the in-code comment block The // Remove now-empty parent directories up to (but not including) the
// storage root. `remove_dir` fails atomically on non-empty dirs
// (ENOTEMPTY is the normal exit, not an error to propagate).
//
// SAFETY: path comparison is lexical. Sound because `full_path` is
// built with `base_path.push(path)` and server paths are trusted
// not to contain `..` (see indexer's WalkDir usage). Use
// `starts_with` if that contract ever changes.4. Missing test: file-not-found on the file itself returns an error The four tests cover the happy path and all three stop conditions for the walk. They don't cover the case where What's working well
Summary: The implementation logic is correct. The two blocking items are (1) using |
Summary
Closes #21. When the syncer downloads a file-delete event, parent
directories that become empty as a result are now also removed —
fixing ghost folders left behind on receiving devices.
Chunker::deletewalks parents upward afterfs::remove_fileandcalls
fs::remove_dir(which natively fails on non-empty dirs,giving strict-empty semantics). Stops at the storage root and on
any error (ENOTEMPTY is the normal terminating condition).
ancestor, storage-root preservation, sibling preservation.
// SAFETY:comment documents the trusted-server assumptionbehind the lexical
dir == base_pathguard.trace!log on theswallowed
remove_direrror path aids field debugging.docs/superpowers/specs/2026-05-18-empty-folder-cleanup-design.mddocs/superpowers/plans/2026-05-18-empty-folder-cleanup.mdTest Plan
cargo test -p cooklang-sync-client— 125 passed, 0 failed