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
116 lines
3.7 KiB
Go
116 lines
3.7 KiB
Go
package snapshot
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/spf13/afero"
|
|
"go.uber.org/fx"
|
|
"sneak.berlin/go/vaultik/internal/config"
|
|
"sneak.berlin/go/vaultik/internal/database"
|
|
"sneak.berlin/go/vaultik/internal/pidlock"
|
|
"sneak.berlin/go/vaultik/internal/storage"
|
|
"sneak.berlin/go/vaultik/internal/ui"
|
|
)
|
|
|
|
// ScannerParams holds parameters for scanner creation
|
|
type ScannerParams struct {
|
|
EnableProgress bool
|
|
UI *ui.Writer // Where user-facing scanner messages go; nil = discard
|
|
Fs afero.Fs
|
|
Exclude []string // Exclude patterns (combined global + snapshot-specific)
|
|
SkipErrors bool // Skip file read errors (log loudly but continue)
|
|
// PreviousSnapshotID is the most recent completed snapshot of the
|
|
// same name, "" for none.
|
|
PreviousSnapshotID string
|
|
// DeviceNumber is ScannerConfig.DeviceNumber; nil reads it from the
|
|
// file's stat.
|
|
DeviceNumber func(info os.FileInfo) (uint64, bool)
|
|
}
|
|
|
|
// Module exports backup functionality as an fx module.
|
|
// It provides a ScannerFactory that can create Scanner instances
|
|
// with custom parameters while sharing common dependencies.
|
|
//
|
|
//nolint:gochecknoglobals // fx module definitions are conventionally globals
|
|
var Module = fx.Module("backup",
|
|
fx.Provide(
|
|
provideScannerFactory,
|
|
NewSnapshotManager,
|
|
),
|
|
)
|
|
|
|
// ScannerFactory creates scanners with custom parameters
|
|
type ScannerFactory func(params ScannerParams) *Scanner
|
|
|
|
func provideScannerFactory(
|
|
cfg *config.Config, repos *database.Repositories, storer storage.Storer,
|
|
) ScannerFactory {
|
|
return func(params ScannerParams) *Scanner {
|
|
// Use provided excludes, or fall back to global config excludes
|
|
excludes := params.Exclude
|
|
if len(excludes) == 0 {
|
|
excludes = cfg.Exclude
|
|
}
|
|
|
|
return NewScanner(ScannerConfig{
|
|
FS: params.Fs,
|
|
ChunkSize: cfg.ChunkSize.Int64(),
|
|
Repositories: repos,
|
|
Storage: storer,
|
|
MaxBlobSize: cfg.BlobSizeLimit.Int64(),
|
|
CompressionLevel: cfg.CompressionLevel,
|
|
AgeRecipients: cfg.AgeRecipients,
|
|
EnableProgress: params.EnableProgress,
|
|
UI: params.UI,
|
|
Exclude: excludes,
|
|
IndexAndLockFiles: indexAndLockFiles(cfg.IndexPath, pidlock.Dir()),
|
|
SkipErrors: params.SkipErrors,
|
|
PreviousSnapshotID: params.PreviousSnapshotID,
|
|
DeviceNumber: params.DeviceNumber,
|
|
})
|
|
}
|
|
}
|
|
|
|
// indexAndLockFiles 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.
|
|
func indexAndLockFiles(indexPath, lockDir string) []string {
|
|
paths := append(database.IndexFiles(indexPath), pidlock.Path(lockDir))
|
|
for i, p := range paths {
|
|
paths[i] = resolveParentDir(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 := resolveParentDir(indexPath)
|
|
|
|
target, err := filepath.EvalSymlinks(index)
|
|
if err == nil && target != index {
|
|
paths = append(paths, database.IndexFiles(target)...)
|
|
}
|
|
|
|
return paths
|
|
}
|
|
|
|
// resolveParentDir returns the absolute form of path with the symlinks in
|
|
// its parent directory resolved. If the parent directory cannot be
|
|
// resolved, it returns the absolute form unresolved. If path has no
|
|
// absolute form, it returns path.
|
|
func resolveParentDir(path string) string {
|
|
abs, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return path
|
|
}
|
|
|
|
dir, err := filepath.EvalSymlinks(filepath.Dir(abs))
|
|
if err != nil {
|
|
return abs
|
|
}
|
|
|
|
return filepath.Join(dir, filepath.Base(abs))
|
|
}
|