From 85860e6e97a5e6a5b0f6a99397868955975021e7 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Thu, 18 May 2023 23:15:38 +0200 Subject: [PATCH] Add support for snapshot:subpath syntax This snapshot specification syntax is supported by the cat, diff, dump, ls and restore command. --- cmd/restic/cmd_backup.go | 2 +- cmd/restic/cmd_cat.go | 2 +- cmd/restic/cmd_diff.go | 22 +++++++++++++------ cmd/restic/cmd_dump.go | 7 +++++- cmd/restic/cmd_ls.go | 7 +++++- cmd/restic/cmd_restore.go | 7 +++++- internal/restic/snapshot_find.go | 29 ++++++++++++++++++------- internal/restic/snapshot_find_test.go | 4 ++-- internal/restic/tree.go | 31 +++++++++++++++++++++++++++ 9 files changed, 90 insertions(+), 21 deletions(-) diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index d7e899eaf..bc1b99908 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -453,7 +453,7 @@ func findParentSnapshot(ctx context.Context, repo restic.Repository, opts Backup 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 if opts.Parent == "" && errors.Is(err, restic.ErrNoSnapshotFound) { err = nil diff --git a/cmd/restic/cmd_cat.go b/cmd/restic/cmd_cat.go index 771731a58..ee34c813a 100644 --- a/cmd/restic/cmd_cat.go +++ b/cmd/restic/cmd_cat.go @@ -80,7 +80,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error { Println(string(buf)) return nil 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 { return errors.Fatalf("could not find snapshot: %v\n", err) } diff --git a/cmd/restic/cmd_diff.go b/cmd/restic/cmd_diff.go index 3c59b9580..a65b502fb 100644 --- a/cmd/restic/cmd_diff.go +++ b/cmd/restic/cmd_diff.go @@ -54,12 +54,12 @@ func init() { 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) { - sn, err := restic.FindSnapshot(ctx, be, repo, desc) +func loadSnapshot(ctx context.Context, be restic.Lister, repo restic.Repository, desc string) (*restic.Snapshot, string, error) { + sn, subpath, err := restic.FindSnapshot(ctx, be, repo, desc) 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. @@ -346,12 +346,12 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args [] if err != nil { return err } - sn1, err := loadSnapshot(ctx, be, repo, args[0]) + sn1, subpath1, err := loadSnapshot(ctx, be, repo, args[0]) if err != nil { return err } - sn2, err := loadSnapshot(ctx, be, repo, args[1]) + sn2, subpath2, err := loadSnapshot(ctx, be, repo, args[1]) if err != nil { 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()) } + 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{ repo: repo, opts: diffOptions, diff --git a/cmd/restic/cmd_dump.go b/cmd/restic/cmd_dump.go index 34313f582..5874c6a00 100644 --- a/cmd/restic/cmd_dump.go +++ b/cmd/restic/cmd_dump.go @@ -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, Paths: opts.Paths, Tags: opts.Tags, @@ -153,6 +153,11 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args [] 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) if err != nil { return errors.Fatalf("loading tree for snapshot %q failed: %v", snapshotIDString, err) diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index 1cd549e7c..2485d3fc9 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -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, Paths: opts.Paths, Tags: opts.Tags, @@ -221,6 +221,11 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri return err } + sn.Tree, err = restic.FindTreeDirectory(ctx, repo, sn.Tree, subpath) + if err != nil { + return err + } + printSnapshot(sn) err = walker.Walk(ctx, repo, *sn.Tree, nil, func(_ restic.ID, nodepath string, node *restic.Node, err error) (bool, error) { diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index c59ac34de..00c5b62b1 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -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, Paths: opts.Paths, Tags: opts.Tags, @@ -175,6 +175,11 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, return err } + sn.Tree, err = restic.FindTreeDirectory(ctx, repo, sn.Tree, subpath) + if err != nil { + return err + } + msg := ui.NewMessage(term, gopts.verbosity) var printer restoreui.ProgressPrinter if gopts.JSON { diff --git a/internal/restic/snapshot_find.go b/internal/restic/snapshot_find.go index b577b0919..9b0aa8d82 100644 --- a/internal/restic/snapshot_find.go +++ b/internal/restic/snapshot_find.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "path/filepath" + "strings" "time" "github.com/restic/restic/internal/errors" @@ -82,31 +83,40 @@ func (f *SnapshotFilter) findLatest(ctx context.Context, be Lister, loader Loade 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 // 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 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 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 // or a snapshot specified by `snapshotID`. -func (f *SnapshotFilter) FindLatest(ctx context.Context, be Lister, loader LoaderUnpacked, snapshotID string) (*Snapshot, error) { - if snapshotID == "latest" { +func (f *SnapshotFilter) FindLatest(ctx context.Context, be Lister, loader LoaderUnpacked, snapshotID string) (*Snapshot, string, error) { + id, subpath := splitSnapshotID(snapshotID) + if id == "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 sn, subpath, err } 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()) } } else { - sn, err = FindSnapshot(ctx, be, loader, s) - if err == nil { + var subpath string + 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()) { continue } diff --git a/internal/restic/snapshot_find_test.go b/internal/restic/snapshot_find_test.go index d098b5224..d330a5b01 100644 --- a/internal/restic/snapshot_find_test.go +++ b/internal/restic/snapshot_find_test.go @@ -15,7 +15,7 @@ func TestFindLatestSnapshot(t *testing.T) { latestSnapshot := restic.TestCreateSnapshot(t, repo, parseTimeUTC("2019-09-09 09:09:09"), 1, 0) 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 { 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) restic.TestCreateSnapshot(t, repo, parseTimeUTC("2019-09-09 09:09:09"), 1, 0) - sn, err := (&restic.SnapshotFilter{ + sn, _, err := (&restic.SnapshotFilter{ Hosts: []string{"foo"}, TimestampLimit: parseTimeUTC("2018-08-08 08:08:08"), }).FindLatest(context.TODO(), repo.Backend(), repo, "latest") diff --git a/internal/restic/tree.go b/internal/restic/tree.go index 373b36746..a839212d4 100644 --- a/internal/restic/tree.go +++ b/internal/restic/tree.go @@ -5,7 +5,9 @@ import ( "context" "encoding/json" "fmt" + "path" "sort" + "strings" "github.com/restic/restic/internal/errors" @@ -184,3 +186,32 @@ func (builder *TreeJSONBuilder) Finalize() ([]byte, error) { builder.buf = bytes.Buffer{} 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 +}