check / check (push) Waiting to run
Every backup 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 act as for a destination that cannot be listed. remote nuke deletes it 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 a mount point. Fixtures standing for a written destination carry the marker; two tests check they get past it. Judgement call: the marker is rewritten on every backup. Deviation: for the complexity limit, snapshot name selection moved into snapshotNamesToCreate and the --force/--json test into confirmRemoveSnapshot, both unchanged. Model: opus-5-5
203 lines
5.8 KiB
Go
203 lines
5.8 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"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/storage/faultstore"
|
|
"sneak.berlin/go/vaultik/internal/ui"
|
|
"sneak.berlin/go/vaultik/internal/vaultik"
|
|
)
|
|
|
|
// These tests cover https://git.eeqj.de/sneak/vaultik/issues/309: a path
|
|
// given to restore that matches nothing in the snapshot is named in a
|
|
// warning and makes the restore fail, after the paths that do match are
|
|
// restored.
|
|
|
|
func TestRestoreNamesUnmatchedPathAndRestoresTheMatchingOne(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
fs := afero.NewOsFs()
|
|
tempDir := t.TempDir()
|
|
dataDir := filepath.Join(tempDir, "src")
|
|
storeDir := t.TempDir()
|
|
restoreDir := filepath.Join(tempDir, "restored")
|
|
dbPath := filepath.Join(tempDir, "index.sqlite")
|
|
|
|
ctx := context.Background()
|
|
cfg := faultTestConfig()
|
|
files := writeFaultSourceTree(t, fs, dataDir)
|
|
|
|
store, err := storage.NewFileStorer(storeDir)
|
|
require.NoError(t, err)
|
|
|
|
db, err := database.New(ctx, dbPath)
|
|
require.NoError(t, err)
|
|
|
|
id := fullFaultBackup(ctx, t, fs, store, cfg,
|
|
database.NewRepositories(db), dataDir, dbPath, "unmatched")
|
|
require.NoError(t, db.Close())
|
|
|
|
matched := filepath.Join(dataDir, "small.txt")
|
|
unmatched := filepath.Join(dataDir, "typo.txt")
|
|
|
|
var out bytes.Buffer
|
|
|
|
v := newReaderVaultik(ctx, cfg, store, nil, fs)
|
|
v.UI = ui.NewWithColor(&out, false)
|
|
|
|
err = v.Restore(&vaultik.RestoreOptions{
|
|
SnapshotID: id,
|
|
TargetDir: restoreDir,
|
|
Paths: []string{matched, unmatched},
|
|
})
|
|
|
|
require.Error(t, err, "a path that matches nothing must fail the restore")
|
|
assert.Contains(t, out.String(), unmatched,
|
|
"the path that matches nothing must be named")
|
|
assert.NotContains(t, out.String(), matched,
|
|
"the path that matches must not be reported")
|
|
|
|
got, err := afero.ReadFile(fs, filepath.Join(restoreDir, matched))
|
|
require.NoError(t, err, "the path that matches must still be restored")
|
|
assert.Equal(t, files[matched], got)
|
|
}
|
|
|
|
// When no requested path matches, nothing is restored and each path is
|
|
// named. One path is relative. The other goes through the symlink that
|
|
// is the snapshot's configured path, while the entries are stored under
|
|
// the directory the symlink points to.
|
|
func TestRestoreNamesEveryPathWhenNothingMatches(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
const snapshotName = "linked"
|
|
|
|
fs := afero.NewOsFs()
|
|
tempDir := t.TempDir()
|
|
dataDir := filepath.Join(tempDir, "src")
|
|
linkDir := filepath.Join(tempDir, "srclink")
|
|
storeDir := t.TempDir()
|
|
restoreDir := filepath.Join(tempDir, "restored")
|
|
dbPath := filepath.Join(tempDir, "index.sqlite")
|
|
|
|
ctx := context.Background()
|
|
|
|
writeFaultSourceTree(t, fs, dataDir)
|
|
require.NoError(t, os.Symlink(dataDir, linkDir))
|
|
|
|
cfg := faultTestConfig()
|
|
cfg.IndexPath = dbPath
|
|
cfg.ChunkSize = config.Size(faultChunkSize)
|
|
cfg.Snapshots = map[string]config.SnapshotConfig{
|
|
snapshotName: {Paths: []string{linkDir}},
|
|
}
|
|
|
|
store, err := storage.NewFileStorer(storeDir)
|
|
require.NoError(t, err)
|
|
|
|
db, err := database.New(ctx, dbPath)
|
|
require.NoError(t, err)
|
|
|
|
repos := database.NewRepositories(db)
|
|
v := newBackupVaultik(ctx, cfg, store, repos, db, fs)
|
|
|
|
require.NoError(t, backUp(v, snapshotName))
|
|
|
|
id := localSnapshotID(ctx, t, repos, snapshotName)
|
|
require.NoError(t, db.Close())
|
|
|
|
throughLink := filepath.Join(linkDir, "sub")
|
|
relative := strings.TrimPrefix(filepath.Join(dataDir, "small.txt"), "/")
|
|
|
|
var out bytes.Buffer
|
|
|
|
reader := newReaderVaultik(ctx, cfg, store, nil, fs)
|
|
reader.UI = ui.NewWithColor(&out, false)
|
|
|
|
err = reader.Restore(&vaultik.RestoreOptions{
|
|
SnapshotID: id,
|
|
TargetDir: restoreDir,
|
|
Paths: []string{throughLink, relative},
|
|
})
|
|
|
|
require.Error(t, err, "a restore that matches nothing must fail")
|
|
assert.Contains(t, out.String(), throughLink,
|
|
"the path through the symlink must be named")
|
|
assert.Contains(t, out.String(), relative,
|
|
"the relative path must be named")
|
|
|
|
_, err = fs.Stat(restoreDir)
|
|
assert.True(t, os.IsNotExist(err), "nothing must be restored")
|
|
}
|
|
|
|
// Without --skip-errors a damaged blob stops the restore part way. The
|
|
// path that matches nothing must still be named.
|
|
func TestRestoreNamesUnmatchedPathWhenADamagedBlobStopsIt(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
fs := afero.NewOsFs()
|
|
tempDir := t.TempDir()
|
|
dataDir := filepath.Join(tempDir, "src")
|
|
storeDir := t.TempDir()
|
|
restoreDir := filepath.Join(tempDir, "restored")
|
|
dbPath := filepath.Join(tempDir, "index.sqlite")
|
|
|
|
ctx := context.Background()
|
|
cfg := faultTestConfig()
|
|
|
|
writeFaultSourceTree(t, fs, dataDir)
|
|
|
|
inner, err := storage.NewFileStorer(storeDir)
|
|
require.NoError(t, err)
|
|
|
|
db, err := database.New(ctx, dbPath)
|
|
require.NoError(t, err)
|
|
|
|
id := fullFaultBackup(ctx, t, fs, inner, cfg,
|
|
database.NewRepositories(db), dataDir, dbPath, "damaged")
|
|
require.NoError(t, db.Close())
|
|
|
|
damaged := faultstore.New(inner)
|
|
damaged.OnGet = func(key string) faultstore.GetFault {
|
|
if strings.HasPrefix(key, "blobs/") {
|
|
return faultstore.GetCorrupt
|
|
}
|
|
|
|
return faultstore.GetNormal
|
|
}
|
|
|
|
matched := filepath.Join(dataDir, "small.txt")
|
|
unmatched := filepath.Join(dataDir, "typo.txt")
|
|
|
|
var out bytes.Buffer
|
|
|
|
v := newReaderVaultik(ctx, cfg, damaged, nil, fs)
|
|
v.UI = ui.NewWithColor(&out, false)
|
|
|
|
err = v.Restore(&vaultik.RestoreOptions{
|
|
SnapshotID: id,
|
|
TargetDir: restoreDir,
|
|
Paths: []string{matched, unmatched},
|
|
})
|
|
|
|
require.Error(t, err, "a damaged blob must stop the restore")
|
|
assert.Contains(t, err.Error(), "blob",
|
|
"the restore must stop on the damaged blob")
|
|
assert.Contains(t, out.String(), unmatched,
|
|
"the path that matches nothing must be named")
|
|
}
|