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 }