mirror of
https://github.com/kopia/kopia.git
synced 2026-05-18 19:54:37 -04:00
refactored and cleaned up policy management, added retention tags to snapshot list
This commit is contained in:
@@ -95,6 +95,7 @@ func setPolicyFromFlags(target snapshot.SourceInfo, p *snapshot.Policy) error {
|
||||
// It's not really a list, just optional boolean.
|
||||
for _, inherit := range *policySetInherit {
|
||||
p.NoParent = !inherit
|
||||
break
|
||||
}
|
||||
|
||||
if *policySetClearExclude {
|
||||
@@ -108,8 +109,10 @@ func setPolicyFromFlags(target snapshot.SourceInfo, p *snapshot.Policy) error {
|
||||
p.FilesPolicy.Include = addRemoveDedupeAndSort(p.FilesPolicy.Include, *policySetAddInclude, *policySetRemoveInclude)
|
||||
}
|
||||
|
||||
// It's not really a list, just optional value.
|
||||
for _, freq := range *policySetFrequency {
|
||||
p.SchedulingPolicy.Frequency = freq
|
||||
p.SchedulingPolicy.MaxFrequency = &freq
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
@@ -24,121 +22,6 @@
|
||||
snapshotExpireDelete = snapshotExpireCommand.Flag("delete", "Whether to actually delete snapshots").Default("no").String()
|
||||
)
|
||||
|
||||
type cutoffTimes struct {
|
||||
annual time.Time
|
||||
monthly time.Time
|
||||
daily time.Time
|
||||
hourly time.Time
|
||||
weekly time.Time
|
||||
}
|
||||
|
||||
func yearsAgo(base time.Time, n int) time.Time {
|
||||
return base.AddDate(-n, 0, 0)
|
||||
}
|
||||
|
||||
func monthsAgo(base time.Time, n int) time.Time {
|
||||
return base.AddDate(0, -n, 0)
|
||||
}
|
||||
|
||||
func daysAgo(base time.Time, n int) time.Time {
|
||||
return base.AddDate(0, 0, -n)
|
||||
}
|
||||
|
||||
func weeksAgo(base time.Time, n int) time.Time {
|
||||
return base.AddDate(0, 0, -n*7)
|
||||
}
|
||||
|
||||
func hoursAgo(base time.Time, n int) time.Time {
|
||||
return base.Add(time.Duration(-n) * time.Hour)
|
||||
}
|
||||
|
||||
func expireSnapshotsForSingleSource(snapshots []*snapshot.Manifest, src snapshot.SourceInfo, pol *snapshot.Policy, snapshotNames []string) []string {
|
||||
var toDelete []string
|
||||
|
||||
now := time.Now()
|
||||
maxTime := now.Add(365 * 24 * time.Hour)
|
||||
|
||||
cutoffTime := func(setting *int, add func(time.Time, int) time.Time) time.Time {
|
||||
if setting != nil {
|
||||
return add(now, *setting)
|
||||
}
|
||||
|
||||
return maxTime
|
||||
}
|
||||
|
||||
cutoff := cutoffTimes{
|
||||
annual: cutoffTime(pol.RetentionPolicy.KeepAnnual, yearsAgo),
|
||||
monthly: cutoffTime(pol.RetentionPolicy.KeepMonthly, monthsAgo),
|
||||
daily: cutoffTime(pol.RetentionPolicy.KeepDaily, daysAgo),
|
||||
hourly: cutoffTime(pol.RetentionPolicy.KeepHourly, hoursAgo),
|
||||
weekly: cutoffTime(pol.RetentionPolicy.KeepHourly, weeksAgo),
|
||||
}
|
||||
|
||||
fmt.Printf("\nProcessing %v\n", src)
|
||||
ids := make(map[string]bool)
|
||||
idCounters := make(map[string]int)
|
||||
|
||||
for i, s := range snapshots {
|
||||
keep := getReasonsToKeep(i, s, cutoff, pol, ids, idCounters)
|
||||
|
||||
tm := s.StartTime.Local().Format("2006-01-02 15:04:05 MST")
|
||||
if len(keep) > 0 {
|
||||
fmt.Printf(" keeping %v (%v) %v\n", tm, s.ID, strings.Join(keep, ","))
|
||||
} else {
|
||||
fmt.Printf(" deleting %v (%v)\n", tm, s.ID)
|
||||
toDelete = append(toDelete, s.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return toDelete
|
||||
}
|
||||
|
||||
func getReasonsToKeep(i int, s *snapshot.Manifest, cutoff cutoffTimes, pol *snapshot.Policy, ids map[string]bool, idCounters map[string]int) []string {
|
||||
if s.IncompleteReason != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var keepReasons []string
|
||||
var zeroTime time.Time
|
||||
|
||||
yyyy, wk := s.StartTime.ISOWeek()
|
||||
|
||||
cases := []struct {
|
||||
cutoffTime time.Time
|
||||
timePeriodID string
|
||||
timePeriodType string
|
||||
max *int
|
||||
}{
|
||||
{zeroTime, fmt.Sprintf("%v", i), "latest", pol.RetentionPolicy.KeepLatest},
|
||||
{cutoff.annual, s.StartTime.Format("2006"), "annual", pol.RetentionPolicy.KeepAnnual},
|
||||
{cutoff.monthly, s.StartTime.Format("2006-01"), "monthly", pol.RetentionPolicy.KeepMonthly},
|
||||
{cutoff.weekly, fmt.Sprintf("%04v-%02v", yyyy, wk), "weekly", pol.RetentionPolicy.KeepWeekly},
|
||||
{cutoff.daily, s.StartTime.Format("2006-01-02"), "daily", pol.RetentionPolicy.KeepDaily},
|
||||
{cutoff.hourly, s.StartTime.Format("2006-01-02 15"), "hourly", pol.RetentionPolicy.KeepHourly},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
if c.max == nil {
|
||||
continue
|
||||
}
|
||||
if s.StartTime.Before(c.cutoffTime) {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, exists := ids[c.timePeriodID]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
if idCounters[c.timePeriodType] < *c.max {
|
||||
ids[c.timePeriodID] = true
|
||||
idCounters[c.timePeriodType]++
|
||||
keepReasons = append(keepReasons, c.timePeriodType)
|
||||
}
|
||||
}
|
||||
|
||||
return keepReasons
|
||||
}
|
||||
|
||||
func getSnapshotNamesToExpire(mgr *snapshot.Manager) ([]string, error) {
|
||||
if !*snapshotExpireAll && len(*snapshotExpirePaths) == 0 {
|
||||
return nil, fmt.Errorf("Must specify paths to expire or --all")
|
||||
@@ -173,56 +56,41 @@ func getSnapshotNamesToExpire(mgr *snapshot.Manager) ([]string, error) {
|
||||
}
|
||||
|
||||
func expireSnapshots(pmgr *snapshot.PolicyManager, snapshots []*snapshot.Manifest, names []string) ([]string, error) {
|
||||
var lastSource snapshot.SourceInfo
|
||||
var pendingSnapshots []*snapshot.Manifest
|
||||
var pendingNames []string
|
||||
var toDelete []string
|
||||
|
||||
flush := func() error {
|
||||
if len(pendingSnapshots) > 0 {
|
||||
src := pendingSnapshots[0].Source
|
||||
pol, err := pmgr.GetEffectivePolicy(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
td := expireSnapshotsForSingleSource(pendingSnapshots, src, pol, pendingNames)
|
||||
if len(td) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "Nothing to delete for %q.\n", src)
|
||||
} else {
|
||||
log.Printf("would delete %v out of %v snapshots for %q", len(td), len(pendingSnapshots), src)
|
||||
toDelete = append(toDelete, td...)
|
||||
}
|
||||
for _, snapshotGroup := range snapshot.GroupBySource(snapshots) {
|
||||
td, err := expireSnapshotsForSingleSource(pmgr, snapshotGroup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pendingSnapshots = nil
|
||||
pendingNames = nil
|
||||
return nil
|
||||
toDelete = append(toDelete, td...)
|
||||
}
|
||||
return toDelete, nil
|
||||
}
|
||||
|
||||
sort.Slice(snapshots, func(i, j int) bool {
|
||||
s1, s2 := snapshots[i].Source, snapshots[j].Source
|
||||
|
||||
if s1.String() != s2.String() {
|
||||
return s1.String() < s2.String()
|
||||
}
|
||||
|
||||
return snapshots[i].StartTime.Before(snapshots[j].StartTime)
|
||||
})
|
||||
|
||||
for i, s := range snapshots {
|
||||
if s.Source != lastSource {
|
||||
lastSource = s.Source
|
||||
if err := flush(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
pendingSnapshots = append(pendingSnapshots, s)
|
||||
pendingNames = append(pendingNames, names[i])
|
||||
}
|
||||
if err := flush(); err != nil {
|
||||
func expireSnapshotsForSingleSource(pmgr *snapshot.PolicyManager, snapshots []*snapshot.Manifest) ([]string, error) {
|
||||
src := snapshots[0].Source
|
||||
pol, err := pmgr.GetEffectivePolicy(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pol.RetentionPolicy.ComputeRetentionReasons(snapshots)
|
||||
|
||||
var toDelete []string
|
||||
for _, s := range snapshots {
|
||||
if len(s.RetentionReasons) == 0 {
|
||||
log.Printf(" deleting %v", s.StartTime)
|
||||
toDelete = append(toDelete, s.ID)
|
||||
} else {
|
||||
log.Printf(" keeping %v reasons: [%v]", s.StartTime, strings.Join(s.RetentionReasons, ","))
|
||||
}
|
||||
}
|
||||
if len(toDelete) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "Nothing to delete for %q.\n", src)
|
||||
} else {
|
||||
fmt.Printf("Would delete %v/%v snapshots for %v\n", len(toDelete), len(snapshots), src)
|
||||
}
|
||||
|
||||
return toDelete, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -80,20 +79,35 @@ func runBackupsCommand(ctx context.Context, rep *repo.Repository) error {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Sort(manifestSorter(manifests))
|
||||
outputManifests(manifests, relPath)
|
||||
polMgr := snapshot.NewPolicyManager(rep)
|
||||
|
||||
outputManifestGroups(manifests, relPath, polMgr)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputManifests(manifests []*snapshot.Manifest, relPath string) {
|
||||
var lastSource snapshot.SourceInfo
|
||||
func outputManifestGroups(manifests []*snapshot.Manifest, relPath string, polMgr *snapshot.PolicyManager) {
|
||||
separator := ""
|
||||
for _, snapshotGroup := range snapshot.GroupBySource(manifests) {
|
||||
src := snapshotGroup[0].Source
|
||||
fmt.Printf("%v%v\n", separator, src)
|
||||
separator = "\n"
|
||||
|
||||
pol, err := polMgr.GetEffectivePolicy(src)
|
||||
if err != nil {
|
||||
log.Warn().Msgf("unable to determine effective policy for %v", src)
|
||||
} else {
|
||||
pol.RetentionPolicy.ComputeRetentionReasons(snapshotGroup)
|
||||
}
|
||||
outputManifestFromSingleSource(snapshotGroup, relPath)
|
||||
}
|
||||
}
|
||||
|
||||
func outputManifestFromSingleSource(manifests []*snapshot.Manifest, relPath string) {
|
||||
var count int
|
||||
var lastTotalFileSize int64
|
||||
|
||||
separator := ""
|
||||
|
||||
for _, m := range manifests {
|
||||
for _, m := range snapshot.SortByTime(manifests, false) {
|
||||
maybeIncomplete := ""
|
||||
if m.IncompleteReason != "" {
|
||||
if !*snapshotListIncludeIncomplete {
|
||||
@@ -102,32 +116,28 @@ func outputManifests(manifests []*snapshot.Manifest, relPath string) {
|
||||
maybeIncomplete = " " + m.IncompleteReason
|
||||
}
|
||||
|
||||
if m.Source != lastSource {
|
||||
fmt.Printf("%v%v\n", separator, m.Source)
|
||||
separator = "\n"
|
||||
lastSource = m.Source
|
||||
count = 0
|
||||
lastTotalFileSize = m.Stats.TotalFileSize
|
||||
if count > *maxResultsPerPath {
|
||||
return
|
||||
}
|
||||
|
||||
if count < *maxResultsPerPath {
|
||||
fmt.Printf(
|
||||
" %v %v%v %v %v%v\n",
|
||||
m.StartTime.Format("2006-01-02 15:04:05 MST"),
|
||||
m.RootObjectID,
|
||||
relPath,
|
||||
units.BytesStringBase10(m.Stats.TotalFileSize),
|
||||
deltaBytes(m.Stats.TotalFileSize-lastTotalFileSize),
|
||||
maybeIncomplete,
|
||||
)
|
||||
if *snapshotListShowItemID {
|
||||
fmt.Printf(" metadata: %v\n", m.ID)
|
||||
}
|
||||
if *snapshotListShowHashCache {
|
||||
fmt.Printf(" hashcache: %v\n", m.HashCacheID)
|
||||
}
|
||||
count++
|
||||
fmt.Printf(
|
||||
" %v %v%v %v %v %v %v\n",
|
||||
m.StartTime.Format("2006-01-02 15:04:05 MST"),
|
||||
m.RootObjectID,
|
||||
relPath,
|
||||
units.BytesStringBase10(m.Stats.TotalFileSize),
|
||||
retentionReasonString(m.RetentionReasons),
|
||||
deltaBytes(m.Stats.TotalFileSize-lastTotalFileSize),
|
||||
maybeIncomplete,
|
||||
)
|
||||
|
||||
if *snapshotListShowItemID {
|
||||
fmt.Printf(" metadata: %v\n", m.ID)
|
||||
}
|
||||
if *snapshotListShowHashCache {
|
||||
fmt.Printf(" hashcache: %v\n", m.HashCacheID)
|
||||
}
|
||||
count++
|
||||
|
||||
if m.IncompleteReason == "" || !*snapshotListIncludeIncomplete {
|
||||
lastTotalFileSize = m.Stats.TotalFileSize
|
||||
@@ -135,19 +145,6 @@ func outputManifests(manifests []*snapshot.Manifest, relPath string) {
|
||||
}
|
||||
}
|
||||
|
||||
type manifestSorter []*snapshot.Manifest
|
||||
|
||||
func (b manifestSorter) Len() int { return len(b) }
|
||||
func (b manifestSorter) Less(i, j int) bool {
|
||||
if c := strings.Compare(b[i].Source.String(), b[j].Source.String()); c != 0 {
|
||||
return c < 0
|
||||
}
|
||||
|
||||
return b[i].StartTime.UnixNano() < b[j].StartTime.UnixNano()
|
||||
}
|
||||
|
||||
func (b manifestSorter) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
||||
|
||||
func deltaBytes(b int64) string {
|
||||
if b > 0 {
|
||||
return "(+" + units.BytesStringBase10(b) + ")"
|
||||
@@ -156,6 +153,13 @@ func deltaBytes(b int64) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func retentionReasonString(s []string) string {
|
||||
if len(s) == 0 {
|
||||
return "-"
|
||||
}
|
||||
return strings.Join(s, ",")
|
||||
}
|
||||
|
||||
func init() {
|
||||
snapshotListCommand.Action(repositoryAction(runBackupsCommand))
|
||||
}
|
||||
|
||||
68
snapshot/files_policy.go
Normal file
68
snapshot/files_policy.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package snapshot
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/kopia/kopia/fs"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// FilesPolicy describes files to be uploaded when taking snapshots
|
||||
type FilesPolicy struct {
|
||||
Include []string `json:"include,omitempty"`
|
||||
Exclude []string `json:"exclude,omitempty"`
|
||||
MaxSize *int `json:"maxSize,omitempty"`
|
||||
}
|
||||
|
||||
// ShouldInclude determines whether given filesystem entry should be included based on the policy.
|
||||
func (p *FilesPolicy) ShouldInclude(e *fs.EntryMetadata) bool {
|
||||
if len(p.Include) > 0 && !fileNameMatchesAnyPattern(e, p.Include) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(p.Exclude) > 0 && fileNameMatchesAnyPattern(e, p.Include) {
|
||||
return false
|
||||
}
|
||||
|
||||
if p.MaxSize != nil && e.Type == fs.EntryTypeFile && e.FileSize > int64(*p.MaxSize) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func fileNameMatches(fname string, pattern string) bool {
|
||||
ok, err := filepath.Match(pattern, fname)
|
||||
if err != nil {
|
||||
log.Printf("warning: %v, assuming %q does not match the pattern", err, fname)
|
||||
return false
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
func fileNameMatchesAnyPattern(e *fs.EntryMetadata, patterns []string) bool {
|
||||
for _, i := range patterns {
|
||||
if fileNameMatches(e.Name, i) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func mergeFilesPolicy(dst, src *FilesPolicy) {
|
||||
if dst.MaxSize == nil {
|
||||
dst.MaxSize = src.MaxSize
|
||||
}
|
||||
|
||||
if len(dst.Include) == 0 {
|
||||
dst.Include = src.Include
|
||||
}
|
||||
|
||||
if len(dst.Exclude) == 0 {
|
||||
dst.Exclude = src.Exclude
|
||||
}
|
||||
}
|
||||
|
||||
var defaultFilesPolicy = &FilesPolicy{}
|
||||
@@ -1,6 +1,7 @@
|
||||
package snapshot
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/kopia/kopia/object"
|
||||
@@ -18,8 +19,37 @@ type Manifest struct {
|
||||
RootObjectID object.ID `json:"root"`
|
||||
HashCacheID object.ID `json:"hashCache"`
|
||||
HashCacheCutoffTime time.Time `json:"hashCacheCutoff"`
|
||||
Stats Stats `json:"stats"`
|
||||
IncompleteReason string `json:"incomplete,omitempty"`
|
||||
|
||||
Stats Stats `json:"stats"`
|
||||
|
||||
IncompleteReason string `json:"incomplete,omitempty"`
|
||||
RetentionReasons []string `json:"-"`
|
||||
}
|
||||
|
||||
// GroupBySource returns a slice of slices, such that each result item contains manifests from a single source.
|
||||
func GroupBySource(manifests []*Manifest) [][]*Manifest {
|
||||
resultMap := map[SourceInfo][]*Manifest{}
|
||||
for _, m := range manifests {
|
||||
resultMap[m.Source] = append(resultMap[m.Source], m)
|
||||
}
|
||||
|
||||
var result [][]*Manifest
|
||||
for _, v := range resultMap {
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i][0].Source.String() < result[j][0].Source.String()
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// SortByTime returns a slice of manifests sorted by start time.
|
||||
func SortByTime(manifests []*Manifest, reverse bool) []*Manifest {
|
||||
result := append([]*Manifest(nil), manifests...)
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].StartTime.After(result[j].StartTime) == reverse
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -4,77 +4,13 @@
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/kopia/kopia/fs"
|
||||
)
|
||||
|
||||
// ErrPolicyNotFound is returned when the policy is not found.
|
||||
var ErrPolicyNotFound = errors.New("policy not found")
|
||||
|
||||
// RetentionPolicy describes snapshot retention policy.
|
||||
type RetentionPolicy struct {
|
||||
KeepLatest *int `json:"keepLatest,omitempty"`
|
||||
KeepHourly *int `json:"keepHourly,omitempty"`
|
||||
KeepDaily *int `json:"keepDaily,omitempty"`
|
||||
KeepWeekly *int `json:"keepWeekly,omitempty"`
|
||||
KeepMonthly *int `json:"keepMonthly,omitempty"`
|
||||
KeepAnnual *int `json:"keepAnnual,omitempty"`
|
||||
}
|
||||
|
||||
var defaultRetentionPolicy = &RetentionPolicy{
|
||||
KeepLatest: intPtr(1),
|
||||
KeepHourly: intPtr(48),
|
||||
KeepDaily: intPtr(7),
|
||||
KeepWeekly: intPtr(4),
|
||||
KeepMonthly: intPtr(4),
|
||||
KeepAnnual: intPtr(0),
|
||||
}
|
||||
|
||||
// FilesPolicy describes files to be uploaded when taking snapshots
|
||||
type FilesPolicy struct {
|
||||
Include []string `json:"include,omitempty"`
|
||||
Exclude []string `json:"exclude,omitempty"`
|
||||
MaxSize *int `json:"maxSize,omitempty"`
|
||||
}
|
||||
|
||||
// SchedulingPolicy describes policy for scheduling snapshots.
|
||||
type SchedulingPolicy struct {
|
||||
Frequency time.Duration `json:"frequency"`
|
||||
}
|
||||
|
||||
// ShouldInclude determines whether given filesystem entry should be included based on the policy.
|
||||
func (p *FilesPolicy) ShouldInclude(e *fs.EntryMetadata) bool {
|
||||
if len(p.Include) > 0 && !fileNameMatchesAnyPattern(e, p.Include) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(p.Exclude) > 0 && fileNameMatchesAnyPattern(e, p.Include) {
|
||||
return false
|
||||
}
|
||||
|
||||
if p.MaxSize != nil && e.Type == fs.EntryTypeFile && e.FileSize > int64(*p.MaxSize) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func fileNameMatchesAnyPattern(e *fs.EntryMetadata, patterns []string) bool {
|
||||
for _, i := range patterns {
|
||||
if fileNameMatches(e.Name, i) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
var defaultFilesPolicy = &FilesPolicy{}
|
||||
|
||||
// Policy describes snapshot policy for a single source.
|
||||
type Policy struct {
|
||||
Labels map[string]string `json:"-"`
|
||||
@@ -95,16 +31,6 @@ func (p *Policy) String() string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func fileNameMatches(fname string, pattern string) bool {
|
||||
ok, err := filepath.Match(pattern, fname)
|
||||
if err != nil {
|
||||
log.Printf("warning: %v, assuming %q does not match the pattern", err, fname)
|
||||
return false
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// MergePolicies computes the policy by applying the specified list of policies in order.
|
||||
func MergePolicies(policies []*Policy) *Policy {
|
||||
var merged Policy
|
||||
@@ -116,50 +42,17 @@ func MergePolicies(policies []*Policy) *Policy {
|
||||
|
||||
mergeRetentionPolicy(&merged.RetentionPolicy, &p.RetentionPolicy)
|
||||
mergeFilesPolicy(&merged.FilesPolicy, &p.FilesPolicy)
|
||||
mergeSchedulingPolicy(&merged.SchedulingPolicy, &p.SchedulingPolicy)
|
||||
}
|
||||
|
||||
// Merge default expiration policy.
|
||||
mergeRetentionPolicy(&merged.RetentionPolicy, defaultRetentionPolicy)
|
||||
mergeFilesPolicy(&merged.FilesPolicy, defaultFilesPolicy)
|
||||
mergeSchedulingPolicy(&merged.SchedulingPolicy, defaultSchedulingPolicy)
|
||||
|
||||
return &merged
|
||||
}
|
||||
|
||||
func mergeRetentionPolicy(dst, src *RetentionPolicy) {
|
||||
if dst.KeepLatest == nil {
|
||||
dst.KeepLatest = src.KeepLatest
|
||||
}
|
||||
if dst.KeepHourly == nil {
|
||||
dst.KeepHourly = src.KeepHourly
|
||||
}
|
||||
if dst.KeepDaily == nil {
|
||||
dst.KeepDaily = src.KeepDaily
|
||||
}
|
||||
if dst.KeepWeekly == nil {
|
||||
dst.KeepWeekly = src.KeepWeekly
|
||||
}
|
||||
if dst.KeepMonthly == nil {
|
||||
dst.KeepMonthly = src.KeepMonthly
|
||||
}
|
||||
if dst.KeepAnnual == nil {
|
||||
dst.KeepAnnual = src.KeepAnnual
|
||||
}
|
||||
}
|
||||
|
||||
func mergeFilesPolicy(dst, src *FilesPolicy) {
|
||||
if dst.MaxSize == nil {
|
||||
dst.MaxSize = src.MaxSize
|
||||
}
|
||||
|
||||
if len(dst.Include) == 0 {
|
||||
dst.Include = src.Include
|
||||
}
|
||||
|
||||
if len(dst.Exclude) == 0 {
|
||||
dst.Exclude = src.Exclude
|
||||
}
|
||||
}
|
||||
|
||||
func intPtr(n int) *int {
|
||||
return &n
|
||||
}
|
||||
|
||||
150
snapshot/retention_policy.go
Normal file
150
snapshot/retention_policy.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package snapshot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RetentionPolicy describes snapshot retention policy.
|
||||
type RetentionPolicy struct {
|
||||
KeepLatest *int `json:"keepLatest,omitempty"`
|
||||
KeepHourly *int `json:"keepHourly,omitempty"`
|
||||
KeepDaily *int `json:"keepDaily,omitempty"`
|
||||
KeepWeekly *int `json:"keepWeekly,omitempty"`
|
||||
KeepMonthly *int `json:"keepMonthly,omitempty"`
|
||||
KeepAnnual *int `json:"keepAnnual,omitempty"`
|
||||
}
|
||||
|
||||
// ComputeRetentionReasons computes the reasons why each snapshot is retained, based on
|
||||
// the settings in retention policy and stores them in RetentionReason field.
|
||||
func (r *RetentionPolicy) ComputeRetentionReasons(manifests []*Manifest) {
|
||||
now := time.Now()
|
||||
maxTime := now.Add(365 * 24 * time.Hour)
|
||||
|
||||
cutoffTime := func(setting *int, add func(time.Time, int) time.Time) time.Time {
|
||||
if setting != nil {
|
||||
return add(now, *setting)
|
||||
}
|
||||
|
||||
return maxTime
|
||||
}
|
||||
|
||||
cutoff := cutoffTimes{
|
||||
annual: cutoffTime(r.KeepAnnual, yearsAgo),
|
||||
monthly: cutoffTime(r.KeepMonthly, monthsAgo),
|
||||
daily: cutoffTime(r.KeepDaily, daysAgo),
|
||||
hourly: cutoffTime(r.KeepHourly, hoursAgo),
|
||||
weekly: cutoffTime(r.KeepHourly, weeksAgo),
|
||||
}
|
||||
|
||||
ids := make(map[string]bool)
|
||||
idCounters := make(map[string]int)
|
||||
|
||||
for i, s := range SortByTime(manifests, true) {
|
||||
s.RetentionReasons = r.getRetentionReasons(i, s, cutoff, ids, idCounters)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RetentionPolicy) getRetentionReasons(i int, s *Manifest, cutoff cutoffTimes, ids map[string]bool, idCounters map[string]int) []string {
|
||||
if s.IncompleteReason != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var keepReasons []string
|
||||
var zeroTime time.Time
|
||||
|
||||
yyyy, wk := s.StartTime.ISOWeek()
|
||||
|
||||
cases := []struct {
|
||||
cutoffTime time.Time
|
||||
timePeriodID string
|
||||
timePeriodType string
|
||||
max *int
|
||||
}{
|
||||
{zeroTime, fmt.Sprintf("%v", i), "latest", r.KeepLatest},
|
||||
{cutoff.annual, s.StartTime.Format("2006"), "annual", r.KeepAnnual},
|
||||
{cutoff.monthly, s.StartTime.Format("2006-01"), "monthly", r.KeepMonthly},
|
||||
{cutoff.weekly, fmt.Sprintf("%04v-%02v", yyyy, wk), "weekly", r.KeepWeekly},
|
||||
{cutoff.daily, s.StartTime.Format("2006-01-02"), "daily", r.KeepDaily},
|
||||
{cutoff.hourly, s.StartTime.Format("2006-01-02 15"), "hourly", r.KeepHourly},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
if c.max == nil {
|
||||
continue
|
||||
}
|
||||
if s.StartTime.Before(c.cutoffTime) {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, exists := ids[c.timePeriodID]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
if idCounters[c.timePeriodType] < *c.max {
|
||||
ids[c.timePeriodID] = true
|
||||
idCounters[c.timePeriodType]++
|
||||
keepReasons = append(keepReasons, c.timePeriodType)
|
||||
}
|
||||
}
|
||||
|
||||
return keepReasons
|
||||
}
|
||||
|
||||
type cutoffTimes struct {
|
||||
annual time.Time
|
||||
monthly time.Time
|
||||
daily time.Time
|
||||
hourly time.Time
|
||||
weekly time.Time
|
||||
}
|
||||
|
||||
func yearsAgo(base time.Time, n int) time.Time {
|
||||
return base.AddDate(-n, 0, 0)
|
||||
}
|
||||
|
||||
func monthsAgo(base time.Time, n int) time.Time {
|
||||
return base.AddDate(0, -n, 0)
|
||||
}
|
||||
|
||||
func daysAgo(base time.Time, n int) time.Time {
|
||||
return base.AddDate(0, 0, -n)
|
||||
}
|
||||
|
||||
func weeksAgo(base time.Time, n int) time.Time {
|
||||
return base.AddDate(0, 0, -n*7)
|
||||
}
|
||||
|
||||
func hoursAgo(base time.Time, n int) time.Time {
|
||||
return base.Add(time.Duration(-n) * time.Hour)
|
||||
}
|
||||
|
||||
var defaultRetentionPolicy = &RetentionPolicy{
|
||||
KeepLatest: intPtr(1),
|
||||
KeepHourly: intPtr(48),
|
||||
KeepDaily: intPtr(7),
|
||||
KeepWeekly: intPtr(4),
|
||||
KeepMonthly: intPtr(4),
|
||||
KeepAnnual: intPtr(0),
|
||||
}
|
||||
|
||||
func mergeRetentionPolicy(dst, src *RetentionPolicy) {
|
||||
if dst.KeepLatest == nil {
|
||||
dst.KeepLatest = src.KeepLatest
|
||||
}
|
||||
if dst.KeepHourly == nil {
|
||||
dst.KeepHourly = src.KeepHourly
|
||||
}
|
||||
if dst.KeepDaily == nil {
|
||||
dst.KeepDaily = src.KeepDaily
|
||||
}
|
||||
if dst.KeepWeekly == nil {
|
||||
dst.KeepWeekly = src.KeepWeekly
|
||||
}
|
||||
if dst.KeepMonthly == nil {
|
||||
dst.KeepMonthly = src.KeepMonthly
|
||||
}
|
||||
if dst.KeepAnnual == nil {
|
||||
dst.KeepAnnual = src.KeepAnnual
|
||||
}
|
||||
}
|
||||
16
snapshot/scheduling_policy.go
Normal file
16
snapshot/scheduling_policy.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package snapshot
|
||||
|
||||
import "time"
|
||||
|
||||
// SchedulingPolicy describes policy for scheduling snapshots.
|
||||
type SchedulingPolicy struct {
|
||||
MaxFrequency *time.Duration `json:"frequency"`
|
||||
}
|
||||
|
||||
func mergeSchedulingPolicy(dst, src *SchedulingPolicy) {
|
||||
if dst.MaxFrequency == nil {
|
||||
dst.MaxFrequency = src.MaxFrequency
|
||||
}
|
||||
}
|
||||
|
||||
var defaultSchedulingPolicy = &SchedulingPolicy{}
|
||||
Reference in New Issue
Block a user