2026-03-19 16:37:57 +08:00

200 lines
4.1 KiB
Go

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
}