200 lines
4.1 KiB
Go
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
|
||
|
|
}
|