1 Commits
Author SHA1 Message Date
sneak 4301a7e246 Skip vaultik's own index and PID file in a backup (closes #421)
check / check (push) Successful in 11m31s
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. pidlock now owns the lock directory and the PID file name,
so the CLI and the scanner use the same path.

Model: opus-5-5
2026-11-11 00:22:57 +00:00
3 changed files with 4 additions and 85 deletions

No files matched your search

@@ -1,68 +0,0 @@
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"
)
// TestEntrySnapshotCreateSkipsOwnFiles 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.
//
//nolint:paralleltest // replaces os.Args, os.Stdout, os.Stderr and the xdg globals
func TestEntrySnapshotCreateSkipsOwnFiles(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.")
})
}
}
+2 -14
View File
@@ -75,30 +75,18 @@ func provideScannerFactory(
// ownFiles returns the paths of the index at indexPath, its side files
// and the PID file in lockDir. Symlinks in their directories are resolved,
// as they are in the snapshot's source paths, so the scan finds them under
// the same names. If indexPath is a symlink, the file it points to and
// that file's side files are returned too.
// the same names.
func ownFiles(indexPath, lockDir string) []string {
paths := append(database.IndexFiles(indexPath), pidlock.Path(lockDir))
for i, p := range paths {
paths[i] = resolveDir(p)
}
// SQLite writes the file a symlinked index points to and keeps the
// side files beside that file. The database package creates the index
// before the scanner is built, so the link resolves.
index := resolveDir(indexPath)
target, err := filepath.EvalSymlinks(index)
if err == nil && target != index {
paths = append(paths, database.IndexFiles(target)...)
}
return paths
}
// resolveDir returns the absolute form of path with the symlinks in its
// directory resolved. If the directory cannot be resolved, it returns the
// absolute form unresolved. If path has no absolute form, it returns path.
// directory resolved. It returns path unchanged when that fails.
func resolveDir(path string) string {
abs, err := filepath.Abs(path)
if err != nil {
+2 -3
View File
@@ -2262,9 +2262,8 @@ func (s *Scanner) detectDeletedFiles(
// excludedOrInExcludedDir reports whether filePath, which is at or
// beneath rootPath, or any directory between rootPath and it, matches an
// exclude pattern or is one of vaultik's own files. A pattern such as
// */.local/share/Trash matches the directory but not the files inside it,
// which the walk skips too.
// exclude pattern. A pattern such as */.local/share/Trash matches the
// directory but not the files inside it, which the walk skips too.
func (s *Scanner) excludedOrInExcludedDir(filePath, rootPath string) bool {
for p := filePath; len(p) > len(rootPath); p = filepath.Dir(p) {
if s.shouldExclude(p, rootPath) {