package backup import ( "os" "path/filepath" "sort" "strings" "time" ) // RetentionPolicy defines how many backups to keep. type RetentionPolicy struct { Count int // keep at most N most recent backups Days int // also delete backups older than N days (0 = disabled) } // ApplyRetention removes old backups from the project's backup directory. // Returns the number of files removed. func ApplyRetention(backupDir, projectName string, policy RetentionPolicy) (int, error) { dir := filepath.Join(backupDir, projectName) entries, err := os.ReadDir(dir) if err != nil { if os.IsNotExist(err) { return 0, nil } return 0, err } type fileInfo struct { name string modTime time.Time } var backups []fileInfo for _, entry := range entries { if entry.IsDir() || !isBackupArchive(projectName, entry.Name()) { continue } info, err := entry.Info() if err != nil { continue } backups = append(backups, fileInfo{ name: entry.Name(), modTime: info.ModTime(), }) } if len(backups) == 0 { return 0, nil } // Sort newest-first by modification time. sort.Slice(backups, func(i, j int) bool { return backups[i].modTime.After(backups[j].modTime) }) removed := 0 // Enforce count-based retention. if policy.Count > 0 && len(backups) > policy.Count { for _, b := range backups[policy.Count:] { path := filepath.Join(dir, b.name) if err := os.Remove(path); err == nil { removed++ } } // Trim the list for the next check. backups = backups[:policy.Count] } // Enforce age-based retention. if policy.Days > 0 { cutoff := time.Now().AddDate(0, 0, -policy.Days) for _, b := range backups { if b.modTime.Before(cutoff) { path := filepath.Join(dir, b.name) if err := os.Remove(path); err == nil { removed++ } } } } return removed, nil } func isBackupArchive(projectName, name string) bool { return strings.HasPrefix(name, projectName+"-") && strings.HasSuffix(name, ".tar.gz") }