github.com/advanderveer/restic@v0.8.1-0.20171209104529-42a8c19aaea6/cmd/restic/cmd_backup.go (about)

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