// Package retention prunes old encrypted Postgres dump files from a backup // directory based on their modification time. // Package retention prunes old encrypted Postgres dump files from a backup // directory based on their modification time. package retention import ( "context" "io/fs" "log/slog" "os" "path/filepath" "time" ) // DumpGlob matches only encrypted Postgres dump artefacts produced by the // backupper: synapse-.dump.pqenc. The pattern is intentionally // strict — any deviation in prefix, middle, or extension is preserved. const DumpGlob = "synapse-*.dump.pqenc" // PruneByAge removes every file in dir matching DumpGlob whose modification // time is older than retentionDays relative to now. It returns the basenames // of the files it removed. // // retentionDays == 0 disables pruning entirely (opt-out): the function // returns a nil slice and a nil error without touching the directory. // // Subdirectories, non-matching files, and the root dir itself are never // removed or followed. func PruneByAge( ctx context.Context, dir string, retentionDays int, now time.Time, ) ( []string, error, ) { if retentionDays == 0 { return nil, nil } if err := ctx.Err(); err != nil { return nil, err } if _, err := os.Stat(dir); err != nil { return nil, err } cutoff := now.Add(-time.Duration(retentionDays) * 24 * time.Hour) logger := slog.Default() var pruned []string walkErr := fs.WalkDir(os.DirFS(dir), ".", func(walkPath string, entry fs.DirEntry, walkErrIn error) error { if walkErrIn != nil { return walkErrIn } if err := ctx.Err(); err != nil { return err } if entry.IsDir() { return nil } name := filepath.Base(walkPath) matched, err := filepath.Match(DumpGlob, name) if err != nil { return err } if !matched { return nil } full := filepath.Join(dir, walkPath) info, err := os.Stat(full) if err != nil { return err } if !info.ModTime().Before(cutoff) { return nil } age := now.Sub(info.ModTime()) if err := os.Remove(full); err != nil { return err } logger.Info( "prune retention: removed old dump", slog.String("file", name), slog.Float64("age_seconds", age.Seconds()), ) pruned = append(pruned, name) return nil }) if walkErr != nil { return pruned, walkErr } return pruned, nil }