package rotatemanage import ( "compress/gzip" "io" "os" "path/filepath" "sort" "strings" "time" ) type Options struct { MaxBackups int MaxAge time.Duration Compress bool Pattern string } type backupFileMeta struct { path string modTime time.Time } func Apply(archivePath string, currentPath string, options Options) error { if archivePath == "" || currentPath == "" { return nil } if options.Compress { if _, err := gzipBackupFile(archivePath); err != nil { return err } } backups, err := listManagedBackups(currentPath, options.Pattern) if err != nil { return err } return cleanupManagedBackups(backups, options) } func listManagedBackups(currentPath string, pattern string) ([]backupFileMeta, error) { dir := filepath.Dir(currentPath) base := filepath.Base(currentPath) stem := strings.TrimSuffix(base, filepath.Ext(base)) entries, err := os.ReadDir(dir) if err != nil { return nil, err } backups := make([]backupFileMeta, 0, len(entries)) for _, entry := range entries { if entry.IsDir() { continue } name := entry.Name() if name == base { continue } matched, err := IsManagedBackupName(name, base, stem, pattern) if err != nil { return nil, err } if !matched { continue } info, err := entry.Info() if err != nil { return nil, err } backups = append(backups, backupFileMeta{ path: filepath.Join(dir, name), modTime: info.ModTime(), }) } return backups, nil } func IsManagedBackupName(name string, base string, stem string, pattern string) (bool, error) { if pattern != "" { return filepath.Match(pattern, name) } prefixes := []string{ base + ".", base + "_", base + "-", } if stem != "" && stem != base { prefixes = append(prefixes, stem+".", stem+"_", stem+"-", ) } for _, prefix := range prefixes { if !strings.HasPrefix(name, prefix) { continue } if isLikelyManagedBackupSuffix(strings.TrimPrefix(name, prefix)) { return true, nil } } return false, nil } func isLikelyManagedBackupSuffix(suffix string) bool { suffix = strings.TrimSpace(strings.ToLower(suffix)) if suffix == "" { return false } suffix = strings.TrimSuffix(suffix, ".gz") suffix = strings.TrimSuffix(suffix, ".zip") if suffix == "" { return false } if strings.Contains(suffix, "bak") { return true } for _, ch := range suffix { if ch >= '0' && ch <= '9' { return true } } return false } func cleanupManagedBackups(backups []backupFileMeta, options Options) error { var firstErr error now := time.Now() kept := make([]backupFileMeta, 0, len(backups)) for _, item := range backups { if options.MaxAge > 0 && now.Sub(item.modTime) > options.MaxAge { if err := os.Remove(item.path); err != nil && firstErr == nil { firstErr = err } continue } kept = append(kept, item) } if options.MaxBackups > 0 && len(kept) > options.MaxBackups { sort.Slice(kept, func(i, j int) bool { return kept[i].modTime.After(kept[j].modTime) }) for _, item := range kept[options.MaxBackups:] { if err := os.Remove(item.path); err != nil && firstErr == nil { firstErr = err } } } return firstErr } func gzipBackupFile(path string) (string, error) { if strings.HasSuffix(path, ".gz") { return path, nil } source, err := os.Open(path) if err != nil { return "", err } destination := path + ".gz" temp := destination + ".tmp" target, err := os.Create(temp) if err != nil { _ = source.Close() return "", err } gzWriter := gzip.NewWriter(target) if _, err = io.Copy(gzWriter, source); err != nil { _ = source.Close() _ = gzWriter.Close() _ = target.Close() _ = os.Remove(temp) return "", err } if err = gzWriter.Close(); err != nil { _ = source.Close() _ = target.Close() _ = os.Remove(temp) return "", err } if err = target.Close(); err != nil { _ = source.Close() _ = os.Remove(temp) return "", err } if err = source.Close(); err != nil { _ = os.Remove(temp) return "", err } if err = os.Rename(temp, destination); err != nil { _ = os.Remove(temp) return "", err } if err = os.Remove(path); err != nil { return "", err } return destination, nil }