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") }