summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorFélix Sipma <felix+debian@gueux.org>2018-06-09 09:38:07 +0200
committerFélix Sipma <felix+debian@gueux.org>2018-06-09 09:38:07 +0200
commitf3635cfc4dbbc177c7aa51ab8be2d69b97ae69a8 (patch)
treed40bcec96bf32015bce20bd027e03d0106bd70c9 /cmd
parent5f619dba707f469ddfbafe10dc3429b332c17745 (diff)
New upstream version 0.9.0+ds
Diffstat (limited to 'cmd')
-rw-r--r--cmd/restic/background.go9
-rw-r--r--cmd/restic/background_linux.go21
-rw-r--r--cmd/restic/cleanup.go5
-rw-r--r--cmd/restic/cmd_backup.go563
-rw-r--r--cmd/restic/cmd_cache.go122
-rw-r--r--cmd/restic/cmd_cat.go2
-rw-r--r--cmd/restic/cmd_check.go62
-rw-r--r--cmd/restic/cmd_forget.go5
-rw-r--r--cmd/restic/cmd_key.go20
-rw-r--r--cmd/restic/cmd_list.go6
-rw-r--r--cmd/restic/cmd_ls.go5
-rw-r--r--cmd/restic/cmd_mount.go1
-rw-r--r--cmd/restic/cmd_version.go2
-rw-r--r--cmd/restic/exclude.go12
-rw-r--r--cmd/restic/excludes31
-rw-r--r--cmd/restic/format.go5
-rw-r--r--cmd/restic/global.go56
-rw-r--r--cmd/restic/global_debug.go46
-rw-r--r--cmd/restic/global_release.go2
-rw-r--r--cmd/restic/integration_fuse_test.go7
-rw-r--r--cmd/restic/integration_helpers_test.go2
-rw-r--r--cmd/restic/integration_test.go338
-rw-r--r--cmd/restic/main.go23
-rw-r--r--cmd/restic/testdata/backup-data.tar.gzbin177734 -> 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
index 337c18fd9..6ba5881ae 100644
--- a/cmd/restic/testdata/backup-data.tar.gz
+++ b/cmd/restic/testdata/backup-data.tar.gz
Binary files differ