diff options
author | Félix Sipma <felix+debian@gueux.org> | 2018-06-09 09:38:07 +0200 |
---|---|---|
committer | Félix Sipma <felix+debian@gueux.org> | 2018-06-09 09:38:07 +0200 |
commit | f3635cfc4dbbc177c7aa51ab8be2d69b97ae69a8 (patch) | |
tree | d40bcec96bf32015bce20bd027e03d0106bd70c9 /cmd | |
parent | 5f619dba707f469ddfbafe10dc3429b332c17745 (diff) |
New upstream version 0.9.0+ds
Diffstat (limited to 'cmd')
-rw-r--r-- | cmd/restic/background.go | 9 | ||||
-rw-r--r-- | cmd/restic/background_linux.go | 21 | ||||
-rw-r--r-- | cmd/restic/cleanup.go | 5 | ||||
-rw-r--r-- | cmd/restic/cmd_backup.go | 563 | ||||
-rw-r--r-- | cmd/restic/cmd_cache.go | 122 | ||||
-rw-r--r-- | cmd/restic/cmd_cat.go | 2 | ||||
-rw-r--r-- | cmd/restic/cmd_check.go | 62 | ||||
-rw-r--r-- | cmd/restic/cmd_forget.go | 5 | ||||
-rw-r--r-- | cmd/restic/cmd_key.go | 20 | ||||
-rw-r--r-- | cmd/restic/cmd_list.go | 6 | ||||
-rw-r--r-- | cmd/restic/cmd_ls.go | 5 | ||||
-rw-r--r-- | cmd/restic/cmd_mount.go | 1 | ||||
-rw-r--r-- | cmd/restic/cmd_version.go | 2 | ||||
-rw-r--r-- | cmd/restic/exclude.go | 12 | ||||
-rw-r--r-- | cmd/restic/excludes | 31 | ||||
-rw-r--r-- | cmd/restic/format.go | 5 | ||||
-rw-r--r-- | cmd/restic/global.go | 56 | ||||
-rw-r--r-- | cmd/restic/global_debug.go | 46 | ||||
-rw-r--r-- | cmd/restic/global_release.go | 2 | ||||
-rw-r--r-- | cmd/restic/integration_fuse_test.go | 7 | ||||
-rw-r--r-- | cmd/restic/integration_helpers_test.go | 2 | ||||
-rw-r--r-- | cmd/restic/integration_test.go | 338 | ||||
-rw-r--r-- | cmd/restic/main.go | 23 | ||||
-rw-r--r-- | cmd/restic/testdata/backup-data.tar.gz | bin | 177734 -> 11704 bytes |
24 files changed, 696 insertions, 649 deletions
diff --git a/cmd/restic/background.go b/cmd/restic/background.go deleted file mode 100644 index 2f115adfd..000000000 --- a/cmd/restic/background.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build !linux - -package main - -// IsProcessBackground should return true if it is running in the background or false if not -func IsProcessBackground() bool { - //TODO: Check if the process are running in the background in other OS than linux - return false -} diff --git a/cmd/restic/background_linux.go b/cmd/restic/background_linux.go deleted file mode 100644 index b9a2a2f00..000000000 --- a/cmd/restic/background_linux.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "syscall" - "unsafe" - - "github.com/restic/restic/internal/debug" -) - -// IsProcessBackground returns true if it is running in the background or false if not -func IsProcessBackground() bool { - var pid int - _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(syscall.Stdin), syscall.TIOCGPGRP, uintptr(unsafe.Pointer(&pid))) - - if err != 0 { - debug.Log("Can't check if we are in the background. Using default behaviour. Error: %s\n", err.Error()) - return false - } - - return pid != syscall.Getpgrp() -} diff --git a/cmd/restic/cleanup.go b/cmd/restic/cleanup.go index a08127b1d..728883452 100644 --- a/cmd/restic/cleanup.go +++ b/cmd/restic/cleanup.go @@ -64,7 +64,10 @@ func CleanupHandler(c <-chan os.Signal) { fmt.Fprintf(stderr, "%ssignal %v received, cleaning up\n", ClearLine(), s) code := 0 - if s != syscall.SIGINT { + + if s == syscall.SIGINT { + code = 130 + } else { code = 1 } diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index 4e78a1534..487d8d587 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -2,21 +2,26 @@ package main import ( "bufio" - "fmt" - "io" + "bytes" + "context" + "io/ioutil" "os" - "path" - "path/filepath" + "strconv" "strings" "time" "github.com/spf13/cobra" + tomb "gopkg.in/tomb.v2" "github.com/restic/restic/internal/archiver" "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/fs" + "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/textfile" + "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/termstatus" ) var cmdBackup = &cobra.Command{ @@ -42,11 +47,16 @@ given as the arguments. return errors.Fatal("cannot use both `--stdin` and `--files-from -`") } - if backupOptions.Stdin { - return readBackupFromStdin(backupOptions, globalOptions, args) - } + var t tomb.Tomb + term := termstatus.New(globalOptions.stdout, globalOptions.stderr, globalOptions.Quiet) + t.Go(func() error { term.Run(t.Context(globalOptions.ctx)); return nil }) - return runBackup(backupOptions, globalOptions, args) + err := runBackup(backupOptions, globalOptions, term, args) + if err != nil { + return err + } + t.Kill(nil) + return t.Wait() }, } @@ -90,127 +100,6 @@ func init() { f.BoolVar(&backupOptions.WithAtime, "with-atime", false, "store the atime for all files and directories") } -func newScanProgress(gopts GlobalOptions) *restic.Progress { - if gopts.Quiet { - return nil - } - - p := restic.NewProgress() - p.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) { - if IsProcessBackground() { - return - } - - PrintProgress("[%s] %d directories, %d files, %s", formatDuration(d), s.Dirs, s.Files, formatBytes(s.Bytes)) - } - - p.OnDone = func(s restic.Stat, d time.Duration, ticker bool) { - PrintProgress("scanned %d directories, %d files in %s\n", s.Dirs, s.Files, formatDuration(d)) - } - - return p -} - -func newArchiveProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress { - if gopts.Quiet { - return nil - } - - archiveProgress := restic.NewProgress() - - var bps, eta uint64 - itemsTodo := todo.Files + todo.Dirs - - archiveProgress.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) { - if IsProcessBackground() { - return - } - - sec := uint64(d / time.Second) - if todo.Bytes > 0 && sec > 0 && ticker { - bps = s.Bytes / sec - if s.Bytes >= todo.Bytes { - eta = 0 - } else if bps > 0 { - eta = (todo.Bytes - s.Bytes) / bps - } - } - - itemsDone := s.Files + s.Dirs - - status1 := fmt.Sprintf("[%s] %s %s / %s %d / %d items %d errors ", - formatDuration(d), - formatPercent(s.Bytes, todo.Bytes), - formatBytes(s.Bytes), formatBytes(todo.Bytes), - itemsDone, itemsTodo, - s.Errors) - status2 := fmt.Sprintf("ETA %s ", formatSeconds(eta)) - - if w := stdoutTerminalWidth(); w > 0 { - maxlen := w - len(status2) - 1 - - if maxlen < 4 { - status1 = "" - } else if len(status1) > maxlen { - status1 = status1[:maxlen-4] - status1 += "... " - } - } - - PrintProgress("%s%s", status1, status2) - } - - archiveProgress.OnDone = func(s restic.Stat, d time.Duration, ticker bool) { - fmt.Printf("\nduration: %s\n", formatDuration(d)) - } - - return archiveProgress -} - -func newArchiveStdinProgress(gopts GlobalOptions) *restic.Progress { - if gopts.Quiet { - return nil - } - - archiveProgress := restic.NewProgress() - - var bps uint64 - - archiveProgress.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) { - if IsProcessBackground() { - return - } - - sec := uint64(d / time.Second) - if s.Bytes > 0 && sec > 0 && ticker { - bps = s.Bytes / sec - } - - status1 := fmt.Sprintf("[%s] %s %s/s", formatDuration(d), - formatBytes(s.Bytes), - formatBytes(bps)) - - if w := stdoutTerminalWidth(); w > 0 { - maxlen := w - len(status1) - - if maxlen < 4 { - status1 = "" - } else if len(status1) > maxlen { - status1 = status1[:maxlen-4] - status1 += "... " - } - } - - PrintProgress("%s", status1) - } - - archiveProgress.OnDone = func(s restic.Stat, d time.Duration, ticker bool) { - fmt.Printf("\nduration: %s\n", formatDuration(d)) - } - - return archiveProgress -} - // 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) { @@ -231,78 +120,33 @@ func filterExisting(items []string) (result []string, err error) { return } -func readBackupFromStdin(opts BackupOptions, gopts GlobalOptions, args []string) error { - if len(args) != 0 { - return errors.Fatal("when reading from stdin, no additional files can be specified") - } - - fn := opts.StdinFilename - - if fn == "" { - return errors.Fatal("filename for backup from stdin must not be empty") - } - - if filepath.Base(fn) != fn || path.Base(fn) != fn { - return errors.Fatal("filename is invalid (may not contain a directory, slash or backslash)") - } - - if gopts.password == "" { - return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD") - } - - repo, err := OpenRepository(gopts) - if err != nil { - return err - } - - lock, err := lockRepo(repo) - defer unlockRepo(lock) - if err != nil { - return err +// 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 } - err = repo.LoadIndex(gopts.ctx) - if err != nil { - return err - } + var ( + data []byte + err error + ) - r := &archiver.Reader{ - Repository: repo, - Tags: opts.Tags, - Hostname: opts.Hostname, + if filename == "-" { + data, err = ioutil.ReadAll(os.Stdin) + } else { + data, err = textfile.Read(filename) } - _, id, err := r.Archive(gopts.ctx, fn, os.Stdin, newArchiveStdinProgress(gopts)) if err != nil { - return err - } - - Verbosef("archived as %v\n", id.Str()) - return nil -} - -// readFromFile will read all lines from the given filename and write them to 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 r io.Reader = os.Stdin - if filename != "-" { - f, err := os.Open(filename) - if err != nil { - return nil, err - } - defer f.Close() - r = f + return nil, err } var lines []string - scanner := bufio.NewScanner(r) + scanner := bufio.NewScanner(bytes.NewReader(data)) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) // ignore empty lines @@ -323,47 +167,45 @@ func readLinesFromFile(filename string) ([]string, error) { return lines, nil } -func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error { +// Check returns an error when an invalid combination of options was set. +func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error { if opts.FilesFrom == "-" && gopts.password == "" { return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD") } - fromfile, err := readLinesFromFile(opts.FilesFrom) - if err != nil { - return err - } - - // 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, fromfile...) - if len(args) == 0 { - return errors.Fatal("nothing to backup, please specify target files/dirs") - } - - target := make([]string, 0, len(args)) - for _, d := range args { - if a, err := filepath.Abs(d); err == nil { - d = a + if opts.Stdin { + if opts.FilesFrom != "" { + return errors.Fatal("--stdin and --files-from cannot be used together") } - target = append(target, d) - } - target, err = filterExisting(target) - if err != nil { - return err + if len(args) > 0 { + return errors.Fatal("--stdin was specified and files/dirs were listed as arguments") + } } - // rejectFuncs collect functions that can reject items from the backup - var rejectFuncs []RejectFunc + return nil +} +// collectRejectFuncs returns a list of all functions which may reject data +// from being saved in a snapshot +func collectRejectFuncs(opts BackupOptions, repo *repository.Repository, targets []string) (fs []RejectFunc, err error) { // allowed devices if opts.ExcludeOtherFS { - f, err := rejectByDevice(target) + f, err := rejectByDevice(targets) if err != nil { - return err + return nil, err } - rejectFuncs = append(rejectFuncs, f) + fs = append(fs, f) + } + + // exclude restic cache + if repo.Cache != nil { + f, err := rejectResticCache(repo) + if err != nil { + return nil, err + } + + fs = append(fs, f) } // add patterns from file @@ -372,7 +214,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error { } if len(opts.Excludes) > 0 { - rejectFuncs = append(rejectFuncs, rejectByPattern(opts.Excludes)) + fs = append(fs, rejectByPattern(opts.Excludes)) } if opts.ExcludeCaches { @@ -382,88 +224,182 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error { for _, spec := range opts.ExcludeIfPresent { f, err := rejectIfPresent(spec) if err != nil { - return err + return nil, err } - rejectFuncs = append(rejectFuncs, f) + fs = append(fs, f) } - repo, err := OpenRepository(gopts) - if err != nil { - return err - } + return fs, nil +} - lock, err := lockRepo(repo) - defer unlockRepo(lock) - if err != nil { - return err - } +// readExcludePatternsFromFiles reads all exclude files and returns the list of +// exclude patterns. +func readExcludePatternsFromFiles(excludeFiles []string) []string { + var excludes []string + for _, filename := range excludeFiles { + err := func() (err error) { + data, err := textfile.Read(filename) + if err != nil { + return err + } - // exclude restic cache - if repo.Cache != nil { - f, err := rejectResticCache(repo) + scanner := bufio.NewScanner(bytes.NewReader(data)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // ignore empty lines + if line == "" { + continue + } + + // strip comments + if strings.HasPrefix(line, "#") { + continue + } + + line = os.ExpandEnv(line) + excludes = append(excludes, line) + } + return scanner.Err() + }() if err != nil { - return err + Warnf("error reading exclude patterns: %v:", err) + return nil } + } + return excludes +} - rejectFuncs = append(rejectFuncs, f) +// 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 } - err = repo.LoadIndex(gopts.ctx) + fromfile, err := readLinesFromFile(opts.FilesFrom) if err != nil { - return err + return nil, err } - var parentSnapshotID *restic.ID + // 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, fromfile...) + if len(args) == 0 && !opts.Stdin { + return nil, errors.Fatal("nothing to backup, please specify target files/dirs") + } + + targets = args + targets, err = filterExisting(targets) + if err != nil { + return nil, err + } + + return targets, nil +} +// 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 if !opts.Force && opts.Parent != "" { id, err := restic.FindSnapshot(repo, opts.Parent) if err != nil { - return errors.Fatalf("invalid id %q: %v", opts.Parent, err) + return nil, errors.Fatalf("invalid id %q: %v", opts.Parent, err) } - parentSnapshotID = &id + parentID = &id } // Find last snapshot to set it as parent, if not already set - if !opts.Force && parentSnapshotID == nil { - id, err := restic.FindLatestSnapshot(gopts.ctx, repo, target, []restic.TagList{}, opts.Hostname) + if !opts.Force && parentID == nil { + id, err := restic.FindLatestSnapshot(ctx, repo, targets, []restic.TagList{}, opts.Hostname) if err == nil { - parentSnapshotID = &id + parentID = &id } else if err != restic.ErrNoSnapshotFound { - return err + return nil, err } } - if parentSnapshotID != nil { - Verbosef("using parent snapshot %v\n", parentSnapshotID.Str()) + 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 } - Verbosef("scan %v\n", target) + targets, err := collectTargets(opts, args) + if err != nil { + return err + } - selectFilter := func(item string, fi os.FileInfo) bool { - for _, reject := range rejectFuncs { - if reject(item, fi) { - return false + var t tomb.Tomb + + p := ui.NewBackup(term, gopts.verbosity) + + // 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 } + p.MinUpdatePause = time.Second / time.Duration(fps) } - return true } - stat, err := archiver.Scan(target, selectFilter, newScanProgress(gopts)) + t.Go(func() error { return p.Run(t.Context(gopts.ctx)) }) + + p.V("open repository") + repo, err := OpenRepository(gopts) if err != nil { return err } - arch := archiver.New(repo) - arch.Excludes = opts.Excludes - arch.SelectFilter = selectFilter - arch.WithAccessTime = opts.WithAtime + p.V("lock repository") + lock, err := lockRepo(repo) + defer unlockRepo(lock) + if err != nil { + return err + } - arch.Warn = func(dir string, fi os.FileInfo, err error) { - // TODO: make ignoring errors configurable - Warnf("%s\rwarning for %s: %v\n", ClearLine(), dir, err) + // rejectFuncs collect functions that can reject items from the backup + rejectFuncs, err := collectRejectFuncs(opts, repo, targets) + if err != nil { + return err + } + + p.V("load index files") + err = repo.LoadIndex(gopts.ctx) + if err != nil { + return err + } + + parentSnapshotID, err := findParentSnapshot(gopts.ctx, repo, opts, targets) + if err != nil { + return err + } + + if parentSnapshotID != nil { + p.V("using parent snapshot %v\n", parentSnapshotID.Str()) + } + + selectFilter := func(item string, fi os.FileInfo) bool { + for _, reject := range rejectFuncs { + if reject(item, fi) { + return false + } + } + return true } timeStamp := time.Now() @@ -474,54 +410,77 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error { } } - _, id, err := arch.Snapshot(gopts.ctx, newArchiveProgress(gopts, stat), target, opts.Tags, opts.Hostname, parentSnapshotID, timeStamp) - if err != nil { - return err + var targetFS fs.FS = fs.Local{} + if opts.Stdin { + p.V("read data from stdin") + targetFS = &fs.Reader{ + ModTime: timeStamp, + Name: opts.StdinFilename, + Mode: 0644, + ReadCloser: os.Stdin, + } + targets = []string{opts.StdinFilename} } - Verbosef("snapshot %s saved\n", id.Str()) + sc := archiver.NewScanner(targetFS) + sc.Select = selectFilter + sc.Error = p.ScannerError + sc.Result = p.ReportTotal - return nil -} + p.V("start scan on %v", targets) + t.Go(func() error { return sc.Scan(t.Context(gopts.ctx), targets) }) -func readExcludePatternsFromFiles(excludeFiles []string) []string { - var excludes []string - for _, filename := range excludeFiles { - err := func() (err error) { - file, err := fs.Open(filename) - if err != nil { - return err - } - defer func() { - // return pre-close error if there was one - if errClose := file.Close(); err == nil { - err = errClose - } - }() + arch := archiver.New(repo, targetFS, archiver.Options{}) + arch.Select = selectFilter + arch.WithAtime = opts.WithAtime + arch.Error = p.Error + arch.CompleteItem = p.CompleteItemFn + arch.StartFile = p.StartFile + arch.CompleteBlob = p.CompleteBlob - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) + if parentSnapshotID == nil { + parentSnapshotID = &restic.ID{} + } - // ignore empty lines - if line == "" { - continue - } + snapshotOpts := archiver.SnapshotOptions{ + Excludes: opts.Excludes, + Tags: opts.Tags, + Time: timeStamp, + Hostname: opts.Hostname, + ParentSnapshot: *parentSnapshotID, + } - // strip comments - if strings.HasPrefix(line, "#") { - continue - } + uploader := archiver.IndexUploader{ + Repository: repo, + Start: func() { + p.VV("uploading intermediate index") + }, + Complete: func(id restic.ID) { + p.V("uploaded intermediate index %v", id.Str()) + }, + } - line = os.ExpandEnv(line) - excludes = append(excludes, line) - } - return scanner.Err() - }() - if err != nil { - Warnf("error reading exclude patterns: %v:", err) - return nil - } + t.Go(func() error { + return uploader.Upload(gopts.ctx, t.Context(gopts.ctx), 30*time.Second) + }) + + p.V("start backup on %v", targets) + _, id, err := arch.Snapshot(gopts.ctx, targets, snapshotOpts) + if err != nil { + return errors.Fatalf("unable to save snapshot: %v", err) } - return excludes + + p.Finish() + p.P("snapshot %s saved\n", id.Str()) + + // cleanly shutdown all running goroutines + t.Kill(nil) + + // let's see if one returned an error + err = t.Wait() + if err != nil { + return err + } + + return nil } diff --git a/cmd/restic/cmd_cache.go b/cmd/restic/cmd_cache.go new file mode 100644 index 000000000..1f19a40f2 --- /dev/null +++ b/cmd/restic/cmd_cache.go @@ -0,0 +1,122 @@ +package main + +import ( + "fmt" + "path/filepath" + "sort" + "time" + + "github.com/restic/restic/internal/cache" + "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/fs" + "github.com/spf13/cobra" +) + +var cmdCache = &cobra.Command{ + Use: "cache", + Short: "Operate on local cache directories", + Long: ` +The "cache" command allows listing and cleaning local cache directories. +`, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + return runCache(cacheOptions, globalOptions, args) + }, +} + +// CacheOptions bundles all options for the snapshots command. +type CacheOptions struct { + Cleanup bool + MaxAge uint +} + +var cacheOptions CacheOptions + +func init() { + cmdRoot.AddCommand(cmdCache) + + f := cmdCache.Flags() + f.BoolVar(&cacheOptions.Cleanup, "cleanup", false, "remove old cache directories") + f.UintVar(&cacheOptions.MaxAge, "max-age", 30, "max age in `days` for cache directories to be considered old") +} + +func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error { + if len(args) > 0 { + return errors.Fatal("the cache command has no arguments") + } + + if gopts.NoCache { + return errors.Fatal("Refusing to do anything, the cache is disabled") + } + + var ( + cachedir = gopts.CacheDir + err error + ) + + if cachedir == "" { + cachedir, err = cache.DefaultDir() + if err != nil { + return err + } + } + + if opts.Cleanup || gopts.CleanupCache { + oldDirs, err := cache.OlderThan(cachedir, time.Duration(opts.MaxAge)*24*time.Hour) + if err != nil { + return err + } + + if len(oldDirs) == 0 { + Verbosef("no old cache dirs found\n") + return nil + } + + Verbosef("remove %d old cache directories\n", len(oldDirs)) + + for _, item := range oldDirs { + dir := filepath.Join(cachedir, item.Name()) + err = fs.RemoveAll(dir) + if err != nil { + Warnf("unable to remove %v: %v\n", dir, err) + } + } + + return nil + } + + tab := NewTable() + tab.Header = fmt.Sprintf("%-14s %-16s %s", "Repository ID", "Last Used", "Old") + tab.RowFormat = "%-14s %-16s %s" + + dirs, err := cache.All(cachedir) + if err != nil { + return err + } + + if len(dirs) == 0 { + Printf("no cache dirs found, basedir is %v\n", cachedir) + return nil + } + + sort.Slice(dirs, func(i, j int) bool { + return dirs[i].ModTime().Before(dirs[j].ModTime()) + }) + + for _, entry := range dirs { + var old string + if cache.IsOld(entry.ModTime(), time.Duration(opts.MaxAge)*24*time.Hour) { + old = "yes" + } + + tab.Rows = append(tab.Rows, []interface{}{ + entry.Name()[:10], + fmt.Sprintf("%d days ago", uint(time.Since(entry.ModTime()).Hours()/24)), + old, + }) + } + + tab.Write(gopts.stdout) + + return nil +} diff --git a/cmd/restic/cmd_cat.go b/cmd/restic/cmd_cat.go index 98c97a5a5..e735daf88 100644 --- a/cmd/restic/cmd_cat.go +++ b/cmd/restic/cmd_cat.go @@ -58,7 +58,7 @@ func runCat(gopts GlobalOptions, args []string) error { // find snapshot id with prefix id, err = restic.FindSnapshot(repo, args[1]) if err != nil { - return err + return errors.Fatalf("could not find snapshot: %v\n", err) } } } diff --git a/cmd/restic/cmd_check.go b/cmd/restic/cmd_check.go index ac669c6c7..b4e922445 100644 --- a/cmd/restic/cmd_check.go +++ b/cmd/restic/cmd_check.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "io/ioutil" "os" "strconv" "strings" @@ -11,6 +12,7 @@ import ( "github.com/restic/restic/internal/checker" "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/restic" ) @@ -117,15 +119,55 @@ func newReadProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress { return readProgress } +// prepareCheckCache configures a special cache directory for check. +// +// * if --with-cache is specified, the default cache is used +// * if the user explicitly requested --no-cache, we don't use any cache +// * by default, we use a cache in a temporary directory that is deleted after the check +func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions) (cleanup func()) { + cleanup = func() {} + if opts.WithCache { + // use the default cache, no setup needed + return cleanup + } + + if gopts.NoCache { + // don't use any cache, no setup needed + return cleanup + } + + // use a cache in a temporary directory + tempdir, err := ioutil.TempDir("", "restic-check-cache-") + if err != nil { + // if an error occurs, don't use any cache + Warnf("unable to create temporary directory for cache during check, disabling cache: %v\n", err) + gopts.NoCache = true + return cleanup + } + + gopts.CacheDir = tempdir + Verbosef("using temporary cache in %v\n", tempdir) + + cleanup = func() { + err := fs.RemoveAll(tempdir) + if err != nil { + Warnf("error removing temporary cache directory: %v\n", err) + } + } + + return cleanup +} + func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error { if len(args) != 0 { return errors.Fatal("check has no arguments") } - if !opts.WithCache { - // do not use a cache for the checker - gopts.NoCache = true - } + cleanup := prepareCheckCache(opts, &gopts) + AddCleanupHandler(func() error { + cleanup() + return nil + }) repo, err := OpenRepository(gopts) if err != nil { @@ -155,7 +197,7 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error { } if dupFound { - Printf("\nrun `restic rebuild-index' to correct this\n") + Printf("This is non-critical, you can run `restic rebuild-index' to correct this\n") } if len(errs) > 0 { @@ -166,16 +208,26 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error { } errorsFound := false + orphanedPacks := 0 errChan := make(chan error) Verbosef("check all packs\n") go chkr.Packs(gopts.ctx, errChan) for err := range errChan { + if checker.IsOrphanedPack(err) { + orphanedPacks++ + Verbosef("%v\n", err) + continue + } errorsFound = true fmt.Fprintf(os.Stderr, "%v\n", err) } + if orphanedPacks > 0 { + Verbosef("%d additional files were found in the repo, which likely contain duplicate data.\nYou can run `restic prune` to correct this.\n", orphanedPacks) + } + Verbosef("check snapshots, trees and blobs\n") errChan = make(chan error) go chkr.Structure(gopts.ctx, errChan) diff --git a/cmd/restic/cmd_forget.go b/cmd/restic/cmd_forget.go index c11e067a5..4afef1380 100644 --- a/cmd/restic/cmd_forget.go +++ b/cmd/restic/cmd_forget.go @@ -33,6 +33,7 @@ type ForgetOptions struct { Weekly int Monthly int Yearly int + Within restic.Duration KeepTags restic.TagLists Host string @@ -58,6 +59,7 @@ func init() { f.IntVarP(&forgetOptions.Weekly, "keep-weekly", "w", 0, "keep the last `n` weekly snapshots") f.IntVarP(&forgetOptions.Monthly, "keep-monthly", "m", 0, "keep the last `n` monthly snapshots") f.IntVarP(&forgetOptions.Yearly, "keep-yearly", "y", 0, "keep the last `n` yearly snapshots") + f.VarP(&forgetOptions.Within, "keep-within", "", "keep snapshots that were created within `duration` before the newest (e.g. 1y5m7d)") f.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)") // Sadly the commonly used shortcut `H` is already used. @@ -170,6 +172,7 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error { Weekly: opts.Weekly, Monthly: opts.Monthly, Yearly: opts.Yearly, + Within: opts.Within, Tags: opts.KeepTags, } @@ -178,6 +181,8 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error { } if !policy.Empty() { + Verbosef("Applying Policy: %v\n", policy) + for k, snapshotGroup := range snapshotGroups { var key key if json.Unmarshal([]byte(k), &key) != nil { diff --git a/cmd/restic/cmd_key.go b/cmd/restic/cmd_key.go index 7552c778d..d81be6bb9 100644 --- a/cmd/restic/cmd_key.go +++ b/cmd/restic/cmd_key.go @@ -3,6 +3,9 @@ package main import ( "context" "fmt" + "io/ioutil" + "os" + "strings" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/repository" @@ -23,8 +26,13 @@ The "key" command manages keys (passwords) for accessing the repository. }, } +var newPasswordFile string + func init() { cmdRoot.AddCommand(cmdKey) + + flags := cmdKey.Flags() + flags.StringVarP(&newPasswordFile, "new-password-file", "", "", "the file from which to load a new password") } func listKeys(ctx context.Context, s *repository.Repository) error { @@ -64,6 +72,10 @@ func getNewPassword(gopts GlobalOptions) (string, error) { return testKeyNewPassword, nil } + if newPasswordFile != "" { + return loadPasswordFromFile(newPasswordFile) + } + // Since we already have an open repository, temporary remove the password // to prompt the user for the passwd. newopts := gopts @@ -182,3 +194,11 @@ func runKey(gopts GlobalOptions, args []string) error { return nil } + +func loadPasswordFromFile(pwdFile string) (string, error) { + s, err := ioutil.ReadFile(pwdFile) + if os.IsNotExist(err) { + return "", errors.Fatalf("%s does not exist", pwdFile) + } + return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile") +} diff --git a/cmd/restic/cmd_list.go b/cmd/restic/cmd_list.go index 431085ff5..9aa7dc9eb 100644 --- a/cmd/restic/cmd_list.go +++ b/cmd/restic/cmd_list.go @@ -18,7 +18,7 @@ The "list" command allows listing objects in the repository based on type. `, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runList(globalOptions, args) + return runList(cmd, globalOptions, args) }, } @@ -26,9 +26,9 @@ func init() { cmdRoot.AddCommand(cmdList) } -func runList(opts GlobalOptions, args []string) error { +func runList(cmd *cobra.Command, opts GlobalOptions, args []string) error { if len(args) != 1 { - return errors.Fatal("type not specified") + return errors.Fatal("type not specified, usage: " + cmd.Use) } repo, err := OpenRepository(opts) diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index 4a046b7ef..d4a768d70 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -56,7 +56,8 @@ func printTree(ctx context.Context, repo *repository.Repository, id *restic.ID, Printf("%s\n", formatNode(prefix, entry, lsOptions.ListLong)) if entry.Type == "dir" && entry.Subtree != nil { - if err = printTree(ctx, repo, entry.Subtree, filepath.Join(prefix, entry.Name)); err != nil { + entryPath := prefix + string(filepath.Separator) + entry.Name + if err = printTree(ctx, repo, entry.Subtree, entryPath); err != nil { return err } } @@ -84,7 +85,7 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) { Verbosef("snapshot %s of %v at %s):\n", sn.ID().Str(), sn.Paths, sn.Time) - if err = printTree(gopts.ctx, repo, sn.Tree, string(filepath.Separator)); err != nil { + if err = printTree(gopts.ctx, repo, sn.Tree, ""); err != nil { return err } } diff --git a/cmd/restic/cmd_mount.go b/cmd/restic/cmd_mount.go index 8bcc2d8bc..8e21ce203 100644 --- a/cmd/restic/cmd_mount.go +++ b/cmd/restic/cmd_mount.go @@ -1,4 +1,5 @@ // +build !openbsd +// +build !solaris // +build !windows package main diff --git a/cmd/restic/cmd_version.go b/cmd/restic/cmd_version.go index 669c356be..677079a50 100644 --- a/cmd/restic/cmd_version.go +++ b/cmd/restic/cmd_version.go @@ -16,7 +16,7 @@ and the version of this software. `, DisableAutoGenTag: true, Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("restic %s\ncompiled with %v on %v/%v\n", + fmt.Printf("restic %s compiled with %v on %v/%v\n", version, runtime.Version(), runtime.GOOS, runtime.GOARCH) }, } diff --git a/cmd/restic/exclude.go b/cmd/restic/exclude.go index e4a934d9b..537bdc344 100644 --- a/cmd/restic/exclude.go +++ b/cmd/restic/exclude.go @@ -185,6 +185,11 @@ func isDirExcludedByFile(dir, tagFilename, header string) bool { func gatherDevices(items []string) (deviceMap map[string]uint64, err error) { deviceMap = make(map[string]uint64) for _, item := range items { + item, err = filepath.Abs(filepath.Clean(item)) + if err != nil { + return nil, err + } + fi, err := fs.Lstat(item) if err != nil { return nil, err @@ -215,6 +220,8 @@ func rejectByDevice(samples []string) (RejectFunc, error) { return false } + item = filepath.Clean(item) + id, err := fs.DeviceID(fi) if err != nil { // This should never happen because gatherDevices() would have @@ -222,11 +229,14 @@ func rejectByDevice(samples []string) (RejectFunc, error) { panic(err) } - for dir := item; dir != ""; dir = filepath.Dir(dir) { + for dir := item; ; dir = filepath.Dir(dir) { debug.Log("item %v, test dir %v", item, dir) allowedID, ok := allowed[dir] if !ok { + if dir == filepath.Dir(dir) { + break + } continue } diff --git a/cmd/restic/excludes b/cmd/restic/excludes deleted file mode 100644 index ab2f4fd31..000000000 --- a/cmd/restic/excludes +++ /dev/null @@ -1,31 +0,0 @@ -/boot -/dev -/etc -/home -/lost+found -/mnt -/proc -/root -/run -/sys -/tmp -/usr -/var -/opt/android-sdk -/opt/bullet -/opt/dex2jar -/opt/jameica -/opt/google -/opt/JDownloader -/opt/JDownloaderScripts -/opt/opencascade -/opt/vagrant -/opt/visual-studio-code -/opt/vtk6 -/bin -/fonts* -/srv/ftp -/srv/http -/sbin -/lib -/lib64 diff --git a/cmd/restic/format.go b/cmd/restic/format.go index 9f66d1c1d..1f8ab366e 100644 --- a/cmd/restic/format.go +++ b/cmd/restic/format.go @@ -64,8 +64,9 @@ func formatDuration(d time.Duration) string { } func formatNode(prefix string, n *restic.Node, long bool) string { + nodepath := prefix + string(filepath.Separator) + n.Name if !long { - return filepath.Join(prefix, n.Name) + return nodepath } var mode os.FileMode @@ -91,6 +92,6 @@ func formatNode(prefix string, n *restic.Node, long bool) string { return fmt.Sprintf("%s %5d %5d %6d %s %s%s", mode|n.Mode, n.UID, n.GID, n.Size, - n.ModTime.Format(TimeFormat), filepath.Join(prefix, n.Name), + n.ModTime.Format(TimeFormat), nodepath, target) } diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 6055132bd..3a66323dc 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "io/ioutil" "os" "path/filepath" "runtime" @@ -18,6 +17,7 @@ import ( "github.com/restic/restic/internal/backend/gs" "github.com/restic/restic/internal/backend/local" "github.com/restic/restic/internal/backend/location" + "github.com/restic/restic/internal/backend/rclone" "github.com/restic/restic/internal/backend/rest" "github.com/restic/restic/internal/backend/s3" "github.com/restic/restic/internal/backend/sftp" @@ -29,6 +29,7 @@ import ( "github.com/restic/restic/internal/options" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/textfile" "github.com/restic/restic/internal/errors" @@ -42,6 +43,7 @@ type GlobalOptions struct { Repo string PasswordFile string Quiet bool + Verbose int NoLock bool JSON bool CacheDir string @@ -58,6 +60,13 @@ type GlobalOptions struct { stdout io.Writer stderr io.Writer + // verbosity is set as follows: + // 0 means: don't print any messages except errors, this is used when --quiet is specified + // 1 is the default: print essential messages + // 2 means: print more messages, report minor things, this is used when --verbose is specified + // 3 means: print very detailed debug messages, this is used when --debug is specified + verbosity uint + Options []string extended options.Options @@ -80,11 +89,12 @@ func init() { f.StringVarP(&globalOptions.Repo, "repo", "r", os.Getenv("RESTIC_REPOSITORY"), "repository to backup to or restore from (default: $RESTIC_REPOSITORY)") f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", os.Getenv("RESTIC_PASSWORD_FILE"), "read the repository password from a file (default: $RESTIC_PASSWORD_FILE)") f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not output comprehensive progress report") + f.CountVarP(&globalOptions.Verbose, "verbose", "v", "be verbose (specify --verbose multiple times or level `n`)") f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos") f.BoolVarP(&globalOptions.JSON, "json", "", false, "set output mode to JSON for commands that support it") f.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache directory") f.BoolVar(&globalOptions.NoCache, "no-cache", false, "do not use a local cache") - f.StringSliceVar(&globalOptions.CACerts, "cacert", nil, "path to load root certificates from (default: use system certificates)") + f.StringSliceVar(&globalOptions.CACerts, "cacert", nil, "`file` to load root certificates from (default: use system certificates)") f.StringVar(&globalOptions.TLSClientCert, "tls-client-cert", "", "path to a file containing PEM encoded TLS client certificate and private key") f.BoolVar(&globalOptions.CleanupCache, "cleanup-cache", false, "auto remove old cache directories") f.IntVar(&globalOptions.LimitUploadKb, "limit-upload", 0, "limits uploads to a maximum rate in KiB/s. (default: unlimited)") @@ -172,11 +182,9 @@ func Printf(format string, args ...interface{}) { // Verbosef calls Printf to write the message when the verbose flag is set. func Verbosef(format string, args ...interface{}) { - if globalOptions.Quiet { - return + if globalOptions.verbosity >= 1 { + Printf(format, args...) } - - Printf(format, args...) } // PrintProgress wraps fmt.Printf to handle the difference in writing progress @@ -227,8 +235,8 @@ func Exitf(exitcode int, format string, args ...interface{}) { // resolvePassword determines the password to be used for opening the repository. func resolvePassword(opts GlobalOptions, env string) (string, error) { if opts.PasswordFile != "" { - s, err := ioutil.ReadFile(opts.PasswordFile) - if os.IsNotExist(err) { + s, err := textfile.Read(opts.PasswordFile) + if os.IsNotExist(errors.Cause(err)) { return "", errors.Fatalf("%s does not exist", opts.PasswordFile) } return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile") @@ -347,7 +355,11 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) { } if stdoutIsTerminal() { - Verbosef("password is correct\n") + id := s.Config().ID + if len(id) > 8 { + id = id[:8] + } + Verbosef("repository %v opened successfully, password is correct\n", id) } if opts.NoCache { @@ -378,7 +390,7 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) { Printf("removing %d old cache dirs from %v\n", len(oldCacheDirs), c.Base) for _, item := range oldCacheDirs { - dir := filepath.Join(c.Base, item) + dir := filepath.Join(c.Base, item.Name()) err = fs.RemoveAll(dir) if err != nil { Warnf("unable to remove %v: %v\n", dir, err) @@ -440,18 +452,6 @@ func parseConfig(loc location.Location, opts options.Options) (interface{}, erro cfg.ProjectID = os.Getenv("GOOGLE_PROJECT_ID") } - if cfg.JSONKeyPath == "" { - if path := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"); path != "" { - // Check read access - if _, err := ioutil.ReadFile(path); err != nil { - return nil, errors.Fatalf("Failed to read google credential from file %v: %v", path, err) - } - cfg.JSONKeyPath = path - } else { - return nil, errors.Fatal("No credential file path is set") - } - } - if err := opts.Apply(loc.Scheme, &cfg); err != nil { return nil, err } @@ -523,6 +523,14 @@ func parseConfig(loc location.Location, opts options.Options) (interface{}, erro debug.Log("opening rest repository at %#v", cfg) return cfg, nil + case "rclone": + cfg := loc.Config.(rclone.Config) + if err := opts.Apply(loc.Scheme, &cfg); err != nil { + return nil, err + } + + debug.Log("opening rest repository at %#v", cfg) + return cfg, nil } return nil, errors.Fatalf("invalid backend: %q", loc.Scheme) @@ -576,6 +584,8 @@ func open(s string, gopts GlobalOptions, opts options.Options) (restic.Backend, be, err = b2.Open(globalOptions.ctx, cfg.(b2.Config), rt) case "rest": be, err = rest.Open(cfg.(rest.Config), rt) + case "rclone": + be, err = rclone.Open(cfg.(rclone.Config)) default: return nil, errors.Fatalf("invalid backend: %q", loc.Scheme) @@ -637,6 +647,8 @@ func create(s string, opts options.Options) (restic.Backend, error) { return b2.Create(globalOptions.ctx, cfg.(b2.Config), rt) case "rest": return rest.Create(cfg.(rest.Config), rt) + case "rclone": + return rclone.Open(cfg.(rclone.Config)) } debug.Log("invalid repository scheme: %v", s) diff --git a/cmd/restic/global_debug.go b/cmd/restic/global_debug.go index 7cad172f6..6f04d047b 100644 --- a/cmd/restic/global_debug.go +++ b/cmd/restic/global_debug.go @@ -1,4 +1,4 @@ -// +build debug +// +build debug profile package main @@ -15,17 +15,21 @@ import ( ) var ( - listenMemoryProfile string - memProfilePath string - cpuProfilePath string - insecure bool + listenProfile string + memProfilePath string + cpuProfilePath string + traceProfilePath string + blockProfilePath string + insecure bool ) func init() { f := cmdRoot.PersistentFlags() - f.StringVar(&listenMemoryProfile, "listen-profile", "", "listen on this `address:port` for memory profiling") + f.StringVar(&listenProfile, "listen-profile", "", "listen on this `address:port` for memory profiling") f.StringVar(&memProfilePath, "mem-profile", "", "write memory profile to `dir`") f.StringVar(&cpuProfilePath, "cpu-profile", "", "write cpu profile to `dir`") + f.StringVar(&traceProfilePath, "trace-profile", "", "write trace to `dir`") + f.StringVar(&blockProfilePath, "block-profile", "", "write block profile to `dir`") f.BoolVar(&insecure, "insecure-kdf", false, "use insecure KDF settings") } @@ -36,18 +40,32 @@ func (fakeTestingTB) Logf(msg string, args ...interface{}) { } func runDebug() error { - if listenMemoryProfile != "" { - fmt.Fprintf(os.Stderr, "running memory profile HTTP server on %v\n", listenMemoryProfile) + if listenProfile != "" { + fmt.Fprintf(os.Stderr, "running profile HTTP server on %v\n", listenProfile) go func() { - err := http.ListenAndServe(listenMemoryProfile, nil) + err := http.ListenAndServe(listenProfile, nil) if err != nil { - fmt.Fprintf(os.Stderr, "memory profile listen failed: %v\n", err) + fmt.Fprintf(os.Stderr, "profile HTTP server listen failed: %v\n", err) } }() } - if memProfilePath != "" && cpuProfilePath != "" { - return errors.Fatal("only one profile (memory or CPU) may be activated at the same time") + profilesEnabled := 0 + if memProfilePath != "" { + profilesEnabled++ + } + if cpuProfilePath != "" { + profilesEnabled++ + } + if traceProfilePath != "" { + profilesEnabled++ + } + if blockProfilePath != "" { + profilesEnabled++ + } + + if profilesEnabled > 1 { + return errors.Fatal("only one profile (memory, CPU, trace, or block) may be activated at the same time") } var prof interface { @@ -58,6 +76,10 @@ func runDebug() error { prof = profile.Start(profile.Quiet, profile.NoShutdownHook, profile.MemProfile, profile.ProfilePath(memProfilePath)) } else if cpuProfilePath != "" { prof = profile.Start(profile.Quiet, profile.NoShutdownHook, profile.CPUProfile, profile.ProfilePath(cpuProfilePath)) + } else if traceProfilePath != "" { + prof = profile.Start(profile.Quiet, profile.NoShutdownHook, profile.TraceProfile, profile.ProfilePath(traceProfilePath)) + } else if blockProfilePath != "" { + prof = profile.Start(profile.Quiet, profile.NoShutdownHook, profile.BlockProfile, profile.ProfilePath(blockProfilePath)) } if prof != nil { diff --git a/cmd/restic/global_release.go b/cmd/restic/global_release.go index 04c7cba31..f17d99639 100644 --- a/cmd/restic/global_release.go +++ b/cmd/restic/global_release.go @@ -1,4 +1,4 @@ -// +build !debug +// +build !debug,!profile package main diff --git a/cmd/restic/integration_fuse_test.go b/cmd/restic/integration_fuse_test.go index d680dd2af..45a9d4eb0 100644 --- a/cmd/restic/integration_fuse_test.go +++ b/cmd/restic/integration_fuse_test.go @@ -1,4 +1,5 @@ // +build !openbsd +// +build !solaris // +build !windows package main @@ -170,7 +171,7 @@ func TestMount(t *testing.T) { rtest.SetupTarTestFixture(t, env.testdata, filepath.Join("testdata", "backup-data.tar.gz")) // first backup - testRunBackup(t, []string{env.testdata}, BackupOptions{}, env.gopts) + testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts) snapshotIDs := testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 1, "expected one snapshot, got %v", snapshotIDs) @@ -178,7 +179,7 @@ func TestMount(t *testing.T) { checkSnapshots(t, env.gopts, repo, env.mountpoint, env.repo, snapshotIDs, 2) // second backup, implicit incremental - testRunBackup(t, []string{env.testdata}, BackupOptions{}, env.gopts) + testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts) snapshotIDs = testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 2, "expected two snapshots, got %v", snapshotIDs) @@ -187,7 +188,7 @@ func TestMount(t *testing.T) { // third backup, explicit incremental bopts := BackupOptions{Parent: snapshotIDs[0].String()} - testRunBackup(t, []string{env.testdata}, bopts, env.gopts) + testRunBackup(t, "", []string{env.testdata}, bopts, env.gopts) snapshotIDs = testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 3, "expected three snapshots, got %v", snapshotIDs) diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go index 2fb026512..d0450817d 100644 --- a/cmd/restic/integration_helpers_test.go +++ b/cmd/restic/integration_helpers_test.go @@ -11,6 +11,7 @@ import ( "github.com/restic/restic/internal/options" "github.com/restic/restic/internal/repository" + "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" ) @@ -189,6 +190,7 @@ func withTestEnvironment(t testing.TB) (env *testEnvironment, cleanup func()) { } repository.TestUseLowSecurityKDFParameters(t) + restic.TestDisableCheckPolynomial(t) tempdir, err := ioutil.TempDir(rtest.TestTempDir, "restic-test-") rtest.OK(t, err) diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index dbc48703e..8ccf28b1e 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -3,6 +3,7 @@ package main import ( "bufio" "bytes" + "context" "crypto/rand" "encoding/json" "fmt" @@ -17,12 +18,14 @@ import ( "testing" "time" - "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/filter" + "github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui/termstatus" + "golang.org/x/sync/errgroup" ) func parseIDsFromReader(t testing.TB, rd io.Reader) restic.IDs { @@ -44,15 +47,36 @@ func parseIDsFromReader(t testing.TB, rd io.Reader) restic.IDs { func testRunInit(t testing.TB, opts GlobalOptions) { repository.TestUseLowSecurityKDFParameters(t) + restic.TestDisableCheckPolynomial(t) restic.TestSetLockTimeout(t, 0) rtest.OK(t, runInit(opts, nil)) t.Logf("repository initialized at %v", opts.Repo) } -func testRunBackup(t testing.TB, target []string, opts BackupOptions, gopts GlobalOptions) { - t.Logf("backing up %v", target) - rtest.OK(t, runBackup(opts, gopts, target)) +func testRunBackup(t testing.TB, dir string, target []string, opts BackupOptions, gopts GlobalOptions) { + ctx, cancel := context.WithCancel(gopts.ctx) + defer cancel() + + var wg errgroup.Group + term := termstatus.New(gopts.stdout, gopts.stderr, gopts.Quiet) + wg.Go(func() error { term.Run(ctx); return nil }) + + gopts.stdout = ioutil.Discard + t.Logf("backing up %v in %v", target, dir) + if dir != "" { + cleanup := fs.TestChdir(t, dir) + defer cleanup() + } + + rtest.OK(t, runBackup(opts, gopts, term, target)) + + cancel() + + err := wg.Wait() + if err != nil { + t.Fatal(err) + } } func testRunList(t testing.TB, tpe string, opts GlobalOptions) restic.IDs { @@ -62,7 +86,7 @@ func testRunList(t testing.TB, tpe string, opts GlobalOptions) restic.IDs { globalOptions.stdout = os.Stdout }() - rtest.OK(t, runList(opts, []string{tpe})) + rtest.OK(t, runList(cmdList, opts, []string{tpe})) return parseIDsFromReader(t, buf) } @@ -218,7 +242,7 @@ func TestBackup(t *testing.T) { opts := BackupOptions{} // first backup - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts) snapshotIDs := testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 1, "expected one snapshot, got %v", snapshotIDs) @@ -227,7 +251,7 @@ func TestBackup(t *testing.T) { stat1 := dirStats(env.repo) // second backup, implicit incremental - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts) snapshotIDs = testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 2, "expected two snapshots, got %v", snapshotIDs) @@ -241,7 +265,7 @@ func TestBackup(t *testing.T) { testRunCheck(t, env.gopts) // third backup, explicit incremental opts.Parent = snapshotIDs[0].String() - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts) snapshotIDs = testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 3, "expected three snapshots, got %v", snapshotIDs) @@ -285,7 +309,7 @@ func TestBackupNonExistingFile(t *testing.T) { globalOptions.stderr = os.Stderr }() - p := filepath.Join(env.testdata, "0", "0") + p := filepath.Join(env.testdata, "0", "0", "9") dirs := []string{ filepath.Join(p, "0"), filepath.Join(p, "1"), @@ -295,198 +319,7 @@ func TestBackupNonExistingFile(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, dirs, opts, env.gopts) -} - -func TestBackupMissingFile1(t *testing.T) { - env, cleanup := withTestEnvironment(t) - defer cleanup() - - datafile := filepath.Join("testdata", "backup-data.tar.gz") - fd, err := os.Open(datafile) - if os.IsNotExist(errors.Cause(err)) { - t.Skipf("unable to find data file %q, skipping", datafile) - return - } - rtest.OK(t, err) - rtest.OK(t, fd.Close()) - - rtest.SetupTarTestFixture(t, env.testdata, datafile) - - testRunInit(t, env.gopts) - globalOptions.stderr = ioutil.Discard - defer func() { - globalOptions.stderr = os.Stderr - }() - - ranHook := false - debug.Hook("pipe.walk1", func(context interface{}) { - pathname := context.(string) - - if pathname != filepath.Join("testdata", "0", "0", "9") { - return - } - - t.Logf("in hook, removing test file testdata/0/0/9/37") - ranHook = true - - rtest.OK(t, os.Remove(filepath.Join(env.testdata, "0", "0", "9", "37"))) - }) - - opts := BackupOptions{} - - testRunBackup(t, []string{env.testdata}, opts, env.gopts) - testRunCheck(t, env.gopts) - - rtest.Assert(t, ranHook, "hook did not run") - debug.RemoveHook("pipe.walk1") -} - -func TestBackupMissingFile2(t *testing.T) { - env, cleanup := withTestEnvironment(t) - defer cleanup() - - datafile := filepath.Join("testdata", "backup-data.tar.gz") - fd, err := os.Open(datafile) - if os.IsNotExist(errors.Cause(err)) { - t.Skipf("unable to find data file %q, skipping", datafile) - return - } - rtest.OK(t, err) - rtest.OK(t, fd.Close()) - - rtest.SetupTarTestFixture(t, env.testdata, datafile) - - testRunInit(t, env.gopts) - - globalOptions.stderr = ioutil.Discard - defer func() { - globalOptions.stderr = os.Stderr - }() - - ranHook := false - debug.Hook("pipe.walk2", func(context interface{}) { - pathname := context.(string) - - if pathname != filepath.Join("testdata", "0", "0", "9", "37") { - return - } - - t.Logf("in hook, removing test file testdata/0/0/9/37") - ranHook = true - - rtest.OK(t, os.Remove(filepath.Join(env.testdata, "0", "0", "9", "37"))) - }) - - opts := BackupOptions{} - - testRunBackup(t, []string{env.testdata}, opts, env.gopts) - testRunCheck(t, env.gopts) - - rtest.Assert(t, ranHook, "hook did not run") - debug.RemoveHook("pipe.walk2") -} - -func TestBackupChangedFile(t *testing.T) { - env, cleanup := withTestEnvironment(t) - defer cleanup() - - datafile := filepath.Join("testdata", "backup-data.tar.gz") - fd, err := os.Open(datafile) - if os.IsNotExist(errors.Cause(err)) { - t.Skipf("unable to find data file %q, skipping", datafile) - return - } - rtest.OK(t, err) - rtest.OK(t, fd.Close()) - - rtest.SetupTarTestFixture(t, env.testdata, datafile) - - testRunInit(t, env.gopts) - - globalOptions.stderr = ioutil.Discard - defer func() { - globalOptions.stderr = os.Stderr - }() - - modFile := filepath.Join(env.testdata, "0", "0", "6", "18") - - ranHook := false - debug.Hook("archiver.SaveFile", func(context interface{}) { - pathname := context.(string) - - if pathname != modFile { - return - } - - t.Logf("in hook, modifying test file %v", modFile) - ranHook = true - - rtest.OK(t, ioutil.WriteFile(modFile, []byte("modified"), 0600)) - }) - - opts := BackupOptions{} - - testRunBackup(t, []string{env.testdata}, opts, env.gopts) - testRunCheck(t, env.gopts) - - rtest.Assert(t, ranHook, "hook did not run") - debug.RemoveHook("archiver.SaveFile") -} - -func TestBackupDirectoryError(t *testing.T) { - env, cleanup := withTestEnvironment(t) - defer cleanup() - - datafile := filepath.Join("testdata", "backup-data.tar.gz") - fd, err := os.Open(datafile) - if os.IsNotExist(errors.Cause(err)) { - t.Skipf("unable to find data file %q, skipping", datafile) - return - } - rtest.OK(t, err) - rtest.OK(t, fd.Close()) - - rtest.SetupTarTestFixture(t, env.testdata, datafile) - - testRunInit(t, env.gopts) - - globalOptions.stderr = ioutil.Discard - defer func() { - globalOptions.stderr = os.Stderr - }() - - ranHook := false - - testdir := filepath.Join(env.testdata, "0", "0", "9") - - // install hook that removes the dir right before readdirnames() - debug.Hook("pipe.readdirnames", func(context interface{}) { - path := context.(string) - - if path != testdir { - return - } - - t.Logf("in hook, removing test file %v", testdir) - ranHook = true - - rtest.OK(t, os.RemoveAll(testdir)) - }) - - testRunBackup(t, []string{filepath.Join(env.testdata, "0", "0")}, BackupOptions{}, env.gopts) - testRunCheck(t, env.gopts) - - rtest.Assert(t, ranHook, "hook did not run") - debug.RemoveHook("pipe.walk2") - - snapshots := testRunList(t, "snapshots", env.gopts) - rtest.Assert(t, len(snapshots) > 0, - "no snapshots found in repo (%v)", datafile) - - files := testRunLs(t, env.gopts, snapshots[0].String()) - - rtest.Assert(t, len(files) > 1, "snapshot is empty") + testRunBackup(t, "", dirs, opts, env.gopts) } func includes(haystack []string, needle string) bool { @@ -551,21 +384,21 @@ func TestBackupExclude(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, []string{datadir}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts) snapshots, snapshotID := lastSnapshot(snapshots, loadSnapshotMap(t, env.gopts)) files := testRunLs(t, env.gopts, snapshotID) rtest.Assert(t, includes(files, filepath.Join(string(filepath.Separator), "testdata", "foo.tar.gz")), "expected file %q in first snapshot, but it's not included", "foo.tar.gz") opts.Excludes = []string{"*.tar.gz"} - testRunBackup(t, []string{datadir}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts) snapshots, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, env.gopts)) files = testRunLs(t, env.gopts, snapshotID) rtest.Assert(t, !includes(files, filepath.Join(string(filepath.Separator), "testdata", "foo.tar.gz")), "expected file %q not in first snapshot, but it's included", "foo.tar.gz") opts.Excludes = []string{"*.tar.gz", "private/secret"} - testRunBackup(t, []string{datadir}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts) _, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, env.gopts)) files = testRunLs(t, env.gopts, snapshotID) rtest.Assert(t, !includes(files, filepath.Join(string(filepath.Separator), "testdata", "foo.tar.gz")), @@ -575,9 +408,9 @@ func TestBackupExclude(t *testing.T) { } const ( - incrementalFirstWrite = 20 * 1042 * 1024 - incrementalSecondWrite = 12 * 1042 * 1024 - incrementalThirdWrite = 4 * 1042 * 1024 + incrementalFirstWrite = 10 * 1042 * 1024 + incrementalSecondWrite = 1 * 1042 * 1024 + incrementalThirdWrite = 1 * 1042 * 1024 ) func appendRandomData(filename string, bytes uint) error { @@ -615,13 +448,13 @@ func TestIncrementalBackup(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, []string{datadir}, opts, env.gopts) + testRunBackup(t, "", []string{datadir}, opts, env.gopts) testRunCheck(t, env.gopts) stat1 := dirStats(env.repo) rtest.OK(t, appendRandomData(testfile, incrementalSecondWrite)) - testRunBackup(t, []string{datadir}, opts, env.gopts) + testRunBackup(t, "", []string{datadir}, opts, env.gopts) testRunCheck(t, env.gopts) stat2 := dirStats(env.repo) if stat2.size-stat1.size > incrementalFirstWrite { @@ -631,7 +464,7 @@ func TestIncrementalBackup(t *testing.T) { rtest.OK(t, appendRandomData(testfile, incrementalThirdWrite)) - testRunBackup(t, []string{datadir}, opts, env.gopts) + testRunBackup(t, "", []string{datadir}, opts, env.gopts) testRunCheck(t, env.gopts) stat3 := dirStats(env.repo) if stat3.size-stat2.size > incrementalFirstWrite { @@ -650,7 +483,7 @@ func TestBackupTags(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) testRunCheck(t, env.gopts) newest, _ := testRunSnapshots(t, env.gopts) rtest.Assert(t, newest != nil, "expected a new backup, got nil") @@ -659,7 +492,7 @@ func TestBackupTags(t *testing.T) { parent := newest opts.Tags = []string{"NL"} - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) testRunCheck(t, env.gopts) newest, _ = testRunSnapshots(t, env.gopts) rtest.Assert(t, newest != nil, "expected a new backup, got nil") @@ -682,7 +515,7 @@ func TestTag(t *testing.T) { testRunInit(t, env.gopts) rtest.SetupTarTestFixture(t, env.testdata, datafile) - testRunBackup(t, []string{env.testdata}, BackupOptions{}, env.gopts) + testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts) testRunCheck(t, env.gopts) newest, _ := testRunSnapshots(t, env.gopts) rtest.Assert(t, newest != nil, "expected a new backup, got nil") @@ -858,7 +691,7 @@ func TestRestoreFilter(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{filepath.Base(env.testdata)}, opts, env.gopts) testRunCheck(t, env.gopts) snapshotID := testRunList(t, "snapshots", env.gopts)[0] @@ -893,12 +726,12 @@ func TestRestore(t *testing.T) { for i := 0; i < 10; i++ { p := filepath.Join(env.testdata, fmt.Sprintf("foo/bar/testfile%v", i)) rtest.OK(t, os.MkdirAll(filepath.Dir(p), 0755)) - rtest.OK(t, appendRandomData(p, uint(mrand.Intn(5<<21)))) + rtest.OK(t, appendRandomData(p, uint(mrand.Intn(2<<21)))) } opts := BackupOptions{} - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{filepath.Base(env.testdata)}, opts, env.gopts) testRunCheck(t, env.gopts) // Restore latest without any filters @@ -921,12 +754,22 @@ func TestRestoreLatest(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + // chdir manually here so we can get the current directory. This is not the + // same as the temp dir returned by ioutil.TempDir() on darwin. + back := fs.TestChdir(t, filepath.Dir(env.testdata)) + defer back() + + curdir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + testRunBackup(t, "", []string{filepath.Base(env.testdata)}, opts, env.gopts) testRunCheck(t, env.gopts) os.Remove(p) rtest.OK(t, appendRandomData(p, 101)) - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, "", []string{filepath.Base(env.testdata)}, opts, env.gopts) testRunCheck(t, env.gopts) // Restore latest without any filters @@ -934,16 +777,18 @@ func TestRestoreLatest(t *testing.T) { rtest.OK(t, testFileSize(filepath.Join(env.base, "restore0", "testdata", "testfile.c"), int64(101))) // Setup test files in different directories backed up in different snapshots - p1 := filepath.Join(env.testdata, "p1/testfile.c") + p1 := filepath.Join(curdir, filepath.FromSlash("p1/testfile.c")) + rtest.OK(t, os.MkdirAll(filepath.Dir(p1), 0755)) rtest.OK(t, appendRandomData(p1, 102)) - testRunBackup(t, []string{filepath.Dir(p1)}, opts, env.gopts) + testRunBackup(t, "", []string{"p1"}, opts, env.gopts) testRunCheck(t, env.gopts) - p2 := filepath.Join(env.testdata, "p2/testfile.c") + p2 := filepath.Join(curdir, filepath.FromSlash("p2/testfile.c")) + rtest.OK(t, os.MkdirAll(filepath.Dir(p2), 0755)) rtest.OK(t, appendRandomData(p2, 103)) - testRunBackup(t, []string{filepath.Dir(p2)}, opts, env.gopts) + testRunBackup(t, "", []string{"p2"}, opts, env.gopts) testRunCheck(t, env.gopts) p1rAbs := filepath.Join(env.base, "restore1", "p1/testfile.c") @@ -1016,7 +861,7 @@ func TestRestoreNoMetadataOnIgnoredIntermediateDirs(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{filepath.Base(env.testdata)}, opts, env.gopts) testRunCheck(t, env.gopts) snapshotID := testRunList(t, "snapshots", env.gopts)[0] @@ -1054,7 +899,7 @@ func TestFind(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) testRunCheck(t, env.gopts) results := testRunFind(t, false, env.gopts, "unexistingfile") @@ -1094,7 +939,7 @@ func TestFindJSON(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) testRunCheck(t, env.gopts) results := testRunFind(t, true, env.gopts, "unexistingfile") @@ -1197,13 +1042,13 @@ func TestPrune(t *testing.T) { rtest.SetupTarTestFixture(t, env.testdata, datafile) opts := BackupOptions{} - testRunBackup(t, []string{filepath.Join(env.testdata, "0", "0")}, opts, env.gopts) + testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, opts, env.gopts) firstSnapshot := testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(firstSnapshot) == 1, "expected one snapshot, got %v", firstSnapshot) - testRunBackup(t, []string{filepath.Join(env.testdata, "0", "0", "2")}, opts, env.gopts) - testRunBackup(t, []string{filepath.Join(env.testdata, "0", "0", "3")}, opts, env.gopts) + testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "2")}, opts, env.gopts) + testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "3")}, opts, env.gopts) snapshotIDs := testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 3, @@ -1237,7 +1082,7 @@ func TestHardLink(t *testing.T) { opts := BackupOptions{} // first backup - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{filepath.Base(env.testdata)}, opts, env.gopts) snapshotIDs := testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 1, "expected one snapshot, got %v", snapshotIDs) @@ -1311,3 +1156,38 @@ func linkEqual(source, dest []string) bool { return true } + +func TestQuietBackup(t *testing.T) { + env, cleanup := withTestEnvironment(t) + defer cleanup() + + datafile := filepath.Join("testdata", "backup-data.tar.gz") + fd, err := os.Open(datafile) + if os.IsNotExist(errors.Cause(err)) { + t.Skipf("unable to find data file %q, skipping", datafile) + return + } + rtest.OK(t, err) + rtest.OK(t, fd.Close()) + + testRunInit(t, env.gopts) + + rtest.SetupTarTestFixture(t, env.testdata, datafile) + opts := BackupOptions{} + + env.gopts.Quiet = false + testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) + snapshotIDs := testRunList(t, "snapshots", env.gopts) + rtest.Assert(t, len(snapshotIDs) == 1, + "expected one snapshot, got %v", snapshotIDs) + + testRunCheck(t, env.gopts) + + env.gopts.Quiet = true + testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) + snapshotIDs = testRunList(t, "snapshots", env.gopts) + rtest.Assert(t, len(snapshotIDs) == 2, + "expected two snapshots, got %v", snapshotIDs) + + testRunCheck(t, env.gopts) +} diff --git a/cmd/restic/main.go b/cmd/restic/main.go index d1f9c5547..01a902b1d 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -29,14 +29,31 @@ directories in an encrypted repository stored on different backends. SilenceUsage: true, DisableAutoGenTag: true, - PersistentPreRunE: func(*cobra.Command, []string) error { + PersistentPreRunE: func(c *cobra.Command, args []string) error { + // set verbosity, default is one + globalOptions.verbosity = 1 + if globalOptions.Quiet && (globalOptions.Verbose > 1) { + return errors.Fatal("--quiet and --verbose cannot be specified at the same time") + } + + switch { + case globalOptions.Verbose >= 2: + globalOptions.verbosity = 3 + case globalOptions.Verbose > 0: + globalOptions.verbosity = 2 + case globalOptions.Quiet: + globalOptions.verbosity = 0 + } + // parse extended options opts, err := options.Parse(globalOptions.Options) if err != nil { return err } globalOptions.extended = opts - + if c.Name() == "version" { + return nil + } pwd, err := resolvePassword(globalOptions, "RESTIC_PASSWORD") if err != nil { fmt.Fprintf(os.Stderr, "Resolving password failed: %v\n", err) @@ -64,7 +81,7 @@ func init() { func main() { debug.Log("main %#v", os.Args) - debug.Log("restic %s, compiled with %v on %v/%v", + debug.Log("restic %s compiled with %v on %v/%v", version, runtime.Version(), runtime.GOOS, runtime.GOARCH) err := cmdRoot.Execute() diff --git a/cmd/restic/testdata/backup-data.tar.gz b/cmd/restic/testdata/backup-data.tar.gz Binary files differindex 337c18fd9..6ba5881ae 100644 --- a/cmd/restic/testdata/backup-data.tar.gz +++ b/cmd/restic/testdata/backup-data.tar.gz |