2
2
mirror of https://github.com/octoleo/restic.git synced 2024-11-22 12:55:18 +00:00

Add support for snapshot:subpath syntax

This snapshot specification syntax is supported by the cat, diff, dump,
ls and restore command.
This commit is contained in:
Michael Eischer 2023-05-18 23:15:38 +02:00
parent 60d8066568
commit 85860e6e97
9 changed files with 90 additions and 21 deletions

View File

@ -453,7 +453,7 @@ func findParentSnapshot(ctx context.Context, repo restic.Repository, opts Backup
f.Tags = []restic.TagList{opts.Tags.Flatten()} f.Tags = []restic.TagList{opts.Tags.Flatten()}
} }
sn, err := f.FindLatest(ctx, repo.Backend(), repo, snName) sn, _, err := f.FindLatest(ctx, repo.Backend(), repo, snName)
// Snapshot not found is ok if no explicit parent was set // Snapshot not found is ok if no explicit parent was set
if opts.Parent == "" && errors.Is(err, restic.ErrNoSnapshotFound) { if opts.Parent == "" && errors.Is(err, restic.ErrNoSnapshotFound) {
err = nil err = nil

View File

@ -80,7 +80,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
Println(string(buf)) Println(string(buf))
return nil return nil
case "snapshot": case "snapshot":
sn, err := restic.FindSnapshot(ctx, repo.Backend(), repo, args[1]) sn, _, err := restic.FindSnapshot(ctx, repo.Backend(), repo, args[1])
if err != nil { if err != nil {
return errors.Fatalf("could not find snapshot: %v\n", err) return errors.Fatalf("could not find snapshot: %v\n", err)
} }

View File

@ -54,12 +54,12 @@ func init() {
f.BoolVar(&diffOptions.ShowMetadata, "metadata", false, "print changes in metadata") f.BoolVar(&diffOptions.ShowMetadata, "metadata", false, "print changes in metadata")
} }
func loadSnapshot(ctx context.Context, be restic.Lister, repo restic.Repository, desc string) (*restic.Snapshot, error) { func loadSnapshot(ctx context.Context, be restic.Lister, repo restic.Repository, desc string) (*restic.Snapshot, string, error) {
sn, err := restic.FindSnapshot(ctx, be, repo, desc) sn, subpath, err := restic.FindSnapshot(ctx, be, repo, desc)
if err != nil { if err != nil {
return nil, errors.Fatal(err.Error()) return nil, "", errors.Fatal(err.Error())
} }
return sn, err return sn, subpath, err
} }
// Comparer collects all things needed to compare two snapshots. // Comparer collects all things needed to compare two snapshots.
@ -346,12 +346,12 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
if err != nil { if err != nil {
return err return err
} }
sn1, err := loadSnapshot(ctx, be, repo, args[0]) sn1, subpath1, err := loadSnapshot(ctx, be, repo, args[0])
if err != nil { if err != nil {
return err return err
} }
sn2, err := loadSnapshot(ctx, be, repo, args[1]) sn2, subpath2, err := loadSnapshot(ctx, be, repo, args[1])
if err != nil { if err != nil {
return err return err
} }
@ -372,6 +372,16 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
return errors.Errorf("snapshot %v has nil tree", sn2.ID().Str()) return errors.Errorf("snapshot %v has nil tree", sn2.ID().Str())
} }
sn1.Tree, err = restic.FindTreeDirectory(ctx, repo, sn1.Tree, subpath1)
if err != nil {
return err
}
sn2.Tree, err = restic.FindTreeDirectory(ctx, repo, sn2.Tree, subpath2)
if err != nil {
return err
}
c := &Comparer{ c := &Comparer{
repo: repo, repo: repo,
opts: diffOptions, opts: diffOptions,

View File

@ -139,7 +139,7 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []
} }
} }
sn, err := (&restic.SnapshotFilter{ sn, subpath, err := (&restic.SnapshotFilter{
Hosts: opts.Hosts, Hosts: opts.Hosts,
Paths: opts.Paths, Paths: opts.Paths,
Tags: opts.Tags, Tags: opts.Tags,
@ -153,6 +153,11 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []
return err return err
} }
sn.Tree, err = restic.FindTreeDirectory(ctx, repo, sn.Tree, subpath)
if err != nil {
return err
}
tree, err := restic.LoadTree(ctx, repo, *sn.Tree) tree, err := restic.LoadTree(ctx, repo, *sn.Tree)
if err != nil { if err != nil {
return errors.Fatalf("loading tree for snapshot %q failed: %v", snapshotIDString, err) return errors.Fatalf("loading tree for snapshot %q failed: %v", snapshotIDString, err)

View File

@ -212,7 +212,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
} }
} }
sn, err := (&restic.SnapshotFilter{ sn, subpath, err := (&restic.SnapshotFilter{
Hosts: opts.Hosts, Hosts: opts.Hosts,
Paths: opts.Paths, Paths: opts.Paths,
Tags: opts.Tags, Tags: opts.Tags,
@ -221,6 +221,11 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
return err return err
} }
sn.Tree, err = restic.FindTreeDirectory(ctx, repo, sn.Tree, subpath)
if err != nil {
return err
}
printSnapshot(sn) printSnapshot(sn)
err = walker.Walk(ctx, repo, *sn.Tree, nil, func(_ restic.ID, nodepath string, node *restic.Node, err error) (bool, error) { err = walker.Walk(ctx, repo, *sn.Tree, nil, func(_ restic.ID, nodepath string, node *restic.Node, err error) (bool, error) {

View File

@ -161,7 +161,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
} }
} }
sn, err := (&restic.SnapshotFilter{ sn, subpath, err := (&restic.SnapshotFilter{
Hosts: opts.Hosts, Hosts: opts.Hosts,
Paths: opts.Paths, Paths: opts.Paths,
Tags: opts.Tags, Tags: opts.Tags,
@ -175,6 +175,11 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
return err return err
} }
sn.Tree, err = restic.FindTreeDirectory(ctx, repo, sn.Tree, subpath)
if err != nil {
return err
}
msg := ui.NewMessage(term, gopts.verbosity) msg := ui.NewMessage(term, gopts.verbosity)
var printer restoreui.ProgressPrinter var printer restoreui.ProgressPrinter
if gopts.JSON { if gopts.JSON {

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
@ -82,31 +83,40 @@ func (f *SnapshotFilter) findLatest(ctx context.Context, be Lister, loader Loade
return latest, nil return latest, nil
} }
func splitSnapshotID(s string) (id, subpath string) {
id, subpath, _ = strings.Cut(s, ":")
return
}
// FindSnapshot takes a string and tries to find a snapshot whose ID matches // FindSnapshot takes a string and tries to find a snapshot whose ID matches
// the string as closely as possible. // the string as closely as possible.
func FindSnapshot(ctx context.Context, be Lister, loader LoaderUnpacked, s string) (*Snapshot, error) { func FindSnapshot(ctx context.Context, be Lister, loader LoaderUnpacked, s string) (*Snapshot, string, error) {
s, subpath := splitSnapshotID(s)
// no need to list snapshots if `s` is already a full id // no need to list snapshots if `s` is already a full id
id, err := ParseID(s) id, err := ParseID(s)
if err != nil { if err != nil {
// find snapshot id with prefix // find snapshot id with prefix
id, err = Find(ctx, be, SnapshotFile, s) id, err = Find(ctx, be, SnapshotFile, s)
if err != nil { if err != nil {
return nil, err return nil, "", err
} }
} }
return LoadSnapshot(ctx, loader, id) sn, err := LoadSnapshot(ctx, loader, id)
return sn, subpath, err
} }
// FindLatest returns either the latest of a filtered list of all snapshots // FindLatest returns either the latest of a filtered list of all snapshots
// or a snapshot specified by `snapshotID`. // or a snapshot specified by `snapshotID`.
func (f *SnapshotFilter) FindLatest(ctx context.Context, be Lister, loader LoaderUnpacked, snapshotID string) (*Snapshot, error) { func (f *SnapshotFilter) FindLatest(ctx context.Context, be Lister, loader LoaderUnpacked, snapshotID string) (*Snapshot, string, error) {
if snapshotID == "latest" { id, subpath := splitSnapshotID(snapshotID)
if id == "latest" {
sn, err := f.findLatest(ctx, be, loader) sn, err := f.findLatest(ctx, be, loader)
if err == ErrNoSnapshotFound { if err == ErrNoSnapshotFound {
err = fmt.Errorf("snapshot filter (Paths:%v Tags:%v Hosts:%v): %w", err = fmt.Errorf("snapshot filter (Paths:%v Tags:%v Hosts:%v): %w",
f.Paths, f.Tags, f.Hosts, err) f.Paths, f.Tags, f.Hosts, err)
} }
return sn, err return sn, subpath, err
} }
return FindSnapshot(ctx, be, loader, snapshotID) return FindSnapshot(ctx, be, loader, snapshotID)
} }
@ -139,8 +149,11 @@ func (f *SnapshotFilter) FindAll(ctx context.Context, be Lister, loader LoaderUn
ids.Insert(*sn.ID()) ids.Insert(*sn.ID())
} }
} else { } else {
sn, err = FindSnapshot(ctx, be, loader, s) var subpath string
if err == nil { sn, subpath, err = FindSnapshot(ctx, be, loader, s)
if err == nil && subpath != "" {
err = errors.New("snapshot:path syntax not allowed")
} else if err == nil {
if ids.Has(*sn.ID()) { if ids.Has(*sn.ID()) {
continue continue
} }

View File

@ -15,7 +15,7 @@ func TestFindLatestSnapshot(t *testing.T) {
latestSnapshot := restic.TestCreateSnapshot(t, repo, parseTimeUTC("2019-09-09 09:09:09"), 1, 0) latestSnapshot := restic.TestCreateSnapshot(t, repo, parseTimeUTC("2019-09-09 09:09:09"), 1, 0)
f := restic.SnapshotFilter{Hosts: []string{"foo"}} f := restic.SnapshotFilter{Hosts: []string{"foo"}}
sn, err := f.FindLatest(context.TODO(), repo.Backend(), repo, "latest") sn, _, err := f.FindLatest(context.TODO(), repo.Backend(), repo, "latest")
if err != nil { if err != nil {
t.Fatalf("FindLatest returned error: %v", err) t.Fatalf("FindLatest returned error: %v", err)
} }
@ -31,7 +31,7 @@ func TestFindLatestSnapshotWithMaxTimestamp(t *testing.T) {
desiredSnapshot := restic.TestCreateSnapshot(t, repo, parseTimeUTC("2017-07-07 07:07:07"), 1, 0) desiredSnapshot := restic.TestCreateSnapshot(t, repo, parseTimeUTC("2017-07-07 07:07:07"), 1, 0)
restic.TestCreateSnapshot(t, repo, parseTimeUTC("2019-09-09 09:09:09"), 1, 0) restic.TestCreateSnapshot(t, repo, parseTimeUTC("2019-09-09 09:09:09"), 1, 0)
sn, err := (&restic.SnapshotFilter{ sn, _, err := (&restic.SnapshotFilter{
Hosts: []string{"foo"}, Hosts: []string{"foo"},
TimestampLimit: parseTimeUTC("2018-08-08 08:08:08"), TimestampLimit: parseTimeUTC("2018-08-08 08:08:08"),
}).FindLatest(context.TODO(), repo.Backend(), repo, "latest") }).FindLatest(context.TODO(), repo.Backend(), repo, "latest")

View File

@ -5,7 +5,9 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"path"
"sort" "sort"
"strings"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
@ -184,3 +186,32 @@ func (builder *TreeJSONBuilder) Finalize() ([]byte, error) {
builder.buf = bytes.Buffer{} builder.buf = bytes.Buffer{}
return buf, nil return buf, nil
} }
func FindTreeDirectory(ctx context.Context, repo BlobLoader, id *ID, dir string) (*ID, error) {
if id == nil {
return nil, errors.New("tree id is null")
}
dirs := strings.Split(path.Clean(dir), "/")
subpath := ""
for _, name := range dirs {
if name == "" || name == "." {
continue
}
subpath = path.Join(subpath, name)
tree, err := LoadTree(ctx, repo, *id)
if err != nil {
return nil, fmt.Errorf("path %s: %w", subpath, err)
}
node := tree.Find(name)
if node == nil {
return nil, fmt.Errorf("path %s: not found", subpath)
}
if node.Type != "dir" || node.Subtree == nil {
return nil, fmt.Errorf("path %s: not a directory", subpath)
}
id = node.Subtree
}
return id, nil
}