Skip vaultik's own index and PID file in a backup (closes #421)
check / check (push) Failing after 20m56s
check / check (push) Failing after 20m56s
A backup of a directory that holds the local index stored a torn copy of it, its -wal and -shm side files and vaultik.pid, all being written while they were read. The scan now treats these files as excluded, whatever the config says. Their paths come from index_path and the lock directory, with symlinks in their directories resolved the way source paths are. If index_path is itself a symlink, SQLite writes the file it points to, so that file and its side files are skipped too. pidlock now owns the lock directory and the PID file name, so the CLI and the scanner use the same path. database delete now removes the side files database.IndexFiles lists, the -journal file included. Model: opus-5-5
This commit is contained in:
1 parent
0d0c76fa24
commit
17ab7b16ee
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