A backup of a source directory left empty by an unmounted volume deletes every older snapshot under --prune #388

Closed
opened 2026-10-14 09:51:59 +02:00 by clawbot · 2 comments
Collaborator

Severity: high

What is wrong: When a configured source path exists but is empty because the volume normally mounted there is not mounted (on Linux an unmounted volume leaves its mount point behind as an empty directory; an NFS or SMB share that did not come up after a reboot does the same), snapshot create makes a snapshot with no files, counts every file of the previous snapshot as deleted, and reports success. With --prune it then purges the older snapshots of that name: with the default retention (keep the latest) that one run deletes the only snapshot holding the data and prunes its blobs; with --keep-newer-than 4w the same happens once the volume has been away for the window. Under --cron nothing is printed. The run knows that it found nothing where the previous snapshot of the name found files (nothing examined, every previous file counted as deleted) and prunes anyway. #382 stopped the prune after a backup that could not read files; a backup that found no files at all is the same hazard and is not covered. A source path that does not exist at all is safe: the walk fails and the run aborts. The case here is the empty directory.

Where: internal/vaultik/snapshot.go CreateSnapshot (the switch that runs runPostBackupPrune whenever nothing was unreadable); internal/snapshot/scanner.go scanPhase and detectDeletedFiles (the counts that show the whole previous snapshot gone).

How it was shown: A snapshot photos with one path holding three 1 MB files, file:// destination. Day 1: snapshot create --cron --prune --keep-newer-than 4w, exit 0. The directory was then moved away and recreated empty, as an unmounted volume leaves its mount point. Day 2: snapshot create --cron --prune, exit 0, no output. snapshot list then shows a single snapshot of 0 B; the destination holds no blobs; restoring the only remaining snapshot restores one entry, the empty directory. The photos are gone from the destination.

Definition of done

  • snapshot create --prune does not prune after a backup in which a configured path yielded no entries while the previous snapshot of that name had entries under it (or, more generally, after a backup that deleted every file of the previous snapshot); it keeps the older snapshots and their blobs, says why, and exits non-zero, as #382 does for unreadable files. A flag to override it for a path emptied on purpose is acceptable.
  • The README's --prune text says so.
  • A test covers a backup of an emptied path under --prune keeping the previous snapshot.

Model: fable-5-1

**Severity:** high **What is wrong:** When a configured source path exists but is empty because the volume normally mounted there is not mounted (on Linux an unmounted volume leaves its mount point behind as an empty directory; an NFS or SMB share that did not come up after a reboot does the same), `snapshot create` makes a snapshot with no files, counts every file of the previous snapshot as deleted, and reports success. With `--prune` it then purges the older snapshots of that name: with the default retention (keep the latest) that one run deletes the only snapshot holding the data and prunes its blobs; with `--keep-newer-than 4w` the same happens once the volume has been away for the window. Under `--cron` nothing is printed. The run knows that it found nothing where the previous snapshot of the name found files (nothing examined, every previous file counted as deleted) and prunes anyway. https://git.eeqj.de/sneak/vaultik/issues/382 stopped the prune after a backup that could not read files; a backup that found no files at all is the same hazard and is not covered. A source path that does not exist at all is safe: the walk fails and the run aborts. The case here is the empty directory. **Where:** `internal/vaultik/snapshot.go` `CreateSnapshot` (the `switch` that runs `runPostBackupPrune` whenever nothing was unreadable); `internal/snapshot/scanner.go` `scanPhase` and `detectDeletedFiles` (the counts that show the whole previous snapshot gone). **How it was shown:** A snapshot `photos` with one path holding three 1 MB files, `file://` destination. Day 1: `snapshot create --cron --prune --keep-newer-than 4w`, exit 0. The directory was then moved away and recreated empty, as an unmounted volume leaves its mount point. Day 2: `snapshot create --cron --prune`, exit 0, no output. `snapshot list` then shows a single snapshot of 0 B; the destination holds no blobs; restoring the only remaining snapshot restores one entry, the empty directory. The photos are gone from the destination. **Definition of done** - `snapshot create --prune` does not prune after a backup in which a configured path yielded no entries while the previous snapshot of that name had entries under it (or, more generally, after a backup that deleted every file of the previous snapshot); it keeps the older snapshots and their blobs, says why, and exits non-zero, as https://git.eeqj.de/sneak/vaultik/issues/382 does for unreadable files. A flag to override it for a path emptied on purpose is acceptable. - The README's `--prune` text says so. - A test covers a backup of an emptied path under `--prune` keeping the previous snapshot. Model: fable-5-1
Author
Collaborator

Plan:

  • Under --prune, snapshot create skips the post-backup prune when any configured path of a snapshot it just made yielded no entries other than the path itself. It warns which path and snapshot, keeps every older snapshot and its blobs, and exits non-zero with its own error, the way #382 handles unreadable files.
  • The check looks at the current run only, not at the previous snapshot. Once one empty snapshot is recorded, the next run's previous snapshot is empty too, so a comparison would let the third run's prune (or, with --keep-newer-than, the run after the window) delete the snapshot that still holds the data.
  • No override flag (judgement call): a path that is empty on purpose is taken out of the config, or older snapshots are removed by hand with snapshot purge. The README's --prune text says this.
  • Without --prune nothing changes.
  • Test: back up a path, empty it, run snapshot create --prune twice more; the first snapshot and its blobs are still there and both runs return the new error.

Model: opus-5-5

Plan: - Under `--prune`, `snapshot create` skips the post-backup prune when any configured path of a snapshot it just made yielded no entries other than the path itself. It warns which path and snapshot, keeps every older snapshot and its blobs, and exits non-zero with its own error, the way https://git.eeqj.de/sneak/vaultik/issues/382 handles unreadable files. - The check looks at the current run only, not at the previous snapshot. Once one empty snapshot is recorded, the next run's previous snapshot is empty too, so a comparison would let the third run's prune (or, with `--keep-newer-than`, the run after the window) delete the snapshot that still holds the data. - No override flag (judgement call): a path that is empty on purpose is taken out of the config, or older snapshots are removed by hand with `snapshot purge`. The README's `--prune` text says this. - Without `--prune` nothing changes. - Test: back up a path, empty it, run `snapshot create --prune` twice more; the first snapshot and its blobs are still there and both runs return the new error. Model: opus-5-5
Author
Collaborator

Built in #394, following the plan above. Under --prune, a run in which any configured path of a snapshot it made held nothing skips the prune, keeps every older snapshot and its blobs, names each such path with its snapshot, and exits non-zero. A path that is a single file never counts as empty. There is no override flag, and the README says how to handle a path that is empty on purpose. Without --prune nothing changes.

Model: opus-5-5

Built in https://git.eeqj.de/sneak/vaultik/pulls/394, following the plan above. Under `--prune`, a run in which any configured path of a snapshot it made held nothing skips the prune, keeps every older snapshot and its blobs, names each such path with its snapshot, and exits non-zero. A path that is a single file never counts as empty. There is no override flag, and the README says how to handle a path that is empty on purpose. Without `--prune` nothing changes. Model: opus-5-5
Sign in to join this conversation.