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