2
2
mirror of https://github.com/octoleo/restic.git synced 2024-11-16 01:57:10 +00:00
restic/cmd/restic/cmd_backup.go

654 lines
18 KiB
Go
Raw Normal View History

2014-04-27 22:00:15 +00:00
package main
import (
2016-04-01 11:50:45 +00:00
"bufio"
"bytes"
2018-04-22 09:57:20 +00:00
"context"
"fmt"
"io"
"io/ioutil"
2014-09-23 20:39:12 +00:00
"os"
"path"
"path/filepath"
"runtime"
2018-04-22 09:57:20 +00:00
"strconv"
"strings"
"time"
2016-09-17 10:36:05 +00:00
"github.com/spf13/cobra"
2018-04-22 09:57:20 +00:00
tomb "gopkg.in/tomb.v2"
2015-04-25 17:20:41 +00:00
2017-07-23 12:21:03 +00:00
"github.com/restic/restic/internal/archiver"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs"
2018-04-22 09:57:20 +00:00
"github.com/restic/restic/internal/repository"
2017-07-24 15:42:25 +00:00
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/textfile"
2018-04-22 09:57:20 +00:00
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/json"
2018-04-22 09:57:20 +00:00
"github.com/restic/restic/internal/ui/termstatus"
2016-09-17 10:36:05 +00:00
)
2015-04-25 17:20:41 +00:00
2016-09-17 10:36:05 +00:00
var cmdBackup = &cobra.Command{
Use: "backup [flags] FILE/DIR [FILE/DIR] ...",
Short: "Create a new backup of files and/or directories",
2016-09-17 10:36:05 +00:00
Long: `
The "backup" command creates a new snapshot and saves the files and directories
given as the arguments.
EXIT STATUS
===========
Exit status is 0 if the command was successful.
Exit status is 1 if there was a fatal error (no snapshot created).
Exit status is 3 if some source data could not be read (incomplete snapshot created).
2016-09-17 10:36:05 +00:00
`,
PreRun: func(cmd *cobra.Command, args []string) {
if backupOptions.Host == "" {
hostname, err := os.Hostname()
if err != nil {
debug.Log("os.Hostname() returned err: %v", err)
return
}
backupOptions.Host = hostname
}
},
DisableAutoGenTag: true,
2016-09-17 10:36:05 +00:00
RunE: func(cmd *cobra.Command, args []string) error {
if backupOptions.Stdin {
for _, filename := range backupOptions.FilesFrom {
if filename == "-" {
return errors.Fatal("cannot use both `--stdin` and `--files-from -`")
}
}
}
2018-04-22 09:57:20 +00:00
var t tomb.Tomb
term := termstatus.New(globalOptions.stdout, globalOptions.stderr, globalOptions.Quiet)
2018-04-22 09:57:20 +00:00
t.Go(func() error { term.Run(t.Context(globalOptions.ctx)); return nil })
2015-04-25 17:20:41 +00:00
2018-04-22 09:57:20 +00:00
err := runBackup(backupOptions, globalOptions, term, args)
if err != nil {
return err
}
t.Kill(nil)
return t.Wait()
2016-09-17 10:36:05 +00:00
},
2015-04-25 17:20:41 +00:00
}
2016-09-17 10:36:05 +00:00
// BackupOptions bundles all options for the backup command.
type BackupOptions struct {
2020-08-25 20:30:57 +00:00
Parent string
Force bool
Excludes []string
InsensitiveExcludes []string
ExcludeFiles []string
InsensitiveExcludeFiles []string
ExcludeOtherFS bool
ExcludeIfPresent []string
ExcludeCaches bool
2020-09-03 01:18:58 +00:00
ExcludeLargerThan string
2020-08-25 20:30:57 +00:00
Stdin bool
StdinFilename string
Tags []string
Host string
FilesFrom []string
TimeStamp string
WithAtime bool
IgnoreInode bool
UseFsSnapshot bool
2015-04-25 17:20:41 +00:00
}
2016-09-17 10:36:05 +00:00
var backupOptions BackupOptions
// ErrInvalidSourceData is used to report an incomplete backup
var ErrInvalidSourceData = errors.New("failed to read all source data during backup")
2016-09-17 10:36:05 +00:00
func init() {
cmdRoot.AddCommand(cmdBackup)
f := cmdBackup.Flags()
f.StringVar(&backupOptions.Parent, "parent", "", "use this parent `snapshot` (default: last snapshot in the repo that has the same target files/directories)")
f.BoolVarP(&backupOptions.Force, "force", "f", false, `force re-reading the target files/directories (overrides the "parent" flag)`)
f.StringArrayVarP(&backupOptions.Excludes, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)")
f.StringArrayVar(&backupOptions.InsensitiveExcludes, "iexclude", nil, "same as --exclude `pattern` but ignores the casing of filenames")
f.StringArrayVar(&backupOptions.ExcludeFiles, "exclude-file", nil, "read exclude patterns from a `file` (can be specified multiple times)")
2020-08-25 20:30:57 +00:00
f.StringArrayVar(&backupOptions.InsensitiveExcludeFiles, "iexclude-file", nil, "same as --exclude-file but ignores casing of `file`names in patterns")
f.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems")
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)")
f.BoolVar(&backupOptions.ExcludeCaches, "exclude-caches", false, `excludes cache directories that are marked with a CACHEDIR.TAG file. See https://bford.info/cachedir/ for the Cache Directory Tagging Standard`)
2020-09-03 01:18:58 +00:00
f.StringVar(&backupOptions.ExcludeLargerThan, "exclude-larger-than", "", "max `size` of the files to be backed up (allowed suffixes: k/K, m/M, g/G, t/T)")
2016-09-17 10:36:05 +00:00
f.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin")
f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "`filename` to use when reading from stdin")
f.StringArrayVar(&backupOptions.Tags, "tag", nil, "add a `tag` for the new snapshot (can be specified multiple times)")
f.StringVarP(&backupOptions.Host, "host", "H", "", "set the `hostname` for the snapshot manually. To prevent an expensive rescan use the \"parent\" flag")
f.StringVar(&backupOptions.Host, "hostname", "", "set the `hostname` for the snapshot manually")
f.MarkDeprecated("hostname", "use --host")
f.StringArrayVar(&backupOptions.FilesFrom, "files-from", nil, "read the files to backup from `file` (can be combined with file args/can be specified multiple times)")
f.StringVar(&backupOptions.TimeStamp, "time", "", "`time` of the backup (ex. '2012-11-01 22:08:41') (default: now)")
f.BoolVar(&backupOptions.WithAtime, "with-atime", false, "store the atime for all files and directories")
f.BoolVar(&backupOptions.IgnoreInode, "ignore-inode", false, "ignore inode number changes when checking for modified files")
if runtime.GOOS == "windows" {
f.BoolVar(&backupOptions.UseFsSnapshot, "use-fs-snapshot", false, "use filesystem snapshot where possible (currently only Windows VSS)")
}
2014-12-07 15:30:52 +00:00
}
// filterExisting returns a slice of all existing items, or an error if no
// items exist at all.
func filterExisting(items []string) (result []string, err error) {
for _, item := range items {
_, err := fs.Lstat(item)
if err != nil && os.IsNotExist(errors.Cause(err)) {
2017-09-09 19:12:41 +00:00
Warnf("%v does not exist, skipping\n", item)
continue
}
result = append(result, item)
}
if len(result) == 0 {
2016-09-01 20:17:37 +00:00
return nil, errors.Fatal("all target directories/files do not exist")
}
return
}
2018-04-22 09:57:20 +00:00
// readFromFile will read all lines from the given filename and return them as
// a string array, if filename is empty readFromFile returns and empty string
// array. If filename is a dash (-), readFromFile will read the lines from the
// standard input.
func readLinesFromFile(filename string) ([]string, error) {
if filename == "" {
return nil, nil
}
var (
data []byte
err error
)
if filename == "-" {
data, err = ioutil.ReadAll(os.Stdin)
} else {
data, err = textfile.Read(filename)
}
if err != nil {
return nil, err
}
var lines []string
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// ignore empty lines
if line == "" {
continue
}
2017-10-19 14:52:06 +00:00
// strip comments
if strings.HasPrefix(line, "#") {
continue
}
lines = append(lines, line)
}
if err := scanner.Err(); err != nil {
return nil, err
}
return lines, nil
}
2018-04-22 09:57:20 +00:00
// Check returns an error when an invalid combination of options was set.
func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error {
if gopts.password == "" {
for _, filename := range opts.FilesFrom {
if filename == "-" {
return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD")
}
}
}
2018-04-22 09:57:20 +00:00
if opts.Stdin {
if len(opts.FilesFrom) > 0 {
2018-04-22 09:57:20 +00:00
return errors.Fatal("--stdin and --files-from cannot be used together")
2015-03-02 13:48:47 +00:00
}
2018-04-22 09:57:20 +00:00
if len(args) > 0 {
return errors.Fatal("--stdin was specified and files/dirs were listed as arguments")
}
}
2018-04-22 09:57:20 +00:00
return nil
}
2017-09-10 13:13:40 +00:00
// collectRejectByNameFuncs returns a list of all functions which may reject data
// from being saved in a snapshot based on path only
func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, targets []string) (fs []RejectByNameFunc, err error) {
2018-04-22 09:57:20 +00:00
// exclude restic cache
if repo.Cache != nil {
f, err := rejectResticCache(repo)
if err != nil {
return nil, err
}
fs = append(fs, f)
2017-09-10 13:13:40 +00:00
}
// add patterns from file
if len(opts.ExcludeFiles) > 0 {
excludes, err := readExcludePatternsFromFiles(opts.ExcludeFiles)
if err != nil {
return nil, err
}
opts.Excludes = append(opts.Excludes, excludes...)
2017-09-10 13:13:40 +00:00
}
2020-08-25 20:30:57 +00:00
if len(opts.InsensitiveExcludeFiles) > 0 {
excludes, err := readExcludePatternsFromFiles(opts.InsensitiveExcludeFiles)
if err != nil {
return nil, err
}
opts.InsensitiveExcludes = append(opts.InsensitiveExcludes, excludes...)
}
2018-10-16 20:39:14 +00:00
if len(opts.InsensitiveExcludes) > 0 {
fs = append(fs, rejectByInsensitivePattern(opts.InsensitiveExcludes))
}
2017-09-10 13:13:40 +00:00
if len(opts.Excludes) > 0 {
2018-04-22 09:57:20 +00:00
fs = append(fs, rejectByPattern(opts.Excludes))
2017-09-10 13:13:40 +00:00
}
if opts.ExcludeCaches {
opts.ExcludeIfPresent = append(opts.ExcludeIfPresent, "CACHEDIR.TAG:Signature: 8a477f597d28d172789f06886806bc55")
}
for _, spec := range opts.ExcludeIfPresent {
f, err := rejectIfPresent(spec)
if err != nil {
2018-04-22 09:57:20 +00:00
return nil, err
}
2017-09-10 13:13:40 +00:00
2018-04-22 09:57:20 +00:00
fs = append(fs, f)
}
2018-04-22 09:57:20 +00:00
return fs, nil
}
2014-04-27 22:00:15 +00:00
// collectRejectFuncs returns a list of all functions which may reject data
// from being saved in a snapshot based on path and file info
func collectRejectFuncs(opts BackupOptions, repo *repository.Repository, targets []string) (fs []RejectFunc, err error) {
// allowed devices
if opts.ExcludeOtherFS && !opts.Stdin {
f, err := rejectByDevice(targets)
if err != nil {
return nil, err
}
fs = append(fs, f)
}
2020-09-03 01:18:58 +00:00
if len(opts.ExcludeLargerThan) != 0 && !opts.Stdin {
f, err := rejectBySize(opts.ExcludeLargerThan)
if err != nil {
return nil, err
}
fs = append(fs, f)
}
return fs, nil
}
2018-04-22 09:57:20 +00:00
// readExcludePatternsFromFiles reads all exclude files and returns the list of
// exclude patterns. For each line, leading and trailing white space is removed
// and comment lines are ignored. For each remaining pattern, environment
// variables are resolved. For adding a literal dollar sign ($), write $$ to
// the file.
func readExcludePatternsFromFiles(excludeFiles []string) ([]string, error) {
getenvOrDollar := func(s string) string {
if s == "$" {
return "$"
}
return os.Getenv(s)
}
2018-04-22 09:57:20 +00:00
var excludes []string
for _, filename := range excludeFiles {
err := func() (err error) {
data, err := textfile.Read(filename)
2018-04-22 09:57:20 +00:00
if err != nil {
return err
}
2015-06-27 12:40:18 +00:00
scanner := bufio.NewScanner(bytes.NewReader(data))
2018-04-22 09:57:20 +00:00
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// ignore empty lines
if line == "" {
continue
}
// strip comments
if strings.HasPrefix(line, "#") {
continue
}
line = os.Expand(line, getenvOrDollar)
2018-04-22 09:57:20 +00:00
excludes = append(excludes, line)
}
return scanner.Err()
}()
if err != nil {
return nil, err
}
2018-04-22 09:57:20 +00:00
}
return excludes, nil
2018-04-22 09:57:20 +00:00
}
2018-04-22 09:57:20 +00:00
// collectTargets returns a list of target files/dirs from several sources.
func collectTargets(opts BackupOptions, args []string) (targets []string, err error) {
if opts.Stdin {
return nil, nil
}
var lines []string
for _, file := range opts.FilesFrom {
fromfile, err := readLinesFromFile(file)
if err != nil {
return nil, err
}
// expand wildcards
for _, line := range fromfile {
var expanded []string
expanded, err := filepath.Glob(line)
if err != nil {
return nil, errors.WithMessage(err, fmt.Sprintf("pattern: %s", line))
}
if len(expanded) == 0 {
Warnf("pattern %q does not match any files, skipping\n", line)
}
lines = append(lines, expanded...)
}
}
2018-04-22 09:57:20 +00:00
// merge files from files-from into normal args so we can reuse the normal
// args checks and have the ability to use both files-from and args at the
// same time
args = append(args, lines...)
2018-04-22 09:57:20 +00:00
if len(args) == 0 && !opts.Stdin {
return nil, errors.Fatal("nothing to backup, please specify target files/dirs")
}
2018-04-22 09:57:20 +00:00
targets = args
targets, err = filterExisting(targets)
if err != nil {
return nil, err
}
return targets, nil
}
2014-11-30 21:34:21 +00:00
2018-04-22 09:57:20 +00:00
// parent returns the ID of the parent snapshot. If there is none, nil is
// returned.
func findParentSnapshot(ctx context.Context, repo restic.Repository, opts BackupOptions, targets []string) (parentID *restic.ID, err error) {
// Force using a parent
2016-09-17 10:36:05 +00:00
if !opts.Force && opts.Parent != "" {
id, err := restic.FindSnapshot(ctx, repo, opts.Parent)
2014-11-30 21:34:21 +00:00
if err != nil {
2018-04-22 09:57:20 +00:00
return nil, errors.Fatalf("invalid id %q: %v", opts.Parent, err)
2014-11-30 21:34:21 +00:00
}
2018-04-22 09:57:20 +00:00
parentID = &id
2014-11-30 21:34:21 +00:00
}
2014-04-27 22:00:15 +00:00
// Find last snapshot to set it as parent, if not already set
2018-04-22 09:57:20 +00:00
if !opts.Force && parentID == nil {
id, err := restic.FindLatestSnapshot(ctx, repo, targets, []restic.TagList{}, []string{opts.Host})
if err == nil {
2018-04-22 09:57:20 +00:00
parentID = &id
} else if err != restic.ErrNoSnapshotFound {
2018-04-22 09:57:20 +00:00
return nil, err
}
}
2018-04-22 09:57:20 +00:00
return parentID, nil
}
func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Terminal, args []string) error {
err := opts.Check(gopts, args)
if err != nil {
return err
}
2018-04-22 09:57:20 +00:00
targets, err := collectTargets(opts, args)
if err != nil {
return err
}
2018-06-01 16:41:44 +00:00
timeStamp := time.Now()
if opts.TimeStamp != "" {
timeStamp, err = time.ParseInLocation(TimeFormat, opts.TimeStamp, time.Local)
2018-06-01 16:41:44 +00:00
if err != nil {
return errors.Fatalf("error in time option: %v\n", err)
}
}
2018-04-22 09:57:20 +00:00
var t tomb.Tomb
if gopts.verbosity >= 2 && !gopts.JSON {
Verbosef("open repository\n")
}
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
type ArchiveProgressReporter interface {
CompleteItem(item string, previous, current *restic.Node, s archiver.ItemStats, d time.Duration)
StartFile(filename string)
CompleteBlob(filename string, bytes uint64)
ScannerError(item string, fi os.FileInfo, err error) error
ReportTotal(item string, s archiver.ScanStats)
SetMinUpdatePause(d time.Duration)
Run(ctx context.Context) error
Error(item string, fi os.FileInfo, err error) error
2018-12-01 20:53:00 +00:00
Finish(snapshotID restic.ID)
// ui.StdioWrapper
Stdout() io.WriteCloser
Stderr() io.WriteCloser
// ui.Message
E(msg string, args ...interface{})
P(msg string, args ...interface{})
V(msg string, args ...interface{})
VV(msg string, args ...interface{})
}
var p ArchiveProgressReporter
if gopts.JSON {
p = json.NewBackup(term, gopts.verbosity)
} else {
p = ui.NewBackup(term, gopts.verbosity)
}
2018-04-22 09:57:20 +00:00
// use the terminal for stdout/stderr
prevStdout, prevStderr := gopts.stdout, gopts.stderr
defer func() {
gopts.stdout, gopts.stderr = prevStdout, prevStderr
}()
gopts.stdout, gopts.stderr = p.Stdout(), p.Stderr()
if s, ok := os.LookupEnv("RESTIC_PROGRESS_FPS"); ok {
fps, err := strconv.Atoi(s)
if err == nil && fps >= 1 {
if fps > 60 {
fps = 60
2017-09-09 19:24:29 +00:00
}
p.SetMinUpdatePause(time.Second / time.Duration(fps))
}
}
2018-04-22 09:57:20 +00:00
t.Go(func() error { return p.Run(t.Context(gopts.ctx)) })
if !gopts.JSON {
p.V("lock repository")
}
2020-08-09 11:24:47 +00:00
lock, err := lockRepo(gopts.ctx, repo)
2018-04-22 09:57:20 +00:00
defer unlockRepo(lock)
if err != nil {
return err
}
// rejectByNameFuncs collect functions that can reject items from the backup based on path only
rejectByNameFuncs, err := collectRejectByNameFuncs(opts, repo, targets)
if err != nil {
return err
}
// rejectFuncs collect functions that can reject items from the backup based on path and file info
2018-04-22 09:57:20 +00:00
rejectFuncs, err := collectRejectFuncs(opts, repo, targets)
if err != nil {
return err
}
if !gopts.JSON {
p.V("load index files")
}
2018-04-22 09:57:20 +00:00
err = repo.LoadIndex(gopts.ctx)
if err != nil {
return err
}
2014-11-23 11:05:43 +00:00
2018-04-22 09:57:20 +00:00
parentSnapshotID, err := findParentSnapshot(gopts.ctx, repo, opts, targets)
if err != nil {
return err
}
if !gopts.JSON && parentSnapshotID != nil {
2018-04-22 09:57:20 +00:00
p.V("using parent snapshot %v\n", parentSnapshotID.Str())
}
selectByNameFilter := func(item string) bool {
for _, reject := range rejectByNameFuncs {
if reject(item) {
return false
}
}
return true
}
2018-04-22 09:57:20 +00:00
selectFilter := func(item string, fi os.FileInfo) bool {
for _, reject := range rejectFuncs {
if reject(item, fi) {
return false
}
}
return true
}
2018-04-22 09:57:20 +00:00
var targetFS fs.FS = fs.Local{}
if runtime.GOOS == "windows" && opts.UseFsSnapshot {
if err = fs.HasSufficientPrivilegesForVSS(); err != nil {
return err
}
errorHandler := func(item string, err error) error {
return p.Error(item, nil, err)
}
messageHandler := func(msg string, args ...interface{}) {
if !gopts.JSON {
p.P(msg, args...)
}
}
localVss := fs.NewLocalVss(errorHandler, messageHandler)
defer localVss.DeleteSnapshots()
targetFS = localVss
}
2018-04-22 09:57:20 +00:00
if opts.Stdin {
if !gopts.JSON {
p.V("read data from stdin")
}
filename := path.Join("/", opts.StdinFilename)
2018-04-22 09:57:20 +00:00
targetFS = &fs.Reader{
ModTime: timeStamp,
Name: filename,
2018-04-22 09:57:20 +00:00
Mode: 0644,
ReadCloser: os.Stdin,
}
targets = []string{filename}
}
2018-04-22 09:57:20 +00:00
sc := archiver.NewScanner(targetFS)
sc.SelectByName = selectByNameFilter
2018-04-22 09:57:20 +00:00
sc.Select = selectFilter
sc.Error = p.ScannerError
sc.Result = p.ReportTotal
2014-04-27 22:00:15 +00:00
if !gopts.JSON {
p.V("start scan on %v", targets)
}
2018-04-22 09:57:20 +00:00
t.Go(func() error { return sc.Scan(t.Context(gopts.ctx), targets) })
2017-08-01 17:20:09 +00:00
2018-04-22 09:57:20 +00:00
arch := archiver.New(repo, targetFS, archiver.Options{})
arch.SelectByName = selectByNameFilter
2018-04-22 09:57:20 +00:00
arch.Select = selectFilter
arch.WithAtime = opts.WithAtime
success := true
arch.Error = func(item string, fi os.FileInfo, err error) error {
success = false
return p.Error(item, fi, err)
}
arch.CompleteItem = p.CompleteItem
2018-04-22 09:57:20 +00:00
arch.StartFile = p.StartFile
arch.CompleteBlob = p.CompleteBlob
arch.IgnoreInode = opts.IgnoreInode
2017-08-01 17:20:09 +00:00
2018-04-22 09:57:20 +00:00
if parentSnapshotID == nil {
parentSnapshotID = &restic.ID{}
}
2017-08-01 17:20:09 +00:00
2018-04-22 09:57:20 +00:00
snapshotOpts := archiver.SnapshotOptions{
Excludes: opts.Excludes,
Tags: opts.Tags,
Time: timeStamp,
Hostname: opts.Host,
2018-04-22 09:57:20 +00:00
ParentSnapshot: *parentSnapshotID,
}
2017-08-01 17:20:09 +00:00
if !gopts.JSON {
p.V("start backup on %v", targets)
}
2018-04-22 09:57:20 +00:00
_, id, err := arch.Snapshot(gopts.ctx, targets, snapshotOpts)
if err != nil {
return errors.Fatalf("unable to save snapshot: %v", err)
2017-08-01 17:20:09 +00:00
}
2018-04-22 09:57:20 +00:00
// cleanly shutdown all running goroutines
t.Kill(nil)
// let's see if one returned an error
err = t.Wait()
// Report finished execution
p.Finish(id)
if !gopts.JSON {
p.P("snapshot %s saved\n", id.Str())
2018-04-22 09:57:20 +00:00
}
if !success {
return ErrInvalidSourceData
}
2018-04-22 09:57:20 +00:00
// Return error if any
return err
2017-08-01 17:20:09 +00:00
}