mirror of
https://github.com/octoleo/restic.git
synced 2024-11-22 04:45:15 +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:
parent
60d8066568
commit
85860e6e97
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user