From 87d084e18c831d5a68d06acbb8d9f4ea7fe96afd Mon Sep 17 00:00:00 2001 From: Fabian Wickborn Date: Mon, 9 Oct 2017 22:09:41 +0200 Subject: [PATCH 1/4] 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 +} From 814e992c0b03e6243cee8e23461e76a310b4ac06 Mon Sep 17 00:00:00 2001 From: Fabian Wickborn Date: Sat, 14 Oct 2017 11:34:04 +0200 Subject: [PATCH 2/4] Rename subcommand catfile to dump --- cmd/restic/cmd_catfile.go | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/cmd/restic/cmd_catfile.go b/cmd/restic/cmd_catfile.go index 57d3b129e..566819095 100644 --- a/cmd/restic/cmd_catfile.go +++ b/cmd/restic/cmd_catfile.go @@ -13,11 +13,11 @@ import ( "github.com/spf13/cobra" ) -var cmdCatFile = &cobra.Command{ - Use: "catfile [flags] file snapshotID", +var cmdDump = &cobra.Command{ + Use: "dump [flags] snapshotID file", Short: "Print a backed-up file to stdout", Long: ` -The "catfile" command extracts a single file from a snapshot from the repository and +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 @@ -25,26 +25,26 @@ repository. `, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runCatFile(catFileOptions, globalOptions, args) + return runDump(dumpOptions, globalOptions, args) }, } -// CatFileOptions collects all options for the catfile command. -type CatFileOptions struct { +// DumpOptions collects all options for the dump command. +type DumpOptions struct { Host string Paths []string Tags restic.TagLists } -var catFileOptions CatFileOptions +var dumpOptions DumpOptions func init() { - cmdRoot.AddCommand(cmdCatFile) + cmdRoot.AddCommand(cmdDump) - 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\"") + 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 { @@ -56,7 +56,7 @@ func splitPath(path string) []string { return append(s, f) } -func catNode(ctx context.Context, repo restic.Repository, node *restic.Node) error { +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) @@ -99,7 +99,7 @@ func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.Repositor if node.Name == pathComponents[0] { switch { case l == 1 && node.Type == "file": - return catNode(ctx, repo, node) + return dumpNode(ctx, repo, node) case l > 1 && node.Type == "dir": subtree, err := repo.LoadTree(ctx, *node.Subtree) if err != nil { @@ -116,17 +116,17 @@ func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.Repositor return fmt.Errorf("path %q not found in snapshot", item) } -func runCatFile(opts CatFileOptions, gopts GlobalOptions, args []string) error { +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") } - pathToPrint := args[0] - snapshotIDString := args[1] + snapshotIDString := args[0] + pathToPrint := args[1] - debug.Log("cat file %q from %q", pathToPrint, snapshotIDString) + debug.Log("dump file %q from %q", pathToPrint, snapshotIDString) splittedPath := splitPath(pathToPrint) @@ -174,7 +174,7 @@ func runCatFile(opts CatFileOptions, gopts GlobalOptions, args []string) error { err = printFromTree(ctx, tree, repo, "", splittedPath) if err != nil { - Exitf(2, "cannot cat file: %v", err) + Exitf(2, "cannot dump file: %v", err) } return nil From cd5cbe0910afa7dbe7aac7c185ff68928d1ee9c5 Mon Sep 17 00:00:00 2001 From: Fabian Wickborn Date: Sat, 14 Oct 2017 13:55:00 +0200 Subject: [PATCH 3/4] Rename debug dump related variable and run function --- cmd/restic/cmd_debug.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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") } From d1d9c3f9d7f9bf25a96642e75a80a55ca1673e5b Mon Sep 17 00:00:00 2001 From: Fabian Wickborn Date: Sat, 14 Oct 2017 13:55:21 +0200 Subject: [PATCH 4/4] Renamed cmd_catfile.go to cmd_dump.go --- cmd/restic/{cmd_catfile.go => cmd_dump.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cmd/restic/{cmd_catfile.go => cmd_dump.go} (100%) diff --git a/cmd/restic/cmd_catfile.go b/cmd/restic/cmd_dump.go similarity index 100% rename from cmd/restic/cmd_catfile.go rename to cmd/restic/cmd_dump.go