package fileops import ( "fmt" "os" "path/filepath" "sort" "time" ) // PruneBackups retains the newest regular-file backups whose basenames match // pattern inside the literal directory. Parent path characters never become // glob syntax. Symlinks and non-regular entries are never deleted. func PruneBackups(directory, pattern string, retain int) error { if retain < 0 { return fmt.Errorf("backup retention cannot be negative") } if pattern == "" || filepath.Base(pattern) != pattern { return fmt.Errorf("backup pattern must match basenames only") } if _, err := filepath.Match(pattern, ""); err != nil { return err } entries, err := os.ReadDir(directory) if os.IsNotExist(err) { return nil } if err != nil { return err } type backupFile struct { path string modTime time.Time } backups := make([]backupFile, 0, len(entries)) for _, entry := range entries { matches, err := filepath.Match(pattern, entry.Name()) if err != nil { return err } if !matches { continue } path := filepath.Join(directory, entry.Name()) info, err := os.Lstat(path) if err != nil { return err } if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 { continue } backups = append(backups, backupFile{path: path, modTime: info.ModTime()}) } sort.Slice(backups, func(i, j int) bool { if backups[i].modTime.Equal(backups[j].modTime) { return backups[i].path < backups[j].path } return backups[i].modTime.Before(backups[j].modTime) }) for len(backups) > retain { if err := RemoveDurable(backups[0].path); err != nil { return err } backups = backups[1:] } return nil }