- go.mod: 将非法版本号 go 1.26.4 改为 go 1.26 - engine: 压缩失败时删除残留的半截 tar.gz 文件 - config: 支持仅按天数保留(count 和 days 均未设时才用默认 count=7) - engine: docker compose stop 失败时也尝试重启,避免容器停摆 - list: 仅显示真正的备份归档文件(新增导出 IsBackupArchive) - systemd: 移除 PrivateTmp,暂存目录改到安装目录内,避免私有 /tmp 被大目录撑爆
96 lines
2.1 KiB
Go
96 lines
2.1 KiB
Go
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 IsBackupArchive(projectName, name)
|
|
}
|
|
|
|
// IsBackupArchive reports whether name is a backup archive for the project.
|
|
func IsBackupArchive(projectName, name string) bool {
|
|
return strings.HasPrefix(name, projectName+"-") && strings.HasSuffix(name, ".tar.gz")
|
|
} |