Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc69561b3e |
@@ -382,9 +382,7 @@ on the destination in one go, use `vaultik remote nuke --force`.
|
|||||||
store — its remote key (or an unambiguous leading part of it) as shown
|
store — its remote key (or an unambiguous leading part of it) as shown
|
||||||
by `snapshot list`. See
|
by `snapshot list`. See
|
||||||
[restoring on another machine](#restoring-on-another-machine).
|
[restoring on another machine](#restoring-on-another-machine).
|
||||||
* Optional path arguments to restore specific files/directories (default: all).
|
* Optional path arguments to restore specific files/directories (default: all)
|
||||||
A path that matches nothing in the snapshot is named in a warning, and the
|
|
||||||
command exits non-zero after restoring the paths that match.
|
|
||||||
* Preserves file permissions, timestamps, ownership (ownership requires root),
|
* Preserves file permissions, timestamps, ownership (ownership requires root),
|
||||||
symlinks, and empty directories
|
symlinks, and empty directories
|
||||||
* `--verify`: After restoring, verify every restored file's chunk hashes
|
* `--verify`: After restoring, verify every restored file's chunk hashes
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
package cli //nolint:testpackage // shares runEntry and the hermetic config helpers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"sneak.berlin/go/vaultik/internal/database"
|
|
||||||
"sneak.berlin/go/vaultik/internal/snapshot"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Words and flags of `snapshot purge` used to build argument vectors
|
|
||||||
// below.
|
|
||||||
const (
|
|
||||||
cmdPurge = "purge"
|
|
||||||
flagOlderThan = "--older-than"
|
|
||||||
flagForce = "--force"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestEntrySnapshotPurgeKeepsMetadataWhenLocalIndexDeleteFails makes
|
|
||||||
// the local index refuse to delete an old snapshot and checks that
|
|
||||||
// `snapshot purge` exits non-zero and leaves that snapshot's metadata on
|
|
||||||
// the destination store. It used to log the error, delete the metadata
|
|
||||||
// and exit 0.
|
|
||||||
//
|
|
||||||
// `snapshot create --prune` runs the same purge, but cannot be driven
|
|
||||||
// this way: the refusal also breaks the backup's metadata export, which
|
|
||||||
// deletes the other snapshots from its copy of the local index.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // replaces os.Args, os.Stdout, os.Stderr and the xdg globals
|
|
||||||
func TestEntrySnapshotPurgeKeepsMetadataWhenLocalIndexDeleteFails(t *testing.T) {
|
|
||||||
configPath := writeHermeticPruneConfig(t, false)
|
|
||||||
dir := filepath.Dir(configPath)
|
|
||||||
indexPath := filepath.Join(dir, "index.sqlite")
|
|
||||||
|
|
||||||
// seedStaleSnapshotRecord writes an old snapshot to the local index
|
|
||||||
// only; the manifest planted here puts it on the destination store
|
|
||||||
// too, so the purge selects it rather than dropping it as stale.
|
|
||||||
seedStaleSnapshotRecord(t, indexPath)
|
|
||||||
|
|
||||||
manifestDir := filepath.Join(dir, "store", "metadata",
|
|
||||||
snapshot.RemoteSnapshotKey(stalePruneSnapshotID))
|
|
||||||
require.NoError(t, os.MkdirAll(manifestDir, 0o750))
|
|
||||||
|
|
||||||
manifestPath := filepath.Join(manifestDir, "manifest.json.zst")
|
|
||||||
require.NoError(t, os.WriteFile(manifestPath, []byte("stub"), 0o600))
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
db, err := database.New(ctx, indexPath)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
_, err = db.Conn().ExecContext(ctx, `
|
|
||||||
CREATE TRIGGER refuse_snapshot_delete BEFORE DELETE ON snapshots
|
|
||||||
BEGIN SELECT RAISE(ABORT, 'simulated index error'); END`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, db.Close())
|
|
||||||
|
|
||||||
code, _, _ := runEntry(t, flagConfig, configPath,
|
|
||||||
cmdSnapshot, cmdPurge, flagOlderThan, "30d", flagForce)
|
|
||||||
|
|
||||||
assert.Equal(t, 1, code)
|
|
||||||
assert.FileExists(t, manifestPath,
|
|
||||||
"the snapshot is still in the local index, so its metadata must stay")
|
|
||||||
}
|
|
||||||
@@ -30,8 +30,6 @@ This command will restore files from the specified snapshot to the
|
|||||||
target directory.
|
target directory.
|
||||||
If no paths are specified, all files are restored.
|
If no paths are specified, all files are restored.
|
||||||
If paths are specified, only matching files/directories are restored.
|
If paths are specified, only matching files/directories are restored.
|
||||||
A path that matches nothing in the snapshot is named in a warning, and
|
|
||||||
the command exits non-zero after restoring the paths that match.
|
|
||||||
|
|
||||||
The snapshot may be named by its ID or, when restoring on a host with no
|
The snapshot may be named by its ID or, when restoring on a host with no
|
||||||
local index, by the remote key that 'snapshot list' prints for a
|
local index, by the remote key that 'snapshot list' prints for a
|
||||||
|
|||||||
+24
-55
@@ -27,7 +27,6 @@ import (
|
|||||||
// Sentinel errors for restore failures.
|
// Sentinel errors for restore failures.
|
||||||
var (
|
var (
|
||||||
errFilesFailedRestore = errors.New("file(s) failed to restore")
|
errFilesFailedRestore = errors.New("file(s) failed to restore")
|
||||||
errPathsNotInSnapshot = errors.New("requested path(s) not in the snapshot")
|
|
||||||
errFilesFailedVerify = errors.New("files failed verification")
|
errFilesFailedVerify = errors.New("files failed verification")
|
||||||
errDecryptionKeyRequired = errors.New(
|
errDecryptionKeyRequired = errors.New(
|
||||||
"decryption key required for restore\n\n" +
|
"decryption key required for restore\n\n" +
|
||||||
@@ -151,20 +150,16 @@ func (v *Vaultik) Restore(opts *RestoreOptions) error {
|
|||||||
repos := database.NewRepositories(tempDB)
|
repos := database.NewRepositories(tempDB)
|
||||||
|
|
||||||
// Step 2: Get list of files to restore
|
// Step 2: Get list of files to restore
|
||||||
files, unmatched, err := v.getFilesToRestore(v.ctx, repos, opts.Paths)
|
files, err := v.getFilesToRestore(v.ctx, repos, opts.Paths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getting files to restore: %w", err)
|
return fmt.Errorf("getting files to restore: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Named before anything is restored, so the names are shown even
|
|
||||||
// when the restore stops on an error.
|
|
||||||
unmatchedErr := v.reportUnmatchedPaths(unmatched)
|
|
||||||
|
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
log.Warn("No files found to restore")
|
log.Warn("No files found to restore")
|
||||||
v.UI.Warningf("No files found to restore.")
|
v.UI.Warningf("No files found to restore.")
|
||||||
|
|
||||||
return unmatchedErr
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("Found files to restore", "count", len(files))
|
log.Info("Found files to restore", "count", len(files))
|
||||||
@@ -190,20 +185,6 @@ func (v *Vaultik) Restore(opts *RestoreOptions) error {
|
|||||||
|
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
|
|
||||||
return v.finishRestore(repos, files, unmatchedErr, opts, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// finishRestore prints the restore summary and the post-restore
|
|
||||||
// warnings, runs optional verification, and converts any failed-file
|
|
||||||
// count into an error. Otherwise it returns unmatchedErr, the error for
|
|
||||||
// the requested paths that matched nothing (nil when there are none).
|
|
||||||
func (v *Vaultik) finishRestore(
|
|
||||||
repos *database.Repositories,
|
|
||||||
files []*database.File,
|
|
||||||
unmatchedErr error,
|
|
||||||
opts *RestoreOptions,
|
|
||||||
result *RestoreResult,
|
|
||||||
) error {
|
|
||||||
log.Info("Restore complete",
|
log.Info("Restore complete",
|
||||||
"files_restored", result.FilesRestored,
|
"files_restored", result.FilesRestored,
|
||||||
"bytes_restored", ubytes(result.BytesRestored),
|
"bytes_restored", ubytes(result.BytesRestored),
|
||||||
@@ -218,6 +199,17 @@ func (v *Vaultik) finishRestore(
|
|||||||
v.UI.Duration(result.Duration),
|
v.UI.Duration(result.Duration),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return v.finishRestore(repos, files, opts, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// finishRestore emits the post-restore warnings, runs optional
|
||||||
|
// verification, and converts any failed-file count into an error.
|
||||||
|
func (v *Vaultik) finishRestore(
|
||||||
|
repos *database.Repositories,
|
||||||
|
files []*database.File,
|
||||||
|
opts *RestoreOptions,
|
||||||
|
result *RestoreResult,
|
||||||
|
) error {
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
v.UI.Warningf("Restore did not preserve file ownership: chown(2) " +
|
v.UI.Warningf("Restore did not preserve file ownership: chown(2) " +
|
||||||
"requires root. Re-run as root (e.g. with sudo) if you need " +
|
"requires root. Re-run as root (e.g. with sudo) if you need " +
|
||||||
@@ -244,23 +236,7 @@ func (v *Vaultik) finishRestore(
|
|||||||
return fmt.Errorf("%d %w", result.FilesFailed, errFilesFailedRestore)
|
return fmt.Errorf("%d %w", result.FilesFailed, errFilesFailedRestore)
|
||||||
}
|
}
|
||||||
|
|
||||||
return unmatchedErr
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
// reportUnmatchedPaths warns, by name, about each requested path that
|
|
||||||
// matched nothing in the snapshot, and returns an error counting them,
|
|
||||||
// or nil when there are none.
|
|
||||||
func (v *Vaultik) reportUnmatchedPaths(unmatched []string) error {
|
|
||||||
if len(unmatched) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, path := range unmatched {
|
|
||||||
log.Warn("Requested path matches nothing in the snapshot", "path", path)
|
|
||||||
v.UI.Warningf("%s matches nothing in the snapshot.", v.UI.Path(path))
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("%d %w", len(unmatched), errPathsNotInSnapshot)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// restoreIdentities parses the configured age secret key once into every
|
// restoreIdentities parses the configured age secret key once into every
|
||||||
@@ -864,34 +840,27 @@ func (v *Vaultik) materializeSnapshotDB(
|
|||||||
return db, tempDir, nil
|
return db, tempDir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFilesToRestore returns the list of files to restore based on path
|
// getFilesToRestore returns the list of files to restore based on path filters
|
||||||
// filters, and the filters, as given, that matched no file.
|
|
||||||
func (v *Vaultik) getFilesToRestore(
|
func (v *Vaultik) getFilesToRestore(
|
||||||
ctx context.Context, repos *database.Repositories, pathFilters []string,
|
ctx context.Context, repos *database.Repositories, pathFilters []string,
|
||||||
) ([]*database.File, []string, error) {
|
) ([]*database.File, error) {
|
||||||
// If no filters, get all files
|
// If no filters, get all files
|
||||||
if len(pathFilters) == 0 {
|
if len(pathFilters) == 0 {
|
||||||
files, err := repos.Files.ListAll(ctx)
|
return repos.Files.ListAll(ctx)
|
||||||
|
|
||||||
return files, nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get files matching the path filters
|
// Get files matching the path filters
|
||||||
var (
|
var result []*database.File
|
||||||
result []*database.File
|
|
||||||
unmatched []string
|
|
||||||
)
|
|
||||||
|
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
|
|
||||||
for _, filter := range pathFilters {
|
for _, filter := range pathFilters {
|
||||||
files, err := repos.Files.ListUnderPath(ctx, filepath.Clean(filter))
|
// Normalize the filter path
|
||||||
if err != nil {
|
filter = filepath.Clean(filter)
|
||||||
return nil, nil, fmt.Errorf("listing files under %s: %w", filter, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(files) == 0 {
|
files, err := repos.Files.ListUnderPath(ctx, filter)
|
||||||
unmatched = append(unmatched, filter)
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("listing files under %s: %w", filter, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
@@ -902,7 +871,7 @@ func (v *Vaultik) getFilesToRestore(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, unmatched, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildChunkToBlobMap creates a mapping from chunk hash to blob information
|
// buildChunkToBlobMap creates a mapping from chunk hash to blob information
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
@@ -669,11 +669,10 @@ func (v *Vaultik) confirmAndExecutePurge(
|
|||||||
snapshotID := snap.ID.String()
|
snapshotID := snap.ID.String()
|
||||||
log.Info("Deleting snapshot", "id", snapshotID)
|
log.Info("Deleting snapshot", "id", snapshotID)
|
||||||
|
|
||||||
// Stop before the remote delete, so a snapshot still in the local
|
|
||||||
// index keeps its metadata on the destination store.
|
|
||||||
err := v.deleteSnapshotFromLocalDB(snapshotID)
|
err := v.deleteSnapshotFromLocalDB(snapshotID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("deleting snapshot %s from local index: %w", snapshotID, err)
|
log.Error("Failed to delete from local database",
|
||||||
|
"snapshot_id", snapshotID, "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = v.deleteRemoteSnapshotByKey(snapshot.RemoteSnapshotKey(snapshotID))
|
_, err = v.deleteRemoteSnapshotByKey(snapshot.RemoteSnapshotKey(snapshotID))
|
||||||
|
|||||||
Reference in New Issue
Block a user