Add custom Duration type
This commit is contained in:
parent
5a0f0e3faa
commit
cc627e832b
5 changed files with 250 additions and 22 deletions
|
@ -27,14 +27,14 @@ data after 'forget' was run successfully, see the 'prune' command. `,
|
|||
|
||||
// ForgetOptions collects all options for the forget command.
|
||||
type ForgetOptions struct {
|
||||
Last int
|
||||
Hourly int
|
||||
Daily int
|
||||
Weekly int
|
||||
Monthly int
|
||||
Yearly int
|
||||
WithinDays int
|
||||
KeepTags restic.TagLists
|
||||
Last int
|
||||
Hourly int
|
||||
Daily int
|
||||
Weekly int
|
||||
Monthly int
|
||||
Yearly int
|
||||
Within restic.Duration
|
||||
KeepTags restic.TagLists
|
||||
|
||||
Host string
|
||||
Tags restic.TagLists
|
||||
|
@ -59,7 +59,7 @@ func init() {
|
|||
f.IntVarP(&forgetOptions.Weekly, "keep-weekly", "w", 0, "keep the last `n` weekly snapshots")
|
||||
f.IntVarP(&forgetOptions.Monthly, "keep-monthly", "m", 0, "keep the last `n` monthly snapshots")
|
||||
f.IntVarP(&forgetOptions.Yearly, "keep-yearly", "y", 0, "keep the last `n` yearly snapshots")
|
||||
f.IntVar(&forgetOptions.WithinDays, "keep-within", 0, "keep snapshots that were created within `days` before the newest")
|
||||
f.VarP(&forgetOptions.Within, "keep-within", "", "keep snapshots that were created within `duration` before the newest (e.g. 1y5m7d)")
|
||||
|
||||
f.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)")
|
||||
// Sadly the commonly used shortcut `H` is already used.
|
||||
|
@ -172,7 +172,7 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
|||
Weekly: opts.Weekly,
|
||||
Monthly: opts.Monthly,
|
||||
Yearly: opts.Yearly,
|
||||
Within: opts.WithinDays,
|
||||
Within: opts.Within,
|
||||
Tags: opts.KeepTags,
|
||||
}
|
||||
|
||||
|
|
131
internal/restic/duration.go
Normal file
131
internal/restic/duration.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
package restic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
)
|
||||
|
||||
// Duration is similar to time.Duration, except it only supports larger ranges
|
||||
// like days, months, and years.
|
||||
type Duration struct {
|
||||
Days, Months, Years int
|
||||
}
|
||||
|
||||
func (d Duration) String() string {
|
||||
var s string
|
||||
if d.Years != 0 {
|
||||
s += fmt.Sprintf("%dy", d.Years)
|
||||
}
|
||||
|
||||
if d.Months != 0 {
|
||||
s += fmt.Sprintf("%dm", d.Months)
|
||||
}
|
||||
|
||||
if d.Days != 0 {
|
||||
s += fmt.Sprintf("%dd", d.Days)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func nextNumber(input string) (num int, rest string, err error) {
|
||||
if len(input) == 0 {
|
||||
return 0, "", nil
|
||||
}
|
||||
|
||||
var (
|
||||
n string
|
||||
negative bool
|
||||
)
|
||||
|
||||
if input[0] == '-' {
|
||||
negative = true
|
||||
input = input[1:]
|
||||
}
|
||||
|
||||
for i, s := range input {
|
||||
if !unicode.IsNumber(s) {
|
||||
rest = input[i:]
|
||||
break
|
||||
}
|
||||
|
||||
n += string(s)
|
||||
}
|
||||
|
||||
if len(n) == 0 {
|
||||
return 0, input, errors.New("no number found")
|
||||
}
|
||||
|
||||
num, err = strconv.Atoi(n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if negative {
|
||||
num = -num
|
||||
}
|
||||
|
||||
return num, rest, nil
|
||||
}
|
||||
|
||||
// ParseDuration parses a duration from a string. The format is:
|
||||
// 6y5m234d
|
||||
func ParseDuration(s string) (Duration, error) {
|
||||
var (
|
||||
d Duration
|
||||
num int
|
||||
err error
|
||||
)
|
||||
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
for s != "" {
|
||||
num, s, err = nextNumber(s)
|
||||
if err != nil {
|
||||
return Duration{}, err
|
||||
}
|
||||
|
||||
if len(s) == 0 {
|
||||
return Duration{}, errors.Errorf("no unit found after number %d", num)
|
||||
}
|
||||
|
||||
switch s[0] {
|
||||
case 'y':
|
||||
d.Years = num
|
||||
case 'm':
|
||||
d.Months = num
|
||||
case 'd':
|
||||
d.Days = num
|
||||
}
|
||||
|
||||
s = s[1:]
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Set calls ParseDuration and updates d.
|
||||
func (d *Duration) Set(s string) error {
|
||||
v, err := ParseDuration(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*d = v
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type returns the type of Duration, usable within github.com/spf13/pflag and
|
||||
// in help texts.
|
||||
func (d Duration) Type() string {
|
||||
return "duration"
|
||||
}
|
||||
|
||||
// Zero returns true if the duration is empty (all values are set to zero).
|
||||
func (d Duration) Zero() bool {
|
||||
return d.Years == 0 && d.Months == 0 && d.Days == 0
|
||||
}
|
82
internal/restic/duration_test.go
Normal file
82
internal/restic/duration_test.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package restic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestNextNumber(t *testing.T) {
|
||||
var tests = []struct {
|
||||
input string
|
||||
num int
|
||||
rest string
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
input: "3d", num: 3, rest: "d",
|
||||
},
|
||||
{
|
||||
input: "7m5d", num: 7, rest: "m5d",
|
||||
},
|
||||
{
|
||||
input: "-23y7m5d", num: -23, rest: "y7m5d",
|
||||
},
|
||||
{
|
||||
input: " 5d", num: 0, rest: " 5d", err: true,
|
||||
},
|
||||
{
|
||||
input: "5d ", num: 5, rest: "d ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
num, rest, err := nextNumber(test.input)
|
||||
|
||||
if err != nil && !test.err {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if num != test.num {
|
||||
t.Errorf("wrong num, want %d, got %d", test.num, num)
|
||||
}
|
||||
|
||||
if rest != test.rest {
|
||||
t.Errorf("wrong rest, want %q, got %q", test.rest, rest)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDuration(t *testing.T) {
|
||||
var tests = []struct {
|
||||
input string
|
||||
d Duration
|
||||
output string
|
||||
}{
|
||||
{"3d", Duration{Days: 3}, "3d"},
|
||||
{"7m5d", Duration{Months: 7, Days: 5}, "7m5d"},
|
||||
{"5d7m", Duration{Months: 7, Days: 5}, "7m5d"},
|
||||
{"-7m5d", Duration{Months: -7, Days: 5}, "-7m5d"},
|
||||
{"2y7m-5d", Duration{Years: 2, Months: 7, Days: -5}, "2y7m-5d"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
d, err := ParseDuration(test.input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !cmp.Equal(d, test.d) {
|
||||
t.Error(cmp.Diff(test.d, d))
|
||||
}
|
||||
|
||||
s := d.String()
|
||||
if s != test.output {
|
||||
t.Errorf("unexpected return of String(), want %q, got %q", test.output, s)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ type ExpirePolicy struct {
|
|||
Weekly int // keep the last n weekly snapshots
|
||||
Monthly int // keep the last n monthly snapshots
|
||||
Yearly int // keep the last n yearly snapshots
|
||||
Within int // keep snapshots made within this number of days since the newest snapshot
|
||||
Within Duration // keep snapshots made within this duration
|
||||
Tags []TagList // keep all snapshots that include at least one of the tag lists.
|
||||
}
|
||||
|
||||
|
@ -40,11 +40,26 @@ func (e ExpirePolicy) String() (s string) {
|
|||
if e.Yearly > 0 {
|
||||
keeps = append(keeps, fmt.Sprintf("%d yearly", e.Yearly))
|
||||
}
|
||||
if e.Within != 0 {
|
||||
keeps = append(keeps, fmt.Sprintf("snapshots within %d days of the newest snapshot", e.Within))
|
||||
|
||||
if len(keeps) > 0 {
|
||||
s = fmt.Sprintf("keep the last %s snapshots", strings.Join(keeps, ", "))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("keep the last %s snapshots", strings.Join(keeps, ", "))
|
||||
if len(e.Tags) > 0 {
|
||||
if s != "" {
|
||||
s += " and "
|
||||
}
|
||||
s += fmt.Sprintf("all snapshots with tags %s", e.Tags)
|
||||
}
|
||||
|
||||
if !e.Within.Zero() {
|
||||
if s != "" {
|
||||
s += " and "
|
||||
}
|
||||
s += fmt.Sprintf("all snapshots within %s of the newest", e.Within)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Sum returns the maximum number of snapshots to be kept according to this
|
||||
|
@ -149,8 +164,8 @@ func ApplyPolicy(list Snapshots, p ExpirePolicy) (keep, remove Snapshots) {
|
|||
}
|
||||
|
||||
// If the timestamp of the snapshot is within the range, then keep it.
|
||||
if p.Within != 0 {
|
||||
t := latest.AddDate(0, 0, -p.Within)
|
||||
if !p.Within.Zero() {
|
||||
t := latest.AddDate(-p.Within.Years, -p.Within.Months, -p.Within.Days)
|
||||
if cur.Time.After(t) {
|
||||
keepSnap = true
|
||||
}
|
||||
|
|
|
@ -21,8 +21,8 @@ func parseTimeUTC(s string) time.Time {
|
|||
return t.UTC()
|
||||
}
|
||||
|
||||
func parseDuration(s string) time.Duration {
|
||||
d, err := time.ParseDuration(s)
|
||||
func parseDuration(s string) restic.Duration {
|
||||
d, err := restic.ParseDuration(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -180,10 +180,10 @@ var expireTests = []restic.ExpirePolicy{
|
|||
{Tags: []restic.TagList{{"foo"}}},
|
||||
{Tags: []restic.TagList{{"foo", "bar"}}},
|
||||
{Tags: []restic.TagList{{"foo"}, {"bar"}}},
|
||||
{Within: 1},
|
||||
{Within: 2},
|
||||
{Within: 7},
|
||||
{Within: 30},
|
||||
{Within: parseDuration("1d")},
|
||||
{Within: parseDuration("2d")},
|
||||
{Within: parseDuration("7d")},
|
||||
{Within: parseDuration("1m")},
|
||||
}
|
||||
|
||||
func TestApplyPolicy(t *testing.T) {
|
||||
|
|
Loading…
Reference in a new issue