github.com/fawick/restic@v0.1.1-0.20171126184616-c02923fbfc79/cmd/restic/cmd_backup.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/spf13/cobra"
    14  
    15  	"github.com/restic/restic/internal/archiver"
    16  	"github.com/restic/restic/internal/debug"
    17  	"github.com/restic/restic/internal/errors"
    18  	"github.com/restic/restic/internal/fs"
    19  	"github.com/restic/restic/internal/restic"
    20  )
    21  
    22  var cmdBackup = &cobra.Command{
    23  	Use:   "backup [flags] FILE/DIR [FILE/DIR] ...",
    24  	Short: "Create a new backup of files and/or directories",
    25  	Long: `
    26  The "backup" command creates a new snapshot and saves the files and directories
    27  given as the arguments.
    28  `,
    29  	PreRun: func(cmd *cobra.Command, args []string) {
    30  		if backupOptions.Hostname == "" {
    31  			hostname, err := os.Hostname()
    32  			if err != nil {
    33  				debug.Log("os.Hostname() returned err: %v", err)
    34  				return
    35  			}
    36  			backupOptions.Hostname = hostname
    37  		}
    38  	},
    39  	DisableAutoGenTag: true,
    40  	RunE: func(cmd *cobra.Command, args []string) error {
    41  		if backupOptions.Stdin && backupOptions.FilesFrom == "-" {
    42  			return errors.Fatal("cannot use both `--stdin` and `--files-from -`")
    43  		}
    44  
    45  		if backupOptions.Stdin {
    46  			return readBackupFromStdin(backupOptions, globalOptions, args)
    47  		}
    48  
    49  		return runBackup(backupOptions, globalOptions, args)
    50  	},
    51  }
    52  
    53  // BackupOptions bundles all options for the backup command.
    54  type BackupOptions struct {
    55  	Parent           string
    56  	Force            bool
    57  	Excludes         []string
    58  	ExcludeFiles     []string
    59  	ExcludeOtherFS   bool
    60  	ExcludeIfPresent []string
    61  	ExcludeCaches    bool
    62  	Stdin            bool
    63  	StdinFilename    string
    64  	Tags             []string
    65  	Hostname         string
    66  	FilesFrom        string
    67  	TimeStamp        string
    68  }
    69  
    70  var backupOptions BackupOptions
    71  
    72  func init() {
    73  	cmdRoot.AddCommand(cmdBackup)
    74  
    75  	f := cmdBackup.Flags()
    76  	f.StringVar(&backupOptions.Parent, "parent", "", "use this parent snapshot (default: last snapshot in the repo that has the same target files/directories)")
    77  	f.BoolVarP(&backupOptions.Force, "force", "f", false, `force re-reading the target files/directories (overrides the "parent" flag)`)
    78  	f.StringArrayVarP(&backupOptions.Excludes, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)")
    79  	f.StringArrayVar(&backupOptions.ExcludeFiles, "exclude-file", nil, "read exclude patterns from a `file` (can be specified multiple times)")
    80  	f.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems")
    81  	f.StringArrayVar(&backupOptions.ExcludeIfPresent, "exclude-if-present", nil, "takes filename[:header], exclude contents of directories containing filename (except filename itself) if header of that file is as provided (can be specified multiple times)")
    82  	f.BoolVar(&backupOptions.ExcludeCaches, "exclude-caches", false, `excludes cache directories that are marked with a CACHEDIR.TAG file`)
    83  	f.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin")
    84  	f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "file name to use when reading from stdin")
    85  	f.StringArrayVar(&backupOptions.Tags, "tag", nil, "add a `tag` for the new snapshot (can be specified multiple times)")
    86  	f.StringVar(&backupOptions.Hostname, "hostname", "", "set the `hostname` for the snapshot manually. To prevent an expensive rescan use the \"parent\" flag")
    87  	f.StringVar(&backupOptions.FilesFrom, "files-from", "", "read the files to backup from file (can be combined with file args)")
    88  	f.StringVar(&backupOptions.TimeStamp, "time", "", "time of the backup (ex. '2012-11-01 22:08:41') (default: now)")
    89  }
    90  
    91  func newScanProgress(gopts GlobalOptions) *restic.Progress {
    92  	if gopts.Quiet {
    93  		return nil
    94  	}
    95  
    96  	p := restic.NewProgress()
    97  	p.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) {
    98  		if IsProcessBackground() {
    99  			return
   100  		}
   101  
   102  		PrintProgress("[%s] %d directories, %d files, %s", formatDuration(d), s.Dirs, s.Files, formatBytes(s.Bytes))
   103  	}
   104  
   105  	p.OnDone = func(s restic.Stat, d time.Duration, ticker bool) {
   106  		PrintProgress("scanned %d directories, %d files in %s\n", s.Dirs, s.Files, formatDuration(d))
   107  	}
   108  
   109  	return p
   110  }
   111  
   112  func newArchiveProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress {
   113  	if gopts.Quiet {
   114  		return nil
   115  	}
   116  
   117  	archiveProgress := restic.NewProgress()
   118  
   119  	var bps, eta uint64
   120  	itemsTodo := todo.Files + todo.Dirs
   121  
   122  	archiveProgress.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) {
   123  		if IsProcessBackground() {
   124  			return
   125  		}
   126  
   127  		sec := uint64(d / time.Second)
   128  		if todo.Bytes > 0 && sec > 0 && ticker {
   129  			bps = s.Bytes / sec
   130  			if s.Bytes >= todo.Bytes {
   131  				eta = 0
   132  			} else if bps > 0 {
   133  				eta = (todo.Bytes - s.Bytes) / bps
   134  			}
   135  		}
   136  
   137  		itemsDone := s.Files + s.Dirs
   138  
   139  		status1 := fmt.Sprintf("[%s] %s  %s/s  %s / %s  %d / %d items  %d errors  ",
   140  			formatDuration(d),
   141  			formatPercent(s.Bytes, todo.Bytes),
   142  			formatBytes(bps),
   143  			formatBytes(s.Bytes), formatBytes(todo.Bytes),
   144  			itemsDone, itemsTodo,
   145  			s.Errors)
   146  		status2 := fmt.Sprintf("ETA %s ", formatSeconds(eta))
   147  
   148  		if w := stdoutTerminalWidth(); w > 0 {
   149  			maxlen := w - len(status2) - 1
   150  
   151  			if maxlen < 4 {
   152  				status1 = ""
   153  			} else if len(status1) > maxlen {
   154  				status1 = status1[:maxlen-4]
   155  				status1 += "... "
   156  			}
   157  		}
   158  
   159  		PrintProgress("%s%s", status1, status2)
   160  	}
   161  
   162  	archiveProgress.OnDone = func(s restic.Stat, d time.Duration, ticker bool) {
   163  		fmt.Printf("\nduration: %s, %s\n", formatDuration(d), formatRate(todo.Bytes, d))
   164  	}
   165  
   166  	return archiveProgress
   167  }
   168  
   169  func newArchiveStdinProgress(gopts GlobalOptions) *restic.Progress {
   170  	if gopts.Quiet {
   171  		return nil
   172  	}
   173  
   174  	archiveProgress := restic.NewProgress()
   175  
   176  	var bps uint64
   177  
   178  	archiveProgress.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) {
   179  		if IsProcessBackground() {
   180  			return
   181  		}
   182  
   183  		sec := uint64(d / time.Second)
   184  		if s.Bytes > 0 && sec > 0 && ticker {
   185  			bps = s.Bytes / sec
   186  		}
   187  
   188  		status1 := fmt.Sprintf("[%s] %s  %s/s", formatDuration(d),
   189  			formatBytes(s.Bytes),
   190  			formatBytes(bps))
   191  
   192  		if w := stdoutTerminalWidth(); w > 0 {
   193  			maxlen := w - len(status1)
   194  
   195  			if maxlen < 4 {
   196  				status1 = ""
   197  			} else if len(status1) > maxlen {
   198  				status1 = status1[:maxlen-4]
   199  				status1 += "... "
   200  			}
   201  		}
   202  
   203  		PrintProgress("%s", status1)
   204  	}
   205  
   206  	archiveProgress.OnDone = func(s restic.Stat, d time.Duration, ticker bool) {
   207  		fmt.Printf("\nduration: %s, %s\n", formatDuration(d), formatRate(s.Bytes, d))
   208  	}
   209  
   210  	return archiveProgress
   211  }
   212  
   213  // filterExisting returns a slice of all existing items, or an error if no
   214  // items exist at all.
   215  func filterExisting(items []string) (result []string, err error) {
   216  	for _, item := range items {
   217  		_, err := fs.Lstat(item)
   218  		if err != nil && os.IsNotExist(errors.Cause(err)) {
   219  			Warnf("%v does not exist, skipping\n", item)
   220  			continue
   221  		}
   222  
   223  		result = append(result, item)
   224  	}
   225  
   226  	if len(result) == 0 {
   227  		return nil, errors.Fatal("all target directories/files do not exist")
   228  	}
   229  
   230  	return
   231  }
   232  
   233  func readBackupFromStdin(opts BackupOptions, gopts GlobalOptions, args []string) error {
   234  	if len(args) != 0 {
   235  		return errors.Fatal("when reading from stdin, no additional files can be specified")
   236  	}
   237  
   238  	if opts.StdinFilename == "" {
   239  		return errors.Fatal("filename for backup from stdin must not be empty")
   240  	}
   241  
   242  	if gopts.password == "" {
   243  		return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD")
   244  	}
   245  
   246  	repo, err := OpenRepository(gopts)
   247  	if err != nil {
   248  		return err
   249  	}
   250  
   251  	lock, err := lockRepo(repo)
   252  	defer unlockRepo(lock)
   253  	if err != nil {
   254  		return err
   255  	}
   256  
   257  	err = repo.LoadIndex(context.TODO())
   258  	if err != nil {
   259  		return err
   260  	}
   261  
   262  	r := &archiver.Reader{
   263  		Repository: repo,
   264  		Tags:       opts.Tags,
   265  		Hostname:   opts.Hostname,
   266  	}
   267  
   268  	_, id, err := r.Archive(context.TODO(), opts.StdinFilename, os.Stdin, newArchiveStdinProgress(gopts))
   269  	if err != nil {
   270  		return err
   271  	}
   272  
   273  	Verbosef("archived as %v\n", id.Str())
   274  	return nil
   275  }
   276  
   277  // readFromFile will read all lines from the given filename and write them to a
   278  // string array, if filename is empty readFromFile returns and empty string
   279  // array. If filename is a dash (-), readFromFile will read the lines from
   280  // the standard input.
   281  func readLinesFromFile(filename string) ([]string, error) {
   282  	if filename == "" {
   283  		return nil, nil
   284  	}
   285  
   286  	var r io.Reader = os.Stdin
   287  	if filename != "-" {
   288  		f, err := os.Open(filename)
   289  		if err != nil {
   290  			return nil, err
   291  		}
   292  		defer f.Close()
   293  		r = f
   294  	}
   295  
   296  	var lines []string
   297  
   298  	scanner := bufio.NewScanner(r)
   299  	for scanner.Scan() {
   300  		line := scanner.Text()
   301  		// ignore empty lines
   302  		if line == "" {
   303  			continue
   304  		}
   305  		// strip comments
   306  		if strings.HasPrefix(line, "#") {
   307  			continue
   308  		}
   309  		lines = append(lines, line)
   310  	}
   311  
   312  	if err := scanner.Err(); err != nil {
   313  		return nil, err
   314  	}
   315  
   316  	return lines, nil
   317  }
   318  
   319  func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error {
   320  	if opts.FilesFrom == "-" && gopts.password == "" {
   321  		return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD")
   322  	}
   323  
   324  	fromfile, err := readLinesFromFile(opts.FilesFrom)
   325  	if err != nil {
   326  		return err
   327  	}
   328  
   329  	// merge files from files-from into normal args so we can reuse the normal
   330  	// args checks and have the ability to use both files-from and args at the
   331  	// same time
   332  	args = append(args, fromfile...)
   333  	if len(args) == 0 {
   334  		return errors.Fatal("nothing to backup, please specify target files/dirs")
   335  	}
   336  
   337  	target := make([]string, 0, len(args))
   338  	for _, d := range args {
   339  		if a, err := filepath.Abs(d); err == nil {
   340  			d = a
   341  		}
   342  		target = append(target, d)
   343  	}
   344  
   345  	target, err = filterExisting(target)
   346  	if err != nil {
   347  		return err
   348  	}
   349  
   350  	// rejectFuncs collect functions that can reject items from the backup
   351  	var rejectFuncs []RejectFunc
   352  
   353  	// allowed devices
   354  	if opts.ExcludeOtherFS {
   355  		f, err := rejectByDevice(target)
   356  		if err != nil {
   357  			return err
   358  		}
   359  		rejectFuncs = append(rejectFuncs, f)
   360  	}
   361  
   362  	// add patterns from file
   363  	if len(opts.ExcludeFiles) > 0 {
   364  		opts.Excludes = append(opts.Excludes, readExcludePatternsFromFiles(opts.ExcludeFiles)...)
   365  	}
   366  
   367  	if len(opts.Excludes) > 0 {
   368  		rejectFuncs = append(rejectFuncs, rejectByPattern(opts.Excludes))
   369  	}
   370  
   371  	if opts.ExcludeCaches {
   372  		opts.ExcludeIfPresent = append(opts.ExcludeIfPresent, "CACHEDIR.TAG:Signature: 8a477f597d28d172789f06886806bc55")
   373  	}
   374  
   375  	rc := &rejectionCache{}
   376  	for _, spec := range opts.ExcludeIfPresent {
   377  		f, err := rejectIfPresent(spec, rc)
   378  		if err != nil {
   379  			return err
   380  		}
   381  
   382  		rejectFuncs = append(rejectFuncs, f)
   383  	}
   384  
   385  	repo, err := OpenRepository(gopts)
   386  	if err != nil {
   387  		return err
   388  	}
   389  
   390  	lock, err := lockRepo(repo)
   391  	defer unlockRepo(lock)
   392  	if err != nil {
   393  		return err
   394  	}
   395  
   396  	// exclude restic cache
   397  	if repo.Cache != nil {
   398  		f, err := rejectResticCache(repo)
   399  		if err != nil {
   400  			return err
   401  		}
   402  
   403  		rejectFuncs = append(rejectFuncs, f)
   404  	}
   405  
   406  	err = repo.LoadIndex(context.TODO())
   407  	if err != nil {
   408  		return err
   409  	}
   410  
   411  	var parentSnapshotID *restic.ID
   412  
   413  	// Force using a parent
   414  	if !opts.Force && opts.Parent != "" {
   415  		id, err := restic.FindSnapshot(repo, opts.Parent)
   416  		if err != nil {
   417  			return errors.Fatalf("invalid id %q: %v", opts.Parent, err)
   418  		}
   419  
   420  		parentSnapshotID = &id
   421  	}
   422  
   423  	// Find last snapshot to set it as parent, if not already set
   424  	if !opts.Force && parentSnapshotID == nil {
   425  		id, err := restic.FindLatestSnapshot(context.TODO(), repo, target, []restic.TagList{}, opts.Hostname)
   426  		if err == nil {
   427  			parentSnapshotID = &id
   428  		} else if err != restic.ErrNoSnapshotFound {
   429  			return err
   430  		}
   431  	}
   432  
   433  	if parentSnapshotID != nil {
   434  		Verbosef("using parent snapshot %v\n", parentSnapshotID.Str())
   435  	}
   436  
   437  	Verbosef("scan %v\n", target)
   438  
   439  	selectFilter := func(item string, fi os.FileInfo) bool {
   440  		for _, reject := range rejectFuncs {
   441  			if reject(item, fi) {
   442  				return false
   443  			}
   444  		}
   445  		return true
   446  	}
   447  
   448  	stat, err := archiver.Scan(target, selectFilter, newScanProgress(gopts))
   449  	if err != nil {
   450  		return err
   451  	}
   452  
   453  	arch := archiver.New(repo)
   454  	arch.Excludes = opts.Excludes
   455  	arch.SelectFilter = selectFilter
   456  
   457  	arch.Warn = func(dir string, fi os.FileInfo, err error) {
   458  		// TODO: make ignoring errors configurable
   459  		Warnf("%s\rwarning for %s: %v\n", ClearLine(), dir, err)
   460  	}
   461  
   462  	timeStamp := time.Now()
   463  	if opts.TimeStamp != "" {
   464  		timeStamp, err = time.Parse(TimeFormat, opts.TimeStamp)
   465  		if err != nil {
   466  			return errors.Fatalf("error in time option: %v\n", err)
   467  		}
   468  	}
   469  
   470  	_, id, err := arch.Snapshot(context.TODO(), newArchiveProgress(gopts, stat), target, opts.Tags, opts.Hostname, parentSnapshotID, timeStamp)
   471  	if err != nil {
   472  		return err
   473  	}
   474  
   475  	Verbosef("snapshot %s saved\n", id.Str())
   476  
   477  	return nil
   478  }
   479  
   480  func readExcludePatternsFromFiles(excludeFiles []string) []string {
   481  	var excludes []string
   482  	for _, filename := range excludeFiles {
   483  		err := func() (err error) {
   484  			file, err := fs.Open(filename)
   485  			if err != nil {
   486  				return err
   487  			}
   488  			defer func() {
   489  				// return pre-close error if there was one
   490  				if errClose := file.Close(); err == nil {
   491  					err = errClose
   492  				}
   493  			}()
   494  
   495  			scanner := bufio.NewScanner(file)
   496  			for scanner.Scan() {
   497  				line := strings.TrimSpace(scanner.Text())
   498  
   499  				// ignore empty lines
   500  				if line == "" {
   501  					continue
   502  				}
   503  
   504  				// strip comments
   505  				if strings.HasPrefix(line, "#") {
   506  					continue
   507  				}
   508  
   509  				line = os.ExpandEnv(line)
   510  				excludes = append(excludes, line)
   511  			}
   512  			return scanner.Err()
   513  		}()
   514  		if err != nil {
   515  			Warnf("error reading exclude patterns: %v:", err)
   516  			return nil
   517  		}
   518  	}
   519  	return excludes
   520  }