1 Commits
Author SHA1 Message Date
sneak b2d0d23927 List a missing file:// destination directory as an error (closes #220)
check / check (pull_request) Successful in 4m41s
The file backend listed a destination directory that does not exist as
an empty store. With the volume unplugged, snapshot list reported every
local snapshot as missing from the store, snapshot remove said it had
removed metadata it never reached, and prune dropped every local
snapshot record. List and ListStream now fail when the destination
directory is missing, so those commands take their existing path for a
store that cannot be listed. A missing prefix under an existing
directory is still an empty listing, and a first backup still creates
the directory.

Two test fixtures listed a file:// destination nothing had created; they
now create it.

Model: opus-5-5
2026-10-06 04:25:12 +00:00
3 changed files with 11 additions and 13 deletions
+6 -9
View File
@@ -172,9 +172,9 @@ func TestBannerSuppressedInArgs(t *testing.T) {
// hermeticConfig is a complete, valid config that needs no network and // hermeticConfig is a complete, valid config that needs no network and
// no credentials: file:// storage is exempt from the S3 credential // no credentials: file:// storage is exempt from the S3 credential
// checks. A test that lists the destination must create its directory // checks, and FileStorer over a directory that does not exist lists
// first, because listing a directory that does not exist is an error. // zero objects without erroring. Chunk, blob and compression settings
// Chunk, blob and compression settings are filled in by config.Load. // are filled in by config.Load.
const hermeticConfig = `age_recipients: const hermeticConfig = `age_recipients:
- age1278m9q7dp3chsh2dcy82qk27v047zywyvtxwnj4cvt0z65jw6a7q5dqhfj - age1278m9q7dp3chsh2dcy82qk27v047zywyvtxwnj4cvt0z65jw6a7q5dqhfj
snapshots: snapshots:
@@ -196,25 +196,22 @@ hostname: test-host
// `snapshot list` is the command chosen because it is the only --json // `snapshot list` is the command chosen because it is the only --json
// command that reaches its document without a populated destination // command that reaches its document without a populated destination
// store: it reads the local index, streams `metadata/` (empty here), // store: it reads the local index, streams `metadata/` (empty here),
// and treats an empty destination directory as an empty list rather // and treats a barren destination as an empty list rather than a
// than a failure. // failure.
// //
// Not parallel: it replaces os.Args, os.Stdout and the xdg globals. // Not parallel: it replaces os.Args, os.Stdout and the xdg globals.
func TestEntryJSONStdoutIsExactlyOneDocument(t *testing.T) { func TestEntryJSONStdoutIsExactlyOneDocument(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
configPath := filepath.Join(dir, "config.yml") configPath := filepath.Join(dir, "config.yml")
storeDir := filepath.Join(dir, "store")
contents := fmt.Sprintf(hermeticConfig, contents := fmt.Sprintf(hermeticConfig,
filepath.Join(dir, "source"), filepath.Join(dir, "source"),
storeDir, filepath.Join(dir, "store"),
filepath.Join(dir, "index.sqlite")) filepath.Join(dir, "index.sqlite"))
require.NoError(t, require.NoError(t,
os.WriteFile(configPath, []byte(contents), configFileMode)) os.WriteFile(configPath, []byte(contents), configFileMode))
require.NoError(t, os.Mkdir(storeDir, 0o750))
// The PID lock lives under xdg.DataHome, which xdg resolves at // The PID lock lives under xdg.DataHome, which xdg resolves at
// package init; point it at the temp dir so the test neither // package init; point it at the temp dir so the test neither
// touches nor collides with the real one. // touches nor collides with the real one.
+3 -2
View File
@@ -25,8 +25,9 @@ type FileStorer struct {
// write. Write operations (Put/PutWithProgress) call MkdirAll for the // write. Write operations (Put/PutWithProgress) call MkdirAll for the
// per-blob parent directory, which also covers basePath on first use. // per-blob parent directory, which also covers basePath on first use.
// Get and Stat report a key under a missing basePath as ErrNotFound. // Get and Stat report a key under a missing basePath as ErrNotFound.
// List and ListStream fail on a missing basePath, because listing it as // List and ListStream fail on a missing basePath: a missing or unmounted
// an empty store would make `prune` drop every local snapshot record. // destination cannot be listed, and listing it as an empty store would
// make `prune` drop every local snapshot record.
// //
// Uses the real OS filesystem by default; call SetFilesystem to // Uses the real OS filesystem by default; call SetFilesystem to
// override for testing. // override for testing.
+2 -2
View File
@@ -31,8 +31,8 @@ func TestFileStorer(t *testing.T) {
} }
// TestFileStorerListMissingDestination checks that List and ListStream // TestFileStorerListMissingDestination checks that List and ListStream
// fail when the destination directory does not exist, instead of // fail when the destination directory does not exist, as on an unmounted
// reporting an empty store. // volume, instead of reporting an empty store.
func TestFileStorerListMissingDestination(t *testing.T) { func TestFileStorerListMissingDestination(t *testing.T) {
t.Parallel() t.Parallel()