From 87d084e18c831d5a68d06acbb8d9f4ea7fe96afd Mon Sep 17 00:00:00 2001 From: Fabian Wickborn Date: Mon, 9 Oct 2017 22:09:41 +0200 Subject: [PATCH] Add subcommand dump --- cmd/restic/cmd_catfile.go | 181 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 cmd/restic/cmd_catfile.go diff --git a/cmd/restic/cmd_catfile.go b/cmd/restic/cmd_catfile.go new file mode 100644 index 000000000..57d3b129e --- /dev/null +++ b/cmd/restic/cmd_catfile.go @@ -0,0 +1,181 @@ +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/restic/restic/internal/debug" + "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/restic" + + "github.com/spf13/cobra" +) + +var cmdCatFile = &cobra.Command{ + Use: "catfile [flags] file snapshotID", + Short: "Print a backed-up file to stdout", + Long: ` +The "catfile" command extracts a single file from a snapshot from the repository and +prints its contents to stdout. + +The special snapshot "latest" can be used to use the latest snapshot in the +repository. +`, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + return runCatFile(catFileOptions, globalOptions, args) + }, +} + +// CatFileOptions collects all options for the catfile command. +type CatFileOptions struct { + Host string + Paths []string + Tags restic.TagLists +} + +var catFileOptions CatFileOptions + +func init() { + cmdRoot.AddCommand(cmdCatFile) + + flags := cmdCatFile.Flags() + flags.StringVarP(&catFileOptions.Host, "host", "H", "", `only consider snapshots for this host when the snapshot ID is "latest"`) + flags.Var(&catFileOptions.Tags, "tag", "only consider snapshots which include this `taglist` for snapshot ID \"latest\"") + flags.StringArrayVar(&catFileOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"") +} + +func splitPath(path string) []string { + d, f := filepath.Split(path) + if d == "" { + return []string{f} + } + s := splitPath(filepath.Clean(d)) + return append(s, f) +} + +func catNode(ctx context.Context, repo restic.Repository, node *restic.Node) error { + var buf []byte + for _, id := range node.Content { + size, err := repo.LookupBlobSize(id, restic.DataBlob) + if err != nil { + return err + } + + buf = buf[:cap(buf)] + if len(buf) < restic.CiphertextLength(int(size)) { + buf = restic.NewBlobBuffer(int(size)) + } + + n, err := repo.LoadBlob(ctx, restic.DataBlob, id, buf) + if err != nil { + return err + } + buf = buf[:n] + + _, err = os.Stdout.Write(buf) + if err != nil { + return errors.Wrap(err, "Write") + } + } + return nil +} + +func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.Repository, prefix string, pathComponents []string) error { + if tree == nil { + return fmt.Errorf("called with a nil tree") + } + if repo == nil { + return fmt.Errorf("called with a nil repository") + } + l := len(pathComponents) + if l == 0 { + return fmt.Errorf("empty path components") + } + item := filepath.Join(prefix, pathComponents[0]) + for _, node := range tree.Nodes { + if node.Name == pathComponents[0] { + switch { + case l == 1 && node.Type == "file": + return catNode(ctx, repo, node) + case l > 1 && node.Type == "dir": + subtree, err := repo.LoadTree(ctx, *node.Subtree) + if err != nil { + return errors.Wrapf(err, "cannot load subtree for %q", item) + } + return printFromTree(ctx, subtree, repo, item, pathComponents[1:]) + case l > 1: + return fmt.Errorf("%q should be a dir, but s a %q", item, node.Type) + case node.Type != "file": + return fmt.Errorf("%q should be a file, but is a %q", item, node.Type) + } + } + } + return fmt.Errorf("path %q not found in snapshot", item) +} + +func runCatFile(opts CatFileOptions, gopts GlobalOptions, args []string) error { + ctx := gopts.ctx + + if len(args) != 2 { + return errors.Fatal("no file and no snapshot ID specified") + } + + pathToPrint := args[0] + snapshotIDString := args[1] + + debug.Log("cat file %q from %q", pathToPrint, snapshotIDString) + + splittedPath := splitPath(pathToPrint) + + repo, err := OpenRepository(gopts) + if err != nil { + return err + } + + if !gopts.NoLock { + lock, err := lockRepo(repo) + defer unlockRepo(lock) + if err != nil { + return err + } + } + + err = repo.LoadIndex(ctx) + if err != nil { + return err + } + + var id restic.ID + + if snapshotIDString == "latest" { + id, err = restic.FindLatestSnapshot(ctx, repo, opts.Paths, opts.Tags, opts.Host) + if err != nil { + Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, opts.Paths, opts.Host) + } + } else { + id, err = restic.FindSnapshot(repo, snapshotIDString) + if err != nil { + Exitf(1, "invalid id %q: %v", snapshotIDString, err) + } + } + + sn, err := restic.LoadSnapshot(context.TODO(), repo, id) + if err != nil { + Exitf(2, "loading snapshot %q failed: %v", snapshotIDString, err) + } + + tree, err := repo.LoadTree(ctx, *sn.Tree) + if err != nil { + Exitf(2, "loading tree for snapshot %q failed: %v", snapshotIDString, err) + } + + err = printFromTree(ctx, tree, repo, "", splittedPath) + if err != nil { + Exitf(2, "cannot cat file: %v", err) + } + + return nil +}