1 Commits
Author SHA1 Message Date
sneak fe66b76148 List a missing file:// destination directory as an error (closes #220)
check / check (pull_request) Successful in 6m8s
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.

Three tests listed a file:// destination nothing had created; they now
create it.

Model: opus-5-5
2026-10-06 06:07:35 +00:00
3 changed files with 13 additions and 11 deletions
+9 -6
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, and FileStorer over a directory that does not exist lists // checks. A test that lists the destination must create its directory
// zero objects without erroring. Chunk, blob and compression settings // first, because listing a directory that does not exist is an error.
// are filled in by config.Load. // Chunk, blob and compression settings are filled in by config.Load.
const hermeticConfig = `age_recipients: const hermeticConfig = `age_recipients:
- age1278m9q7dp3chsh2dcy82qk27v047zywyvtxwnj4cvt0z65jw6a7q5dqhfj - age1278m9q7dp3chsh2dcy82qk27v047zywyvtxwnj4cvt0z65jw6a7q5dqhfj
snapshots: snapshots:
@@ -196,22 +196,25 @@ 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 a barren destination as an empty list rather than a // and treats an empty destination directory as an empty list rather
// failure. // than a 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"),
filepath.Join(dir, "store"), storeDir,
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.
+2 -3
View File
@@ -25,9 +25,8 @@ 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: a missing or unmounted // List and ListStream fail on a missing basePath, because listing it as
// destination cannot be listed, and listing it as an empty store would // an empty store would make `prune` drop every local snapshot record.
// 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, as on an unmounted // fail when the destination directory does not exist, instead of
// volume, instead of reporting an empty store. // reporting an empty store.
func TestFileStorerListMissingDestination(t *testing.T) { func TestFileStorerListMissingDestination(t *testing.T) {
t.Parallel() t.Parallel()