check / check (push) Waiting to run
Every backup now writes vaultik-destination.txt at the root of the backup destination store. 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, as for a destination that cannot be listed. remote nuke deletes the marker last. The file:// backend no longer creates its destination directory and checks for it on every write. Tests that backed up into a directory they never created now use an existing one; fixtures standing for a written destination carry the marker. Judgement call: the marker is rewritten on every backup. Deviation: snapshot name selection moved out of CreateSnapshot unchanged, for the complexity limit. Model: opus-5-5
124 lines
3.6 KiB
Go
124 lines
3.6 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/config"
|
|
"sneak.berlin/go/vaultik/internal/database"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
"sneak.berlin/go/vaultik/internal/storage"
|
|
"sneak.berlin/go/vaultik/internal/ui"
|
|
"sneak.berlin/go/vaultik/internal/vaultik"
|
|
)
|
|
|
|
// openFailFs is the real filesystem, except that opening path fails
|
|
// with err. Phase 1 of a backup only lstats a file, so it still counts
|
|
// path; phase 2 is the first to open it.
|
|
type openFailFs struct {
|
|
afero.OsFs
|
|
|
|
path string
|
|
err error
|
|
}
|
|
|
|
//nolint:ireturn // afero.Fs.Open is defined to return the interface.
|
|
func (f *openFailFs) Open(name string) (afero.File, error) {
|
|
if name == f.path {
|
|
return nil, &os.PathError{Op: "open", Path: name, Err: f.err}
|
|
}
|
|
|
|
return f.OsFs.Open(name)
|
|
}
|
|
|
|
// A file that phase 2 cannot open is reported as failed, not as
|
|
// unchanged, and neither the summary's data total nor the snapshots row
|
|
// counts it. See https://git.eeqj.de/sneak/vaultik/issues/280.
|
|
func TestSnapshotSummaryCountsFileNotStoredAsFailed(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
openErr error
|
|
skipErrors bool
|
|
}{
|
|
// What a normal user gets opening a file with mode 000.
|
|
{name: "unopenable under skip-errors",
|
|
openErr: os.ErrPermission, skipErrors: true},
|
|
// What opening a file removed after phase 1 gives.
|
|
{name: "removed between the phases",
|
|
openErr: os.ErrNotExist, skipErrors: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
// The scan walks the source path with symlinks resolved, so
|
|
// failedPath must be spelled the same way to match.
|
|
tempDir, err := filepath.EvalSymlinks(t.TempDir())
|
|
require.NoError(t, err)
|
|
|
|
srcDir := filepath.Join(tempDir, "src")
|
|
failedPath := filepath.Join(srcDir, "failed.txt")
|
|
storedContent := []byte("this file is backed up")
|
|
storedSize := int64(len(storedContent))
|
|
|
|
fs := &openFailFs{path: failedPath, err: tt.openErr}
|
|
require.NoError(t, fs.MkdirAll(srcDir, 0o755))
|
|
require.NoError(t, afero.WriteFile(fs,
|
|
filepath.Join(srcDir, "stored.txt"), storedContent, 0o644))
|
|
require.NoError(t, afero.WriteFile(fs,
|
|
failedPath, []byte("this file cannot be opened"), 0o644))
|
|
|
|
cfg := faultTestConfig()
|
|
cfg.IndexPath = filepath.Join(tempDir, "index.sqlite")
|
|
cfg.Snapshots = map[string]config.SnapshotConfig{
|
|
"src": {Paths: []string{srcDir}},
|
|
}
|
|
|
|
store, err := storage.NewFileStorer(t.TempDir())
|
|
require.NoError(t, err)
|
|
|
|
db, err := database.New(ctx, cfg.IndexPath)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
repos := database.NewRepositories(db)
|
|
out := &bytes.Buffer{}
|
|
v := newBackupVaultik(ctx, cfg, store, repos, db, fs)
|
|
v.UI = ui.NewWithColor(out, false)
|
|
|
|
require.NoError(t, v.CreateSnapshot(&vaultik.SnapshotCreateOptions{
|
|
SkipErrors: tt.skipErrors,
|
|
Snapshots: []string{"src"},
|
|
}))
|
|
|
|
summary := out.String()
|
|
assert.Contains(t, summary,
|
|
"Files: 2 examined, 1 backed up, 0 unchanged, 1 failed.")
|
|
assert.Contains(t, summary,
|
|
fmt.Sprintf("Data: %s total (%s backed up).",
|
|
v.UI.Size(storedSize), v.UI.Size(storedSize)))
|
|
|
|
snap, err := repos.Snapshots.GetByID(ctx,
|
|
localSnapshotID(ctx, t, repos, "src"))
|
|
require.NoError(t, err)
|
|
require.NotNil(t, snap)
|
|
|
|
assert.Equal(t, int64(1), snap.FileCount)
|
|
assert.Equal(t, storedSize, snap.TotalSize)
|
|
})
|
|
}
|
|
}
|