diff --git a/cmd/restic/cmd_debug.go b/cmd/restic/cmd_debug.go index ce233c613..6a06e96ed 100644 --- a/cmd/restic/cmd_debug.go +++ b/cmd/restic/cmd_debug.go @@ -24,7 +24,7 @@ var cmdDebug = &cobra.Command{ Short: "Debug commands", } -var cmdDump = &cobra.Command{ +var cmdDebugDump = &cobra.Command{ Use: "dump [indexes|snapshots|all|packs]", Short: "Dump data structures", Long: ` @@ -32,13 +32,13 @@ The "dump" command dumps data structures from the repository as JSON objects. It is used for debugging purposes only.`, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runDump(globalOptions, args) + return runDebugDump(globalOptions, args) }, } func init() { cmdRoot.AddCommand(cmdDebug) - cmdDebug.AddCommand(cmdDump) + cmdDebug.AddCommand(cmdDebugDump) } func prettyPrintJSON(wr io.Writer, item interface{}) error { @@ -165,7 +165,7 @@ func dumpIndexes(repo restic.Repository) error { return nil } -func runDump(gopts GlobalOptions, args []string) error { +func runDebugDump(gopts GlobalOptions, args []string) error { if len(args) != 1 { return errors.Fatal("type not specified") } diff --git a/cmd/restic/cmd_dump.go b/cmd/restic/cmd_dump.go new file mode 100644 index 000000000..566819095 --- /dev/null +++ b/cmd/restic/cmd_dump.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 cmdDump = &cobra.Command{ + Use: "dump [flags] snapshotID file", + Short: "Print a backed-up file to stdout", + Long: ` +The "dump" 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 runDump(dumpOptions, globalOptions, args) + }, +} + +// DumpOptions collects all options for the dump command. +type DumpOptions struct { + Host string + Paths []string + Tags restic.TagLists +} + +var dumpOptions DumpOptions + +func init() { + cmdRoot.AddCommand(cmdDump) + + flags := cmdDump.Flags() + flags.StringVarP(&dumpOptions.Host, "host", "H", "", `only consider snapshots for this host when the snapshot ID is "latest"`) + flags.Var(&dumpOptions.Tags, "tag", "only consider snapshots which include this `taglist` for snapshot ID \"latest\"") + flags.StringArrayVar(&dumpOptions.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 dumpNode(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 dumpNode(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 runDump(opts DumpOptions, gopts GlobalOptions, args []string) error { + ctx := gopts.ctx + + if len(args) != 2 { + return errors.Fatal("no file and no snapshot ID specified") + } + + snapshotIDString := args[0] + pathToPrint := args[1] + + debug.Log("dump 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 dump file: %v", err) + } + + return nil +}