check / check (push) Waiting to run
snapshot restore said nothing about a requested path that matched no entry in the snapshot (a typo, a relative path, or a path through a symlinked snapshot path, whose entries are stored under the resolved directory) and exited 0, even when nothing matched. Each such path is now named in a warning as soon as the file list is built, so it shows even when the restore then stops on an error, and restore returns an error after restoring the paths that do match. The restore summary moved into finishRestore to keep Restore within the length limit. Making a path through a symlink match is not part of this change. Judgement call: when the restore itself fails, that error is returned. Model: opus-5-5
203 lines
5.9 KiB
Go
203 lines
5.9 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 := filepath.Join(tempDir, "remote")
|
|
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 := filepath.Join(tempDir, "remote")
|
|
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 := filepath.Join(tempDir, "remote")
|
|
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")
|
|
}
|