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:
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()}
|
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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user