2
2
mirror of https://github.com/octoleo/restic.git synced 2024-05-29 07:00:49 +00:00
restic/internal/restic/snapshot_find.go
Michael Eischer 2841a87cc6 Fix snapshot filtering for relative paths in the backup command
The snapshot filtering internally converts relative paths to absolute
ones to ensure that the parent snapshots selection works for backups of
relative paths.
2023-04-14 21:53:55 +02:00

173 lines
4.3 KiB
Go

package restic
import (
"context"
"fmt"
"path/filepath"
"time"
"github.com/restic/restic/internal/errors"
)
// ErrNoSnapshotFound is returned when no snapshot for the given criteria could be found.
var ErrNoSnapshotFound = errors.New("no snapshot found")
// A SnapshotFilter denotes a set of snapshots based on hosts, tags and paths.
type SnapshotFilter struct {
_ struct{} // Force naming fields in literals.
Hosts []string
Tags TagLists
Paths []string
// Match snapshots from before this timestamp. Zero for no limit.
TimestampLimit time.Time
}
func (f *SnapshotFilter) empty() bool {
return len(f.Hosts)+len(f.Tags)+len(f.Paths) == 0
}
func (f *SnapshotFilter) matches(sn *Snapshot) bool {
return sn.HasHostname(f.Hosts) && sn.HasTagList(f.Tags) && sn.HasPaths(f.Paths)
}
// findLatest finds the latest snapshot with optional target/directory,
// tags, hostname, and timestamp filters.
func (f *SnapshotFilter) findLatest(ctx context.Context, be Lister, loader LoaderUnpacked) (*Snapshot, error) {
var err error
absTargets := make([]string, 0, len(f.Paths))
for _, target := range f.Paths {
if !filepath.IsAbs(target) {
target, err = filepath.Abs(target)
if err != nil {
return nil, errors.Wrap(err, "Abs")
}
}
absTargets = append(absTargets, filepath.Clean(target))
}
f.Paths = absTargets
var latest *Snapshot
err = ForAllSnapshots(ctx, be, loader, nil, func(id ID, snapshot *Snapshot, err error) error {
if err != nil {
return errors.Errorf("Error loading snapshot %v: %v", id.Str(), err)
}
if !f.TimestampLimit.IsZero() && snapshot.Time.After(f.TimestampLimit) {
return nil
}
if latest != nil && snapshot.Time.Before(latest.Time) {
return nil
}
if !f.matches(snapshot) {
return nil
}
latest = snapshot
return nil
})
if err != nil {
return nil, err
}
if latest == nil {
return nil, ErrNoSnapshotFound
}
return latest, nil
}
// FindSnapshot takes a string and tries to find a snapshot whose ID matches
// the string as closely as possible.
func FindSnapshot(ctx context.Context, be Lister, loader LoaderUnpacked, s string) (*Snapshot, error) {
// no need to list snapshots if `s` is already a full id
id, err := ParseID(s)
if err != nil {
// find snapshot id with prefix
id, err = Find(ctx, be, SnapshotFile, s)
if err != nil {
return nil, err
}
}
return LoadSnapshot(ctx, loader, id)
}
// FindLatest returns either the latest of a filtered list of all snapshots
// or a snapshot specified by `snapshotID`.
func (f *SnapshotFilter) FindLatest(ctx context.Context, be Lister, loader LoaderUnpacked, snapshotID string) (*Snapshot, error) {
if snapshotID == "latest" {
sn, err := f.findLatest(ctx, be, loader)
if err == ErrNoSnapshotFound {
err = fmt.Errorf("snapshot filter (Paths:%v Tags:%v Hosts:%v): %w",
f.Paths, f.Tags, f.Hosts, err)
}
return sn, err
}
return FindSnapshot(ctx, be, loader, snapshotID)
}
type SnapshotFindCb func(string, *Snapshot, error) error
// FindAll yields Snapshots, either given explicitly by `snapshotIDs` or filtered from the list of all snapshots.
func (f *SnapshotFilter) FindAll(ctx context.Context, be Lister, loader LoaderUnpacked, snapshotIDs []string, fn SnapshotFindCb) error {
if len(snapshotIDs) != 0 {
var err error
usedFilter := false
ids := NewIDSet()
// Process all snapshot IDs given as arguments.
for _, s := range snapshotIDs {
var sn *Snapshot
if s == "latest" {
if usedFilter {
continue
}
usedFilter = true
sn, err = f.findLatest(ctx, be, loader)
if err == ErrNoSnapshotFound {
err = errors.Errorf("no snapshot matched given filter (Paths:%v Tags:%v Hosts:%v)",
f.Paths, f.Tags, f.Hosts)
}
if sn != nil {
ids.Insert(*sn.ID())
}
} else {
sn, err = FindSnapshot(ctx, be, loader, s)
if err == nil {
if ids.Has(*sn.ID()) {
continue
} else {
ids.Insert(*sn.ID())
s = sn.ID().String()
}
}
}
err = fn(s, sn, err)
if err != nil {
return err
}
}
// Give the user some indication their filters are not used.
if !usedFilter && !f.empty() {
return fn("filters", nil, errors.Errorf("explicit snapshot ids are given"))
}
return nil
}
return ForAllSnapshots(ctx, be, loader, nil, func(id ID, sn *Snapshot, err error) error {
if err == nil && !f.matches(sn) {
return nil
}
return fn(id.String(), sn, err)
})
}