Skip vaultik's own index and PID file in a backup (closes #421)
check / check (push) Successful in 10m54s
check / check (push) Successful in 10m54s
The default config backs up the home directory, which holds vaultik's local index and PID file, so a backup stored a torn copy of the index it was writing. The scan now skips the index, its SQLite side files and the PID file whatever the config says, including when index_path or its directory is a symlink. database purge removes the same list of index files. Model: opus-5-5
This commit was merged in pull request #442.
This commit is contained in:
1 parent
d6eed26482
commit
7aae6e4ff3
10 files changed
+295
-17
No files matched your search
+1
-4
@@ -7,11 +7,9 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/dig"
|
||||
"go.uber.org/fx"
|
||||
@@ -351,8 +349,7 @@ func runVaultikApp(
|
||||
// only one runs at a time; a read-only command runs without it and is not
|
||||
// blocked while a mutator holds the lock (opts.Mode).
|
||||
func RunWithApp(ctx context.Context, opts AppOptions) error {
|
||||
release, err := acquireLockIfMutating(opts.Mode,
|
||||
filepath.Join(xdg.DataHome, "vaultik"))
|
||||
release, err := acquireLockIfMutating(opts.Mode, pidlock.Dir())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"sneak.berlin/go/vaultik/internal/config"
|
||||
"sneak.berlin/go/vaultik/internal/database"
|
||||
"sneak.berlin/go/vaultik/internal/log"
|
||||
"sneak.berlin/go/vaultik/internal/vaultik"
|
||||
)
|
||||
@@ -100,18 +101,16 @@ Use --force to skip the confirmation prompt.`,
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the database file
|
||||
err = os.Remove(dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete database: %w", err)
|
||||
// Delete the database file and its side files. A side file
|
||||
// may not exist, so only failing to delete the database file
|
||||
// is an error.
|
||||
for _, indexFile := range database.IndexFiles(dbPath) {
|
||||
err = os.Remove(indexFile)
|
||||
if err != nil && indexFile == dbPath {
|
||||
return fmt.Errorf("failed to delete database: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Also delete WAL and SHM files if they exist
|
||||
walPath := dbPath + "-wal"
|
||||
shmPath := dbPath + "-shm"
|
||||
_ = os.Remove(walPath) // Ignore errors - files may not exist
|
||||
_ = os.Remove(shmPath)
|
||||
|
||||
out.Infof("Local state database deleted: %s.", dbPath)
|
||||
log.Info("Local state database deleted", "path", dbPath)
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/vaultik/internal/database"
|
||||
"sneak.berlin/go/vaultik/internal/vaultik"
|
||||
)
|
||||
|
||||
@@ -46,3 +48,28 @@ func TestDatabaseDeleteDeclinedIsError(t *testing.T) {
|
||||
out.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestDatabaseDeleteRemovesSideFiles checks that the side files SQLite
|
||||
// keeps beside the database file are deleted with it.
|
||||
//
|
||||
//nolint:paralleltest // mutates the process-global rootFlags
|
||||
func TestDatabaseDeleteRemovesSideFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dbPath, cfgPath := seedDatabaseDeleteConfig(t, dir)
|
||||
|
||||
indexFiles := database.IndexFiles(dbPath)
|
||||
for _, indexFile := range indexFiles {
|
||||
seedFile(t, dir, filepath.Base(indexFile), "data")
|
||||
}
|
||||
|
||||
setRootFlags(t, RootFlags{ConfigPath: cfgPath})
|
||||
|
||||
mustExecute(t, newDatabaseDeleteCommand(), "--force")
|
||||
|
||||
for _, indexFile := range indexFiles {
|
||||
_, err := os.Stat(indexFile)
|
||||
if !os.IsNotExist(err) {
|
||||
t.Errorf("database delete left %s: stat err = %v", indexFile, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package cli //nolint:testpackage // shares runEntry and the hermetic config helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestEntrySnapshotCreateSkipsIndexAndLockFiles backs up a directory
|
||||
// that holds vaultik's index and PID file beside one other file, with no
|
||||
// exclude pattern set. The summary counts only the other file. See
|
||||
// https://git.eeqj.de/sneak/vaultik/issues/421.
|
||||
//
|
||||
// Not parallel: it replaces os.Args, os.Stdout, os.Stderr and the xdg
|
||||
// globals.
|
||||
func TestEntrySnapshotCreateSkipsIndexAndLockFiles(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
// linked sets index_path to a symlink outside the backed-up
|
||||
// directory that points to the index inside it.
|
||||
linked bool
|
||||
}{
|
||||
{name: "index in the backed-up directory"},
|
||||
{name: "index_path links into the backed-up directory", linked: true},
|
||||
} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
configPath := filepath.Join(dir, "config.yml")
|
||||
sourceDir := filepath.Join(dir, "source")
|
||||
storeDir := filepath.Join(dir, "store")
|
||||
|
||||
indexPath := filepath.Join(sourceDir, "index.sqlite")
|
||||
|
||||
if testCase.linked {
|
||||
link := filepath.Join(dir, "index.sqlite")
|
||||
require.NoError(t, os.Symlink(indexPath, link))
|
||||
|
||||
indexPath = link
|
||||
}
|
||||
|
||||
contents := fmt.Sprintf(hermeticConfig,
|
||||
sourceDir, storeDir, indexPath)
|
||||
require.NoError(t,
|
||||
os.WriteFile(configPath, []byte(contents), configFileMode))
|
||||
require.NoError(t, os.Mkdir(storeDir, 0o750))
|
||||
require.NoError(t, os.Mkdir(sourceDir, 0o750))
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(sourceDir, "file.txt"), []byte("hello"), 0o600))
|
||||
|
||||
// The PID file goes in a directory under XDG_DATA_HOME, which
|
||||
// xdg reads at package init.
|
||||
t.Setenv("XDG_DATA_HOME", filepath.Join(sourceDir, "data"))
|
||||
xdg.Reload()
|
||||
t.Cleanup(xdg.Reload)
|
||||
|
||||
code, stdout, _ := runEntry(t,
|
||||
flagConfig, configPath, cmdSnapshot, cmdCreate)
|
||||
|
||||
require.Equal(t, 0, code)
|
||||
assert.Contains(t, stdout,
|
||||
"Files: 1 examined, 1 backed up, 0 unchanged.")
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user