diff --git a/cmd/restic/find.go b/cmd/restic/find.go index faf7024e1..407fcf5e4 100644 --- a/cmd/restic/find.go +++ b/cmd/restic/find.go @@ -17,6 +17,8 @@ func initMultiSnapshotFilter(flags *pflag.FlagSet, filt *restic.SnapshotFilter, } flags.StringArrayVarP(&filt.Hosts, "host", hostShorthand, nil, "only consider snapshots for this `host` (can be specified multiple times) (default: $RESTIC_HOST)") flags.Var(&filt.Tags, "tag", "only consider snapshots including `tag[,tag,...]` (can be specified multiple times)") + flags.Var(&filt.OlderThan, "older-than", "only consider snapshots made more than `duration` time ago") + flags.Var(&filt.NewerThan, "newer-than", "only consider snapshots made less than `duration` time ago") flags.StringArrayVar(&filt.Paths, "path", nil, "only consider snapshots including this (absolute) `path` (can be specified multiple times, snapshots must include all specified paths)") // set default based on env if set diff --git a/internal/restic/duration.go b/internal/restic/duration.go index 831971fe0..ed5acf24e 100644 --- a/internal/restic/duration.go +++ b/internal/restic/duration.go @@ -4,6 +4,7 @@ import ( "fmt" "strconv" "strings" + "time" "unicode" "github.com/restic/restic/internal/errors" @@ -126,6 +127,18 @@ func (d *Duration) Set(s string) error { return nil } +// Subtract duration from t to return past point in time. If t is not given, use time.Now(). +func (d Duration) PastTime(t ...time.Time) time.Time { + var reftime time.Time + if t == nil { + reftime = time.Now() + } else { + reftime = t[0] + } + + return reftime.AddDate(-d.Years, -d.Months, -d.Days).Add(-time.Duration(d.Hours) * time.Hour) +} + // Type returns the type of Duration, usable within github.com/spf13/pflag and // in help texts. func (d Duration) Type() string { diff --git a/internal/restic/duration_test.go b/internal/restic/duration_test.go index f03aa5553..60a618f08 100644 --- a/internal/restic/duration_test.go +++ b/internal/restic/duration_test.go @@ -2,6 +2,7 @@ package restic import ( "testing" + "time" "github.com/google/go-cmp/cmp" ) @@ -104,3 +105,18 @@ func TestParseDuration(t *testing.T) { }) } } + +func TestPastTime(t *testing.T) { + t.Run("", func(t *testing.T) { + d, err := ParseDuration("1y2m3d4h") + if err != nil { + t.Fatal(err) + } + reftime, err := time.Parse(time.DateTime, "1999-12-30 15:16:17") + expected := "1998-10-27 11:16:17" + result := d.PastTime(reftime).Format(time.DateTime) + if result != expected { + t.Errorf("unexpected return of PastTime, wanted %q, got %q", expected, result) + } + }) +} diff --git a/internal/restic/snapshot.go b/internal/restic/snapshot.go index f9cdf4daf..247b7c5f0 100644 --- a/internal/restic/snapshot.go +++ b/internal/restic/snapshot.go @@ -248,6 +248,22 @@ func (sn *Snapshot) HasHostname(hostnames []string) bool { return false } +// HasTimeBetween returns false if either +// - start is given and Time is before start, or +// - stop is given and Time is after stop +// Otherwise return true +// start is the value closest in time, ie. the shortest duration value. +func (sn *Snapshot) HasTimeBetween(start Duration, stop Duration) bool { + if !start.Zero() && sn.Time.After(start.PastTime()) { + return false + } + if !stop.Zero() && sn.Time.Before(stop.PastTime()) { + return false + } + + return true +} + // Snapshots is a list of snapshots. type Snapshots []*Snapshot diff --git a/internal/restic/snapshot_find.go b/internal/restic/snapshot_find.go index 6eb51b237..39d8765d4 100644 --- a/internal/restic/snapshot_find.go +++ b/internal/restic/snapshot_find.go @@ -20,8 +20,12 @@ type SnapshotFilter struct { Hosts []string Tags TagLists Paths []string - // Match snapshots from before this timestamp. Zero for no limit. + // Match snapshots from before this timestamp. Zero for no limit. Only used to find parent. TimestampLimit time.Time + // Match snapshots from before this timestamp. Zero for no limit. + NewerThan Duration + // Match snapshots from after this timestamp. Zero for no limit. + OlderThan Duration } func (f *SnapshotFilter) Empty() bool { @@ -29,7 +33,7 @@ func (f *SnapshotFilter) Empty() bool { } func (f *SnapshotFilter) matches(sn *Snapshot) bool { - return sn.HasHostname(f.Hosts) && sn.HasTagList(f.Tags) && sn.HasPaths(f.Paths) + return sn.HasHostname(f.Hosts) && sn.HasTagList(f.Tags) && sn.HasPaths(f.Paths) && sn.HasTimeBetween(f.OlderThan, f.NewerThan) } // findLatest finds the latest snapshot with optional target/directory, diff --git a/internal/restic/snapshot_find_test.go b/internal/restic/snapshot_find_test.go index 84bffd694..83c2e5c91 100644 --- a/internal/restic/snapshot_find_test.go +++ b/internal/restic/snapshot_find_test.go @@ -3,6 +3,7 @@ package restic_test import ( "context" "testing" + "time" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" @@ -89,3 +90,53 @@ func TestFindAllSubpathError(t *testing.T) { })) test.Assert(t, count == 2, "unexpected number of subfolder errors: %v, wanted %v", count, 2) } + +func TestFindAllNewerThan(t *testing.T) { + repo := repository.TestRepository(t) + now := time.Now() + + oneDay := time.Duration(24) * time.Hour + restic.TestCreateSnapshot(t, repo, now.Add(-14*oneDay), 1) + desiredSnapshot := restic.TestCreateSnapshot(t, repo, now.Add(-4*oneDay), 1) + + var found restic.Snapshot + count := 0 + test.OK(t, (&restic.SnapshotFilter{ + NewerThan: restic.Duration{Days: 5}, + }).FindAll(context.TODO(), repo, repo, nil, + func(id string, sn *restic.Snapshot, err error) error { + if err == nil { + found = *sn + count++ + } + return nil + })) + test.Assert(t, count == 1, "unexpected number of snapshots: %v, wanted %v", count, 1) + test.Assert(t, desiredSnapshot.ID().Equal(*found.ID()), "unexpected snapshot found: %s, wanted %s", desiredSnapshot, found) +} + +func TestFindAllWithin(t *testing.T) { + repo := repository.TestRepository(t) + now := time.Now() + + oneDay := time.Duration(24) * time.Hour + restic.TestCreateSnapshot(t, repo, now.Add(-14*oneDay), 1) + restic.TestCreateSnapshot(t, repo, now.Add(-1*oneDay), 1) + desiredSnapshot := restic.TestCreateSnapshot(t, repo, now.Add(-4*oneDay), 1) + + var found restic.Snapshot + count := 0 + test.OK(t, (&restic.SnapshotFilter{ + NewerThan: restic.Duration{Days: 5}, + OlderThan: restic.Duration{Days: 2}, + }).FindAll(context.TODO(), repo, repo, nil, + func(id string, sn *restic.Snapshot, err error) error { + if err == nil { + found = *sn + count++ + } + return nil + })) + test.Assert(t, count == 1, "unexpected number of snapshots: %v, wanted %v", count, 1) + test.Assert(t, desiredSnapshot.ID().Equal(*found.ID()), "unexpected snapshot found: %s, wanted %s", desiredSnapshot, found) +}