check / check (push) Waiting to run
Every backup now writes vaultik-destination.txt at the destination root. While the local index holds snapshots, a destination without it is not present: snapshot create refuses before writing, prune and snapshot purge fail, and snapshot list and snapshot remove warn. remote nuke deletes the marker last. The file:// backend no longer creates its destination directory and checks for it on every write; that protects a directory on a removable volume, not the mount point itself. Tests back up into existing directories; fixtures standing for a written destination carry the marker. Judgement call: the marker is rewritten on every backup. Deviation: for the complexity limit, snapshot name selection left CreateSnapshot and the --force/--json test moved into confirmRemoveSnapshot, both unchanged. Model: opus-5-5
105 lines
2.5 KiB
Go
105 lines
2.5 KiB
Go
package storage_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/vaultik/internal/storage"
|
|
)
|
|
|
|
// newFileStorer builds a file:// backend rooted at a fresh temp directory.
|
|
//
|
|
//nolint:ireturn // conformance runs against the Storer interface by design
|
|
func newFileStorer(t *testing.T) storage.Storer {
|
|
t.Helper()
|
|
|
|
s, err := storage.NewFileStorer(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("NewFileStorer: %v", err)
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// TestFileStorer runs the shared Storer contract against the file:// backend.
|
|
func TestFileStorer(t *testing.T) {
|
|
t.Parallel()
|
|
runStorerConformance(t, newFileStorer)
|
|
}
|
|
|
|
// TestFileStorerListMissingDestination checks that List and ListStream
|
|
// fail when the destination directory does not exist, instead of
|
|
// reporting an empty store.
|
|
func TestFileStorerListMissingDestination(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
s, err := storage.NewFileStorer(filepath.Join(t.TempDir(), "unmounted"))
|
|
if err != nil {
|
|
t.Fatalf("NewFileStorer: %v", err)
|
|
}
|
|
|
|
keys, err := s.List(ctx, "metadata/")
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
t.Errorf("List = %v, %v; want a not-exist error", keys, err)
|
|
}
|
|
|
|
var streamErr error
|
|
|
|
for object := range s.ListStream(ctx, "metadata/") {
|
|
if object.Err != nil {
|
|
streamErr = object.Err
|
|
}
|
|
}
|
|
|
|
if !errors.Is(streamErr, fs.ErrNotExist) {
|
|
t.Errorf("ListStream error = %v, want a not-exist error", streamErr)
|
|
}
|
|
}
|
|
|
|
// TestFileStorerPutMissingDestination checks that a write fails when the
|
|
// destination directory has gone, even after earlier writes to it
|
|
// succeeded, and that it does not create the directory again.
|
|
func TestFileStorerPutMissingDestination(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
base := filepath.Join(t.TempDir(), "usbstick")
|
|
|
|
err := os.Mkdir(base, 0o750)
|
|
if err != nil {
|
|
t.Fatalf("Mkdir: %v", err)
|
|
}
|
|
|
|
s, err := storage.NewFileStorer(base)
|
|
if err != nil {
|
|
t.Fatalf("NewFileStorer: %v", err)
|
|
}
|
|
|
|
err = s.Put(ctx, "blobs/aa/bb/first", strings.NewReader("first"))
|
|
if err != nil {
|
|
t.Fatalf("first Put: %v", err)
|
|
}
|
|
|
|
err = os.Rename(base, base+"-unplugged")
|
|
if err != nil {
|
|
t.Fatalf("Rename: %v", err)
|
|
}
|
|
|
|
err = s.Put(ctx, "blobs/aa/bb/second", strings.NewReader("second"))
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
t.Errorf("Put = %v, want a not-exist error", err)
|
|
}
|
|
|
|
_, err = os.Stat(base)
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
t.Errorf("Put created the destination directory again (Stat = %v)", err)
|
|
}
|
|
}
|