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