1
0
Fork 0
mirror of https://github.com/restic/restic.git synced 2025-03-09 00:00:02 +01:00
This commit is contained in:
Kjetil Torgrim Homme 2025-02-27 03:41:56 +00:00 committed by GitHub
commit 67570dffe4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 121 additions and 4 deletions

View file

@ -0,0 +1,10 @@
Enhancement: Add snapshot filtering options --older-than and --newer-than
Restic subcommands which work on a set of snapshots (``copy``,
``find``, ``forget``, ``snapshots``, ``tag`` and more) can now filter
on the absolute age of the snapshots specified as a duration, e.g.,
``--newer-than 1d12h`` to only operate on snapshots created less than
36 hours ago, or ``--older-than 90d`` to operate on snapshots older
than three months.
https://github.com/restic/restic/issues/5136

View file

@ -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

View file

@ -34,6 +34,9 @@ size of the contained files at the time when the snapshot was created.
590c8fc8 2015-05-08 21:47:38 kazik /srv 580.200MiB
9f0bc19e 2015-05-08 21:46:11 luigi /srv 572.180MiB
If you have many snapshots, you can restrict it to only snapshots made
the last week, using ``-newer-than 7d``.
You can filter the listing by directory path:
.. code-block:: console

View file

@ -353,7 +353,9 @@ Since restic 0.17.0, it is possible to delete all snapshots for a specific
host, tag or path using the ``--unsafe-allow-remove-all`` option. The option
must always be combined with a snapshot filter (by host, path or tag).
For example the command ``forget --tag example --unsafe-allow-remove-all``
removes all snapshots with tag ``example``.
removes all snapshots with tag ``example``. Since restic 0.17.4, this
can be combined with an option like ``--older-than 1y`` to implement
a hard maximum retention for snapshots.
Security considerations in append-only mode

View file

@ -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
}
// PastTime returns past point in time where this duration is relative to time t. 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 {

View file

@ -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, _ := 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)
}
})
}

View file

@ -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

View file

@ -20,16 +20,20 @@ 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 {
return len(f.Hosts)+len(f.Tags)+len(f.Paths) == 0
return len(f.Hosts)+len(f.Tags)+len(f.Paths) == 0 && f.NewerThan.Zero() && f.OlderThan.Zero()
}
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,

View file

@ -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)
}