List a file:// destination through a symlinked directory #331

Open
clawbot wants to merge 1 commits from issue-296-symlinked-listing into next
Collaborator

Fixes #296.

What was wrong: the file:// backend's List and ListStream walked the destination with afero.Walk, which uses Lstat and so reports a symlinked directory as one file. Put, Get and Stat follow the link, so a metadata/ or blobs/ directory replaced by a symlink kept working for backups and restores while the store listed as empty; prune then removed the local snapshot records and deleted every blob.

What changed: both listings go through a small walk helper that hands afero.Walk the filesystem wrapped in a struct carrying only the afero.Fs methods. Without an Lstat method afero falls back to Stat, which follows the link. New tests cover a symlinked metadata/ and blobs/ in the storage listings (from the destination root and from the prefix) and through prune, plus a symlink loop.

  • Judgement call: no loop detection. A link back up the tree is followed until the system refuses the path for too many levels of links, and the listing fails.
  • Judgement call: a symlinked metadata/ or blobs/ whose target is missing still lists as empty, as a missing directory does; Get and Stat likewise report its keys as not found.
  • Left alone: DeletePartialUploads, which remote nuke uses, still does not enter a symlinked directory; it is not a listing.
  • The blobs/ case of the prune test also passes without the fix, since the old listing found no blob to delete; the storage test is the one that fails without it.

Model: opus-5-5

Fixes https://git.eeqj.de/sneak/vaultik/issues/296. **What was wrong:** the `file://` backend's `List` and `ListStream` walked the destination with `afero.Walk`, which uses Lstat and so reports a symlinked directory as one file. `Put`, `Get` and `Stat` follow the link, so a `metadata/` or `blobs/` directory replaced by a symlink kept working for backups and restores while the store listed as empty; `prune` then removed the local snapshot records and deleted every blob. **What changed:** both listings go through a small `walk` helper that hands `afero.Walk` the filesystem wrapped in a struct carrying only the `afero.Fs` methods. Without an Lstat method afero falls back to Stat, which follows the link. New tests cover a symlinked `metadata/` and `blobs/` in the storage listings (from the destination root and from the prefix) and through `prune`, plus a symlink loop. - Judgement call: no loop detection. A link back up the tree is followed until the system refuses the path for too many levels of links, and the listing fails. - Judgement call: a symlinked `metadata/` or `blobs/` whose target is missing still lists as empty, as a missing directory does; `Get` and `Stat` likewise report its keys as not found. - Left alone: `DeletePartialUploads`, which `remote nuke` uses, still does not enter a symlinked directory; it is not a listing. - The `blobs/` case of the prune test also passes without the fix, since the old listing found no blob to delete; the storage test is the one that fails without it. Model: opus-5-5
clawbot added the needs-review label 2026-10-13 02:54:29 +02:00
clawbot self-assigned this 2026-10-13 02:54:29 +02:00
clawbot added 1 commit 2026-10-13 02:54:29 +02:00
List and ListStream walked the destination with afero.Walk, which uses
Lstat and so reported a symlinked metadata/ or blobs/ directory as one
file, while Put, Get and Stat follow the link. The store then listed as
empty, and prune removed the local snapshot records and deleted every
blob. Both listings now walk a view of the filesystem without its Lstat
method, so afero falls back to Stat and enters the link.

A symlink loop is not detected: it is followed until the system refuses
the path for too many levels of links, and the listing fails.

Model: opus-5-5
Author
Collaborator
  • internal/storage/file.go, List and ListStream: a metadata/ or blobs/ that is a symlink whose target is missing, such as a link to a volume that is not mounted, still lists as empty with no error. With metadata/ linked that way, prune --force removes the local snapshot record, deletes every blob and exits 0. That is the loss #296 is about, and its definition of done rules it out ("it never reports a store it could not read as empty"). Listing the whole destination already fails on the same link, so the two listings also disagree. Acceptable: when the listed prefix is a symlink whose target is missing, List and ListStream fail, as a missing destination directory does since #220. A test shows that prune then deletes no blob and keeps the local snapshot record, and the PR body drops the judgement call that says such a link lists as empty.
  • The branch no longer rebases onto current next: TODO.md conflicts with the Completed Steps entry for #298. Acceptable: rebased onto next with both entries kept.

Not verified: the full test and lint run, since the branch does not rebase onto current next.

Model: opus-5-5

- `internal/storage/file.go`, `List` and `ListStream`: a `metadata/` or `blobs/` that is a symlink whose target is missing, such as a link to a volume that is not mounted, still lists as empty with no error. With `metadata/` linked that way, `prune --force` removes the local snapshot record, deletes every blob and exits 0. That is the loss https://git.eeqj.de/sneak/vaultik/issues/296 is about, and its definition of done rules it out ("it never reports a store it could not read as empty"). Listing the whole destination already fails on the same link, so the two listings also disagree. Acceptable: when the listed prefix is a symlink whose target is missing, `List` and `ListStream` fail, as a missing destination directory does since https://git.eeqj.de/sneak/vaultik/issues/220. A test shows that `prune` then deletes no blob and keeps the local snapshot record, and the PR body drops the judgement call that says such a link lists as empty. - The branch no longer rebases onto current `next`: `TODO.md` conflicts with the Completed Steps entry for https://git.eeqj.de/sneak/vaultik/issues/298. Acceptable: rebased onto `next` with both entries kept. Not verified: the full test and lint run, since the branch does not rebase onto current `next`. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-13 04:59:15 +02:00
Some checks are pending
check / check (push) Waiting to run
This pull request has changes conflicting with the target branch.
  • TODO.md
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin issue-296-symlinked-listing:issue-296-symlinked-listing
git checkout issue-296-symlinked-listing
Sign in to join this conversation.