github.com/mckael/restic@v0.8.3/cmd/restic/cmd_dump.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  
     9  	"github.com/restic/restic/internal/debug"
    10  	"github.com/restic/restic/internal/errors"
    11  	"github.com/restic/restic/internal/restic"
    12  
    13  	"github.com/spf13/cobra"
    14  )
    15  
    16  var cmdDump = &cobra.Command{
    17  	Use:   "dump [flags] snapshotID file",
    18  	Short: "Print a backed-up file to stdout",
    19  	Long: `
    20  The "dump" command extracts a single file from a snapshot from the repository and
    21  prints its contents to stdout.
    22  
    23  The special snapshot "latest" can be used to use the latest snapshot in the
    24  repository.
    25  `,
    26  	DisableAutoGenTag: true,
    27  	RunE: func(cmd *cobra.Command, args []string) error {
    28  		return runDump(dumpOptions, globalOptions, args)
    29  	},
    30  }
    31  
    32  // DumpOptions collects all options for the dump command.
    33  type DumpOptions struct {
    34  	Host  string
    35  	Paths []string
    36  	Tags  restic.TagLists
    37  }
    38  
    39  var dumpOptions DumpOptions
    40  
    41  func init() {
    42  	cmdRoot.AddCommand(cmdDump)
    43  
    44  	flags := cmdDump.Flags()
    45  	flags.StringVarP(&dumpOptions.Host, "host", "H", "", `only consider snapshots for this host when the snapshot ID is "latest"`)
    46  	flags.Var(&dumpOptions.Tags, "tag", "only consider snapshots which include this `taglist` for snapshot ID \"latest\"")
    47  	flags.StringArrayVar(&dumpOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"")
    48  }
    49  
    50  func splitPath(path string) []string {
    51  	d, f := filepath.Split(path)
    52  	if d == "" || d == "/" {
    53  		return []string{f}
    54  	}
    55  	s := splitPath(filepath.Clean(d))
    56  	return append(s, f)
    57  }
    58  
    59  func dumpNode(ctx context.Context, repo restic.Repository, node *restic.Node) error {
    60  	var buf []byte
    61  	for _, id := range node.Content {
    62  		size, found := repo.LookupBlobSize(id, restic.DataBlob)
    63  		if !found {
    64  			return errors.Errorf("id %v not found in repository", id)
    65  		}
    66  
    67  		buf = buf[:cap(buf)]
    68  		if len(buf) < restic.CiphertextLength(int(size)) {
    69  			buf = restic.NewBlobBuffer(int(size))
    70  		}
    71  
    72  		n, err := repo.LoadBlob(ctx, restic.DataBlob, id, buf)
    73  		if err != nil {
    74  			return err
    75  		}
    76  		buf = buf[:n]
    77  
    78  		_, err = os.Stdout.Write(buf)
    79  		if err != nil {
    80  			return errors.Wrap(err, "Write")
    81  		}
    82  	}
    83  	return nil
    84  }
    85  
    86  func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.Repository, prefix string, pathComponents []string) error {
    87  	if tree == nil {
    88  		return fmt.Errorf("called with a nil tree")
    89  	}
    90  	if repo == nil {
    91  		return fmt.Errorf("called with a nil repository")
    92  	}
    93  	l := len(pathComponents)
    94  	if l == 0 {
    95  		return fmt.Errorf("empty path components")
    96  	}
    97  	item := filepath.Join(prefix, pathComponents[0])
    98  	for _, node := range tree.Nodes {
    99  		if node.Name == pathComponents[0] {
   100  			switch {
   101  			case l == 1 && node.Type == "file":
   102  				return dumpNode(ctx, repo, node)
   103  			case l > 1 && node.Type == "dir":
   104  				subtree, err := repo.LoadTree(ctx, *node.Subtree)
   105  				if err != nil {
   106  					return errors.Wrapf(err, "cannot load subtree for %q", item)
   107  				}
   108  				return printFromTree(ctx, subtree, repo, item, pathComponents[1:])
   109  			case l > 1:
   110  				return fmt.Errorf("%q should be a dir, but s a %q", item, node.Type)
   111  			case node.Type != "file":
   112  				return fmt.Errorf("%q should be a file, but is a %q", item, node.Type)
   113  			}
   114  		}
   115  	}
   116  	return fmt.Errorf("path %q not found in snapshot", item)
   117  }
   118  
   119  func runDump(opts DumpOptions, gopts GlobalOptions, args []string) error {
   120  	ctx := gopts.ctx
   121  
   122  	if len(args) != 2 {
   123  		return errors.Fatal("no file and no snapshot ID specified")
   124  	}
   125  
   126  	snapshotIDString := args[0]
   127  	pathToPrint := args[1]
   128  
   129  	debug.Log("dump file %q from %q", pathToPrint, snapshotIDString)
   130  
   131  	splittedPath := splitPath(pathToPrint)
   132  
   133  	repo, err := OpenRepository(gopts)
   134  	if err != nil {
   135  		return err
   136  	}
   137  
   138  	if !gopts.NoLock {
   139  		lock, err := lockRepo(repo)
   140  		defer unlockRepo(lock)
   141  		if err != nil {
   142  			return err
   143  		}
   144  	}
   145  
   146  	err = repo.LoadIndex(ctx)
   147  	if err != nil {
   148  		return err
   149  	}
   150  
   151  	var id restic.ID
   152  
   153  	if snapshotIDString == "latest" {
   154  		id, err = restic.FindLatestSnapshot(ctx, repo, opts.Paths, opts.Tags, opts.Host)
   155  		if err != nil {
   156  			Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, opts.Paths, opts.Host)
   157  		}
   158  	} else {
   159  		id, err = restic.FindSnapshot(repo, snapshotIDString)
   160  		if err != nil {
   161  			Exitf(1, "invalid id %q: %v", snapshotIDString, err)
   162  		}
   163  	}
   164  
   165  	sn, err := restic.LoadSnapshot(gopts.ctx, repo, id)
   166  	if err != nil {
   167  		Exitf(2, "loading snapshot %q failed: %v", snapshotIDString, err)
   168  	}
   169  
   170  	tree, err := repo.LoadTree(ctx, *sn.Tree)
   171  	if err != nil {
   172  		Exitf(2, "loading tree for snapshot %q failed: %v", snapshotIDString, err)
   173  	}
   174  
   175  	err = printFromTree(ctx, tree, repo, "", splittedPath)
   176  	if err != nil {
   177  		Exitf(2, "cannot dump file: %v", err)
   178  	}
   179  
   180  	return nil
   181  }