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, OwnFiles: ownFiles(cfg.IndexPath, pidlock.Dir()), SkipErrors: params.SkipErrors, PreviousSnapshotID: params.PreviousSnapshotID, DeviceNumber: params.DeviceNumber, }) } } // 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. 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. func resolveDir(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)) }