diff --git a/src/cmds/restic/cmd_backup.go b/src/cmds/restic/cmd_backup.go index 53c859fd5..8de69b2af 100644 --- a/src/cmds/restic/cmd_backup.go +++ b/src/cmds/restic/cmd_backup.go @@ -6,101 +6,66 @@ import ( "os" "path/filepath" "restic" - "restic/archiver" - "restic/debug" - "restic/filter" - "restic/fs" "strings" "time" - "restic/errors" - "golang.org/x/crypto/ssh/terminal" + + "github.com/spf13/cobra" + + "restic/archiver" + "restic/debug" + "restic/errors" + "restic/filter" + "restic/fs" ) -type CmdBackup struct { - Parent string `short:"p" long:"parent" description:"use this parent snapshot (default: last snapshot in repo that has the same target)"` - Force bool `short:"f" long:"force" description:"Force re-reading the target. Overrides the \"parent\" flag"` - Excludes []string `short:"e" long:"exclude" description:"Exclude a pattern (can be specified multiple times)"` - ExcludeOtherFS bool `short:"x" long:"one-file-system" description:"Exclude other file systems"` - ExcludeFile string `long:"exclude-file" description:"Read exclude-patterns from file"` - Stdin bool `long:"stdin" description:"read backup data from stdin"` - StdinFilename string `long:"stdin-filename" default:"stdin" description:"file name to use when reading from stdin"` - Tags []string `long:"tag" description:"Add a tag (can be specified multiple times)"` +var cmdBackup = &cobra.Command{ + Use: "backup [flags] FILE/DIR [FILE/DIR] ...", + Short: "create a new backup of files and/or directories", + Long: ` +The "backup" command creates a new snapshot and saves the files and directories +given as the arguments. +`, + RunE: func(cmd *cobra.Command, args []string) error { + if backupOptions.Stdin { + return readBackupFromStdin(backupOptions, globalOptions, args) + } - global *GlobalOptions + return runBackup(backupOptions, globalOptions, args) + }, } +// BackupOptions bundles all options for the backup command. +type BackupOptions struct { + Parent string + Force bool + Excludes []string + ExcludeFile string + ExcludeOtherFS bool + Stdin bool + StdinFilename string + Tags []string +} + +var backupOptions BackupOptions + func init() { - _, err := parser.AddCommand("backup", - "save file/directory", - "The backup command creates a snapshot of a file or directory", - &CmdBackup{global: &globalOpts}) - if err != nil { - panic(err) - } + cmdRoot.AddCommand(cmdBackup) + + f := cmdBackup.Flags() + f.StringVar(&backupOptions.Parent, "parent", "", "use this parent snapshot (default: last snapshot in the repo that has the same target files/directories)") + f.BoolVarP(&backupOptions.Force, "force", "f", false, `force re-reading the target files/directories. Overrides the "parent" flag`) + f.StringSliceVarP(&backupOptions.Excludes, "exclude", "e", []string{}, "exclude a pattern (can be specified multiple times)") + f.StringVar(&backupOptions.ExcludeFile, "exclude-file", "", "read exclude patterns from a file") + f.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "Exclude other file systems") + f.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin") + f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "", "file name to use when reading from stdin") + f.StringSliceVar(&backupOptions.Tags, "tag", []string{}, "add a tag for the new snapshot (can be specified multiple times)") } -func formatBytes(c uint64) string { - b := float64(c) - - switch { - case c > 1<<40: - return fmt.Sprintf("%.3f TiB", b/(1<<40)) - case c > 1<<30: - return fmt.Sprintf("%.3f GiB", b/(1<<30)) - case c > 1<<20: - return fmt.Sprintf("%.3f MiB", b/(1<<20)) - case c > 1<<10: - return fmt.Sprintf("%.3f KiB", b/(1<<10)) - default: - return fmt.Sprintf("%dB", c) - } -} - -func formatSeconds(sec uint64) string { - hours := sec / 3600 - sec -= hours * 3600 - min := sec / 60 - sec -= min * 60 - if hours > 0 { - return fmt.Sprintf("%d:%02d:%02d", hours, min, sec) - } - - return fmt.Sprintf("%d:%02d", min, sec) -} - -func formatPercent(numerator uint64, denominator uint64) string { - if denominator == 0 { - return "" - } - - percent := 100.0 * float64(numerator) / float64(denominator) - - if percent > 100 { - percent = 100 - } - - return fmt.Sprintf("%3.2f%%", percent) -} - -func formatRate(bytes uint64, duration time.Duration) string { - sec := float64(duration) / float64(time.Second) - rate := float64(bytes) / sec / (1 << 20) - return fmt.Sprintf("%.2fMiB/s", rate) -} - -func formatDuration(d time.Duration) string { - sec := uint64(d / time.Second) - return formatSeconds(sec) -} - -func (cmd CmdBackup) Usage() string { - return "DIR/FILE [DIR/FILE] [...]" -} - -func (cmd CmdBackup) newScanProgress() *restic.Progress { - if !cmd.global.ShowProgress() { +func newScanProgress(gopts GlobalOptions) *restic.Progress { + if gopts.Quiet { return nil } @@ -115,8 +80,8 @@ func (cmd CmdBackup) newScanProgress() *restic.Progress { return p } -func (cmd CmdBackup) newArchiveProgress(todo restic.Stat) *restic.Progress { - if !cmd.global.ShowProgress() { +func newArchiveProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress { + if gopts.Quiet { return nil } @@ -169,8 +134,8 @@ func (cmd CmdBackup) newArchiveProgress(todo restic.Stat) *restic.Progress { return archiveProgress } -func (cmd CmdBackup) newArchiveStdinProgress() *restic.Progress { - if !cmd.global.ShowProgress() { +func newArchiveStdinProgress(gopts GlobalOptions) *restic.Progress { + if gopts.Quiet { return nil } @@ -250,12 +215,12 @@ func gatherDevices(items []string) (deviceMap map[uint64]struct{}, err error) { return deviceMap, nil } -func (cmd CmdBackup) readFromStdin(args []string) error { +func readBackupFromStdin(opts BackupOptions, gopts GlobalOptions, args []string) error { if len(args) != 0 { return errors.Fatalf("when reading from stdin, no additional files can be specified") } - repo, err := cmd.global.OpenRepository() + repo, err := OpenRepository(gopts) if err != nil { return err } @@ -271,7 +236,7 @@ func (cmd CmdBackup) readFromStdin(args []string) error { return err } - _, id, err := archiver.ArchiveReader(repo, cmd.newArchiveStdinProgress(), os.Stdin, cmd.StdinFilename, cmd.Tags) + _, id, err := archiver.ArchiveReader(repo, newArchiveStdinProgress(gopts), os.Stdin, opts.StdinFilename, opts.Tags) if err != nil { return err } @@ -280,13 +245,9 @@ func (cmd CmdBackup) readFromStdin(args []string) error { return nil } -func (cmd CmdBackup) Execute(args []string) error { - if cmd.Stdin { - return cmd.readFromStdin(args) - } - +func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error { if len(args) == 0 { - return errors.Fatalf("wrong number of parameters, Usage: %s", cmd.Usage()) + return errors.Fatalf("wrong number of parameters") } target := make([]string, 0, len(args)) @@ -304,7 +265,7 @@ func (cmd CmdBackup) Execute(args []string) error { // allowed devices var allowedDevs map[uint64]struct{} - if cmd.ExcludeOtherFS { + if opts.ExcludeOtherFS { allowedDevs, err = gatherDevices(target) if err != nil { return err @@ -312,7 +273,7 @@ func (cmd CmdBackup) Execute(args []string) error { debug.Log("backup.Execute", "allowed devices: %v\n", allowedDevs) } - repo, err := cmd.global.OpenRepository() + repo, err := OpenRepository(gopts) if err != nil { return err } @@ -331,17 +292,17 @@ func (cmd CmdBackup) Execute(args []string) error { var parentSnapshotID *restic.ID // Force using a parent - if !cmd.Force && cmd.Parent != "" { - id, err := restic.FindSnapshot(repo, cmd.Parent) + if !opts.Force && opts.Parent != "" { + id, err := restic.FindSnapshot(repo, opts.Parent) if err != nil { - return errors.Fatalf("invalid id %q: %v", cmd.Parent, err) + return errors.Fatalf("invalid id %q: %v", opts.Parent, err) } parentSnapshotID = &id } // Find last snapshot to set it as parent, if not already set - if !cmd.Force && parentSnapshotID == nil { + if !opts.Force && parentSnapshotID == nil { id, err := restic.FindLatestSnapshot(repo, target, "") if err == nil { parentSnapshotID = &id @@ -351,16 +312,16 @@ func (cmd CmdBackup) Execute(args []string) error { } if parentSnapshotID != nil { - cmd.global.Verbosef("using parent snapshot %v\n", parentSnapshotID.Str()) + Verbosef("using parent snapshot %v\n", parentSnapshotID.Str()) } - cmd.global.Verbosef("scan %v\n", target) + Verbosef("scan %v\n", target) // add patterns from file - if cmd.ExcludeFile != "" { - file, err := fs.Open(cmd.ExcludeFile) + if opts.ExcludeFile != "" { + file, err := fs.Open(opts.ExcludeFile) if err != nil { - cmd.global.Warnf("error reading exclude patterns: %v", err) + Warnf("error reading exclude patterns: %v", err) return nil } @@ -369,15 +330,15 @@ func (cmd CmdBackup) Execute(args []string) error { line := scanner.Text() if !strings.HasPrefix(line, "#") { line = os.ExpandEnv(line) - cmd.Excludes = append(cmd.Excludes, line) + opts.Excludes = append(opts.Excludes, line) } } } selectFilter := func(item string, fi os.FileInfo) bool { - matched, err := filter.List(cmd.Excludes, item) + matched, err := filter.List(opts.Excludes, item) if err != nil { - cmd.global.Warnf("error for exclude pattern: %v", err) + Warnf("error for exclude pattern: %v", err) } if matched { @@ -385,7 +346,7 @@ func (cmd CmdBackup) Execute(args []string) error { return false } - if !cmd.ExcludeOtherFS { + if !opts.ExcludeOtherFS { return true } @@ -404,27 +365,27 @@ func (cmd CmdBackup) Execute(args []string) error { return true } - stat, err := archiver.Scan(target, selectFilter, cmd.newScanProgress()) + stat, err := archiver.Scan(target, selectFilter, newScanProgress(gopts)) if err != nil { return err } arch := archiver.New(repo) - arch.Excludes = cmd.Excludes + arch.Excludes = opts.Excludes arch.SelectFilter = selectFilter arch.Error = func(dir string, fi os.FileInfo, err error) error { // TODO: make ignoring errors configurable - cmd.global.Warnf("%s\rerror for %s: %v\n", ClearLine(), dir, err) + Warnf("%s\rerror for %s: %v\n", ClearLine(), dir, err) return nil } - _, id, err := arch.Snapshot(cmd.newArchiveProgress(stat), target, cmd.Tags, parentSnapshotID) + _, id, err := arch.Snapshot(newArchiveProgress(gopts, stat), target, opts.Tags, parentSnapshotID) if err != nil { return err } - cmd.global.Verbosef("snapshot %s saved\n", id.Str()) + Verbosef("snapshot %s saved\n", id.Str()) return nil } diff --git a/src/cmds/restic/cmd_cat.go b/src/cmds/restic/cmd_cat.go index 802257066..5b7529f40 100644 --- a/src/cmds/restic/cmd_cat.go +++ b/src/cmds/restic/cmd_cat.go @@ -5,6 +5,8 @@ import ( "fmt" "os" + "github.com/spf13/cobra" + "restic" "restic/backend" "restic/debug" @@ -12,30 +14,27 @@ import ( "restic/repository" ) -type CmdCat struct { - global *GlobalOptions +var cmdCat = &cobra.Command{ + Use: "cat [flags] [pack|blob|tree|snapshot|key|masterkey|config|lock] ID", + Short: "print internal objects to stdout", + Long: ` +The "cat" command is used to print internal objects to stdout. +`, + RunE: func(cmd *cobra.Command, args []string) error { + return runCat(globalOptions, args) + }, } func init() { - _, err := parser.AddCommand("cat", - "dump something", - "The cat command dumps data structures or data from a repository", - &CmdCat{global: &globalOpts}) - if err != nil { - panic(err) - } + cmdRoot.AddCommand(cmdCat) } -func (cmd CmdCat) Usage() string { - return "[pack|blob|tree|snapshot|key|masterkey|config|lock] ID" -} - -func (cmd CmdCat) Execute(args []string) error { +func runCat(gopts GlobalOptions, args []string) error { if len(args) < 1 || (args[0] != "masterkey" && args[0] != "config" && len(args) != 2) { - return errors.Fatalf("type or ID not specified, Usage: %s", cmd.Usage()) + return errors.Fatalf("type or ID not specified") } - repo, err := cmd.global.OpenRepository() + repo, err := OpenRepository(gopts) if err != nil { return err } @@ -158,7 +157,7 @@ func (cmd CmdCat) Execute(args []string) error { hash := restic.Hash(buf) if !hash.Equal(id) { - fmt.Fprintf(cmd.global.stderr, "Warning: hash of data does not match ID, want\n %v\ngot:\n %v\n", id.String(), hash.String()) + fmt.Fprintf(stderr, "Warning: hash of data does not match ID, want\n %v\ngot:\n %v\n", id.String(), hash.String()) } _, err = os.Stdout.Write(buf) diff --git a/src/cmds/restic/cmd_check.go b/src/cmds/restic/cmd_check.go index 01da5b09c..093bbe1b2 100644 --- a/src/cmds/restic/cmd_check.go +++ b/src/cmds/restic/cmd_check.go @@ -5,6 +5,8 @@ import ( "os" "time" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" "restic" @@ -12,29 +14,36 @@ import ( "restic/errors" ) -type CmdCheck struct { - ReadData bool `long:"read-data" description:"Read data blobs"` - CheckUnused bool `long:"check-unused" description:"Check for unused blobs"` - - global *GlobalOptions +var cmdCheck = &cobra.Command{ + Use: "check [flags]", + Short: "check the repository for errors", + Long: ` +The "check" command tests the repository for errors and reports any errors it +finds. It can also be used to read all data and therefore simulate a restore. +`, + RunE: func(cmd *cobra.Command, args []string) error { + return runCheck(checkOptions, globalOptions, args) + }, } +// CheckOptions bundle all options for the 'check' command. +type CheckOptions struct { + ReadData bool + CheckUnused bool +} + +var checkOptions CheckOptions + func init() { - _, err := parser.AddCommand("check", - "check the repository", - "The check command check the integrity and consistency of the repository", - &CmdCheck{global: &globalOpts}) - if err != nil { - panic(err) - } + cmdRoot.AddCommand(cmdCheck) + + f := cmdCheck.Flags() + f.BoolVar(&checkOptions.ReadData, "read-data", false, "Read all data blobs") + f.BoolVar(&checkOptions.CheckUnused, "check-unused", false, "Find unused blobs") } -func (cmd CmdCheck) Usage() string { - return "[check-options]" -} - -func (cmd CmdCheck) newReadProgress(todo restic.Stat) *restic.Progress { - if !cmd.global.ShowProgress() { +func newReadProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress { + if gopts.Quiet { return nil } @@ -64,18 +73,18 @@ func (cmd CmdCheck) newReadProgress(todo restic.Stat) *restic.Progress { return readProgress } -func (cmd CmdCheck) Execute(args []string) error { +func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error { if len(args) != 0 { return errors.Fatal("check has no arguments") } - repo, err := cmd.global.OpenRepository() + repo, err := OpenRepository(gopts) if err != nil { return err } - if !cmd.global.NoLock { - cmd.global.Verbosef("Create exclusive lock for repository\n") + if !gopts.NoLock { + Verbosef("Create exclusive lock for repository\n") lock, err := lockRepoExclusive(repo) defer unlockRepo(lock) if err != nil { @@ -85,24 +94,24 @@ func (cmd CmdCheck) Execute(args []string) error { chkr := checker.New(repo) - cmd.global.Verbosef("Load indexes\n") + Verbosef("Load indexes\n") hints, errs := chkr.LoadIndex() dupFound := false for _, hint := range hints { - cmd.global.Printf("%v\n", hint) + Printf("%v\n", hint) if _, ok := hint.(checker.ErrDuplicatePacks); ok { dupFound = true } } if dupFound { - cmd.global.Printf("\nrun `restic rebuild-index' to correct this\n") + Printf("\nrun `restic rebuild-index' to correct this\n") } if len(errs) > 0 { for _, err := range errs { - cmd.global.Warnf("error: %v\n", err) + Warnf("error: %v\n", err) } return errors.Fatal("LoadIndex returned errors") } @@ -113,7 +122,7 @@ func (cmd CmdCheck) Execute(args []string) error { errorsFound := false errChan := make(chan error) - cmd.global.Verbosef("Check all packs\n") + Verbosef("Check all packs\n") go chkr.Packs(errChan, done) for err := range errChan { @@ -121,7 +130,7 @@ func (cmd CmdCheck) Execute(args []string) error { fmt.Fprintf(os.Stderr, "%v\n", err) } - cmd.global.Verbosef("Check snapshots, trees and blobs\n") + Verbosef("Check snapshots, trees and blobs\n") errChan = make(chan error) go chkr.Structure(errChan, done) @@ -137,17 +146,17 @@ func (cmd CmdCheck) Execute(args []string) error { } } - if cmd.CheckUnused { + if opts.CheckUnused { for _, id := range chkr.UnusedBlobs() { - cmd.global.Verbosef("unused blob %v\n", id.Str()) + Verbosef("unused blob %v\n", id.Str()) errorsFound = true } } - if cmd.ReadData { - cmd.global.Verbosef("Read all data\n") + if opts.ReadData { + Verbosef("Read all data\n") - p := cmd.newReadProgress(restic.Stat{Blobs: chkr.CountPacks()}) + p := newReadProgress(gopts, restic.Stat{Blobs: chkr.CountPacks()}) errChan := make(chan error) go chkr.ReadData(p, errChan, done) diff --git a/src/cmds/restic/cmd_dump.go b/src/cmds/restic/cmd_dump.go index f29aff905..1a93bb45a 100644 --- a/src/cmds/restic/cmd_dump.go +++ b/src/cmds/restic/cmd_dump.go @@ -8,6 +8,8 @@ import ( "io" "os" + "github.com/spf13/cobra" + "restic" "restic/errors" "restic/pack" @@ -16,24 +18,19 @@ import ( "restic/worker" ) -type CmdDump struct { - global *GlobalOptions - - repo *repository.Repository +var cmdDump = &cobra.Command{ + Use: "dump [indexes|snapshots|trees|all|packs]", + Short: "dump data structures", + Long: ` +The "dump" command dumps data structures from a repository as JSON objects. It +is used for debugging purposes only.`, + RunE: func(cmd *cobra.Command, args []string) error { + return runDump(globalOptions, args) + }, } func init() { - _, err := parser.AddCommand("dump", - "dump data structures", - "The dump command dumps data structures from a repository as JSON documents", - &CmdDump{global: &globalOpts}) - if err != nil { - panic(err) - } -} - -func (cmd CmdDump) Usage() string { - return "[indexes|snapshots|trees|all|packs]" + cmdRoot.AddCommand(cmdDump) } func prettyPrintJSON(wr io.Writer, item interface{}) error { @@ -148,14 +145,14 @@ func printPacks(repo *repository.Repository, wr io.Writer) error { return nil } -func (cmd CmdDump) DumpIndexes() error { +func dumpIndexes(repo restic.Repository) error { done := make(chan struct{}) defer close(done) - for id := range cmd.repo.List(restic.IndexFile, done) { + for id := range repo.List(restic.IndexFile, done) { fmt.Printf("index_id: %v\n", id) - idx, err := repository.LoadIndex(cmd.repo, id) + idx, err := repository.LoadIndex(repo, id) if err != nil { return err } @@ -169,21 +166,22 @@ func (cmd CmdDump) DumpIndexes() error { return nil } -func (cmd CmdDump) Execute(args []string) error { +func runDump(gopts GlobalOptions, args []string) error { if len(args) != 1 { - return errors.Fatalf("type not specified, Usage: %s", cmd.Usage()) + return errors.Fatalf("type not specified") } - repo, err := cmd.global.OpenRepository() + repo, err := OpenRepository(gopts) if err != nil { return err } - cmd.repo = repo - lock, err := lockRepo(repo) - defer unlockRepo(lock) - if err != nil { - return err + if !gopts.NoLock { + lock, err := lockRepo(repo) + defer unlockRepo(lock) + if err != nil { + return err + } } err = repo.LoadIndex() @@ -195,7 +193,7 @@ func (cmd CmdDump) Execute(args []string) error { switch tpe { case "indexes": - return cmd.DumpIndexes() + return dumpIndexes(repo) case "snapshots": return debugPrintSnapshots(repo, os.Stdout) case "packs": @@ -208,7 +206,7 @@ func (cmd CmdDump) Execute(args []string) error { } fmt.Printf("\nindexes:\n") - err = cmd.DumpIndexes() + err = dumpIndexes(repo) if err != nil { return err } diff --git a/src/cmds/restic/cmd_find.go b/src/cmds/restic/cmd_find.go index 683adaa87..002f902c3 100644 --- a/src/cmds/restic/cmd_find.go +++ b/src/cmds/restic/cmd_find.go @@ -4,27 +4,53 @@ import ( "path/filepath" "time" + "github.com/spf13/cobra" + "restic" "restic/debug" "restic/errors" "restic/repository" ) +var cmdFind = &cobra.Command{ + Use: "find [flags] PATTERN", + Short: "find a file or directory", + Long: ` +The "find" command searches for files or directories in snapshots stored in the +repo. `, + RunE: func(cmd *cobra.Command, args []string) error { + return runFind(findOptions, globalOptions, args) + }, +} + +// FindOptions bundle all options for the find command. +type FindOptions struct { + Oldest string + Newest string + Snapshot string +} + +var findOptions FindOptions + +func init() { + cmdRoot.AddCommand(cmdFind) + + f := cmdFind.Flags() + f.StringVarP(&findOptions.Oldest, "oldest", "o", "", "Oldest modification date/time") + f.StringVarP(&findOptions.Newest, "newest", "n", "", "Newest modification date/time") + f.StringVarP(&findOptions.Snapshot, "snapshot", "s", "", "Snapshot ID to search in") +} + +type findPattern struct { + oldest, newest time.Time + pattern string +} + type findResult struct { node *restic.Node path string } -type CmdFind struct { - Oldest string `short:"o" long:"oldest" description:"Oldest modification date/time"` - Newest string `short:"n" long:"newest" description:"Newest modification date/time"` - Snapshot string `short:"s" long:"snapshot" description:"Snapshot ID to search in"` - - oldest, newest time.Time - pattern string - global *GlobalOptions -} - var timeFormats = []string{ "2006-01-02", "2006-01-02 15:04", @@ -39,16 +65,6 @@ var timeFormats = []string{ "Mon Jan 2 15:04:05 -0700 MST 2006", } -func init() { - _, err := parser.AddCommand("find", - "find a file/directory", - "The find command searches for files or directories in snapshots", - &CmdFind{global: &globalOpts}) - if err != nil { - panic(err) - } -} - func parseTime(str string) (time.Time, error) { for _, fmt := range timeFormats { if t, err := time.ParseInLocation(fmt, str, time.Local); err == nil { @@ -59,7 +75,7 @@ func parseTime(str string) (time.Time, error) { return time.Time{}, errors.Fatalf("unable to parse time: %q", str) } -func (c CmdFind) findInTree(repo *repository.Repository, id restic.ID, path string) ([]findResult, error) { +func findInTree(repo *repository.Repository, pat findPattern, id restic.ID, path string) ([]findResult, error) { debug.Log("restic.find", "checking tree %v\n", id) tree, err := repo.LoadTree(id) if err != nil { @@ -70,20 +86,20 @@ func (c CmdFind) findInTree(repo *repository.Repository, id restic.ID, path stri for _, node := range tree.Nodes { debug.Log("restic.find", " testing entry %q\n", node.Name) - m, err := filepath.Match(c.pattern, node.Name) + m, err := filepath.Match(pat.pattern, node.Name) if err != nil { return nil, err } if m { debug.Log("restic.find", " pattern matches\n") - if !c.oldest.IsZero() && node.ModTime.Before(c.oldest) { - debug.Log("restic.find", " ModTime is older than %s\n", c.oldest) + if !pat.oldest.IsZero() && node.ModTime.Before(pat.oldest) { + debug.Log("restic.find", " ModTime is older than %s\n", pat.oldest) continue } - if !c.newest.IsZero() && node.ModTime.After(c.newest) { - debug.Log("restic.find", " ModTime is newer than %s\n", c.newest) + if !pat.newest.IsZero() && node.ModTime.After(pat.newest) { + debug.Log("restic.find", " ModTime is newer than %s\n", pat.newest) continue } @@ -93,7 +109,7 @@ func (c CmdFind) findInTree(repo *repository.Repository, id restic.ID, path stri } if node.Type == "dir" { - subdirResults, err := c.findInTree(repo, *node.Subtree, filepath.Join(path, node.Name)) + subdirResults, err := findInTree(repo, pat, *node.Subtree, filepath.Join(path, node.Name)) if err != nil { return nil, err } @@ -105,15 +121,15 @@ func (c CmdFind) findInTree(repo *repository.Repository, id restic.ID, path stri return results, nil } -func (c CmdFind) findInSnapshot(repo *repository.Repository, id restic.ID) error { - debug.Log("restic.find", "searching in snapshot %s\n for entries within [%s %s]", id.Str(), c.oldest, c.newest) +func findInSnapshot(repo *repository.Repository, pat findPattern, id restic.ID) error { + debug.Log("restic.find", "searching in snapshot %s\n for entries within [%s %s]", id.Str(), pat.oldest, pat.newest) sn, err := restic.LoadSnapshot(repo, id) if err != nil { return err } - results, err := c.findInTree(repo, *sn.Tree, "") + results, err := findInTree(repo, pat, *sn.Tree, "") if err != nil { return err } @@ -121,49 +137,50 @@ func (c CmdFind) findInSnapshot(repo *repository.Repository, id restic.ID) error if len(results) == 0 { return nil } - c.global.Verbosef("found %d matching entries in snapshot %s\n", len(results), id) + Verbosef("found %d matching entries in snapshot %s\n", len(results), id) for _, res := range results { res.node.Name = filepath.Join(res.path, res.node.Name) - c.global.Printf(" %s\n", res.node) + Printf(" %s\n", res.node) } return nil } -func (CmdFind) Usage() string { - return "[find-OPTIONS] PATTERN" -} - -func (c CmdFind) Execute(args []string) error { +func runFind(opts FindOptions, gopts GlobalOptions, args []string) error { if len(args) != 1 { - return errors.Fatalf("wrong number of arguments, Usage: %s", c.Usage()) + return errors.Fatalf("wrong number of arguments") } - var err error + var ( + err error + pat findPattern + ) - if c.Oldest != "" { - c.oldest, err = parseTime(c.Oldest) + if opts.Oldest != "" { + pat.oldest, err = parseTime(opts.Oldest) if err != nil { return err } } - if c.Newest != "" { - c.newest, err = parseTime(c.Newest) + if opts.Newest != "" { + pat.newest, err = parseTime(opts.Newest) if err != nil { return err } } - repo, err := c.global.OpenRepository() + repo, err := OpenRepository(gopts) if err != nil { return err } - lock, err := lockRepo(repo) - defer unlockRepo(lock) - if err != nil { - return err + if !gopts.NoLock { + lock, err := lockRepo(repo) + defer unlockRepo(lock) + if err != nil { + return err + } } err = repo.LoadIndex() @@ -171,21 +188,21 @@ func (c CmdFind) Execute(args []string) error { return err } - c.pattern = args[0] + pat.pattern = args[0] - if c.Snapshot != "" { - snapshotID, err := restic.FindSnapshot(repo, c.Snapshot) + if opts.Snapshot != "" { + snapshotID, err := restic.FindSnapshot(repo, opts.Snapshot) if err != nil { return errors.Fatalf("invalid id %q: %v", args[1], err) } - return c.findInSnapshot(repo, snapshotID) + return findInSnapshot(repo, pat, snapshotID) } done := make(chan struct{}) defer close(done) for snapshotID := range repo.List(restic.SnapshotFile, done) { - err := c.findInSnapshot(repo, snapshotID) + err := findInSnapshot(repo, pat, snapshotID) if err != nil { return err diff --git a/src/cmds/restic/cmd_forget.go b/src/cmds/restic/cmd_forget.go index 52db6fea0..f122178eb 100644 --- a/src/cmds/restic/cmd_forget.go +++ b/src/cmds/restic/cmd_forget.go @@ -5,46 +5,58 @@ import ( "io" "restic" "strings" + + "github.com/spf13/cobra" ) -// CmdForget implements the 'forget' command. -type CmdForget struct { - Last int `short:"l" long:"keep-last" description:"keep the last n snapshots"` - Hourly int `short:"H" long:"keep-hourly" description:"keep the last n hourly snapshots"` - Daily int `short:"d" long:"keep-daily" description:"keep the last n daily snapshots"` - Weekly int `short:"w" long:"keep-weekly" description:"keep the last n weekly snapshots"` - Monthly int `short:"m" long:"keep-monthly"description:"keep the last n monthly snapshots"` - Yearly int `short:"y" long:"keep-yearly" description:"keep the last n yearly snapshots"` - - KeepTags []string `long:"keep-tag" description:"alwaps keep snapshots with this tag (can be specified multiple times)"` - - Hostname string `long:"hostname" description:"only forget snapshots for the given hostname"` - Tags []string `long:"tag" description:"only forget snapshots with the tag (can be specified multiple times)"` - - DryRun bool `short:"n" long:"dry-run" description:"do not delete anything, just print what would be done"` - - global *GlobalOptions +var cmdForget = &cobra.Command{ + Use: "forget [flags] [snapshot ID] [...]", + Short: "forget removes snapshots from the repository", + Long: ` +The "forget" command removes snapshots according to a policy. Please note that +this command really only deletes the snapshot object in the repository, which +is a reference to data stored there. In order to remove this (now unreferenced) +data after 'forget' was run successfully, see the 'prune' command. `, + RunE: func(cmd *cobra.Command, args []string) error { + return runForget(forgetOptions, globalOptions, args) + }, } +// ForgetOptions collects all options for the forget command. +type ForgetOptions struct { + Last int + Hourly int + Daily int + Weekly int + Monthly int + Yearly int + + KeepTags []string + + Hostname string + Tags []string + + DryRun bool +} + +var forgetOptions ForgetOptions + func init() { - _, err := parser.AddCommand("forget", - "removes snapshots from a repository", - ` -The forget command removes snapshots according to a policy. Please note -that this command really only deletes the snapshot object in the repo, which -is a reference to data stored there. In order to remove this (now -unreferenced) data after 'forget' was run successfully, see the 'prune' -command. -`, - &CmdForget{global: &globalOpts}) - if err != nil { - panic(err) - } -} + cmdRoot.AddCommand(cmdForget) -// Usage returns usage information for 'forget'. -func (cmd CmdForget) Usage() string { - return "[snapshot ID] ..." + f := cmdForget.Flags() + f.IntVarP(&forgetOptions.Last, "keep-last", "l", 0, "keep the last n snapshots") + f.IntVarP(&forgetOptions.Hourly, "keep-hourly", "H", 0, "keep the last n hourly snapshots") + f.IntVarP(&forgetOptions.Daily, "keep-daily", "d", 0, "keep the last n daily snapshots") + 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.StringSliceVar(&forgetOptions.KeepTags, "keep-tag", []string{}, "always keep snapshots with this tag (can be specified multiple times)") + f.StringVar(&forgetOptions.Hostname, "hostname", "", "only forget snapshots for the given hostname") + f.StringSliceVar(&forgetOptions.Tags, "tag", []string{}, "only forget snapshots with the tag (can be specified multiple times)") + + f.BoolVarP(&forgetOptions.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done") } func printSnapshots(w io.Writer, snapshots restic.Snapshots) { @@ -87,9 +99,8 @@ func printSnapshots(w io.Writer, snapshots restic.Snapshots) { tab.Write(w) } -// Execute runs the 'forget' command. -func (cmd CmdForget) Execute(args []string) error { - repo, err := cmd.global.OpenRepository() +func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error { + repo, err := OpenRepository(gopts) if err != nil { return err } @@ -112,26 +123,26 @@ func (cmd CmdForget) Execute(args []string) error { return err } - if !cmd.DryRun { + if !opts.DryRun { err = repo.Backend().Remove(restic.SnapshotFile, id.String()) if err != nil { return err } - cmd.global.Verbosef("removed snapshot %v\n", id.Str()) + Verbosef("removed snapshot %v\n", id.Str()) } else { - cmd.global.Verbosef("would removed snapshot %v\n", id.Str()) + Verbosef("would removed snapshot %v\n", id.Str()) } } policy := restic.ExpirePolicy{ - Last: cmd.Last, - Hourly: cmd.Hourly, - Daily: cmd.Daily, - Weekly: cmd.Weekly, - Monthly: cmd.Monthly, - Yearly: cmd.Yearly, - Tags: cmd.KeepTags, + Last: opts.Last, + Hourly: opts.Hourly, + Daily: opts.Daily, + Weekly: opts.Weekly, + Monthly: opts.Monthly, + Yearly: opts.Yearly, + Tags: opts.KeepTags, } if policy.Empty() { @@ -153,11 +164,11 @@ func (cmd CmdForget) Execute(args []string) error { snapshotGroups := make(map[key]restic.Snapshots) for _, sn := range snapshots { - if cmd.Hostname != "" && sn.Hostname != cmd.Hostname { + if opts.Hostname != "" && sn.Hostname != opts.Hostname { continue } - if !sn.HasTags(cmd.Tags) { + if !sn.HasTags(opts.Tags) { continue } @@ -168,18 +179,18 @@ func (cmd CmdForget) Execute(args []string) error { } for key, snapshotGroup := range snapshotGroups { - cmd.global.Printf("snapshots for host %v, directories %v:\n\n", key.Hostname, key.Dirs) + Printf("snapshots for host %v, directories %v:\n\n", key.Hostname, key.Dirs) keep, remove := restic.ApplyPolicy(snapshotGroup, policy) - cmd.global.Printf("keep %d snapshots:\n", len(keep)) - printSnapshots(cmd.global.stdout, keep) - cmd.global.Printf("\n") + Printf("keep %d snapshots:\n", len(keep)) + printSnapshots(globalOptions.stdout, keep) + Printf("\n") - cmd.global.Printf("remove %d snapshots:\n", len(remove)) - printSnapshots(cmd.global.stdout, remove) - cmd.global.Printf("\n") + Printf("remove %d snapshots:\n", len(remove)) + printSnapshots(globalOptions.stdout, remove) + Printf("\n") - if !cmd.DryRun { + if !opts.DryRun { for _, sn := range remove { err = repo.Backend().Remove(restic.SnapshotFile, sn.ID().String()) if err != nil { diff --git a/src/cmds/restic/cmd_init.go b/src/cmds/restic/cmd_init.go index 967a8cd10..d134e9376 100644 --- a/src/cmds/restic/cmd_init.go +++ b/src/cmds/restic/cmd_init.go @@ -3,24 +3,37 @@ package main import ( "restic/errors" "restic/repository" + + "github.com/spf13/cobra" ) -type CmdInit struct { - global *GlobalOptions +var cmdInit = &cobra.Command{ + Use: "init", + Short: "initialize a new repository", + Long: ` +The "init" command initializes a new repository. +`, + RunE: func(cmd *cobra.Command, args []string) error { + return runInit(globalOptions, args) + }, } -func (cmd CmdInit) Execute(args []string) error { - if cmd.global.Repo == "" { +func init() { + cmdRoot.AddCommand(cmdInit) +} + +func runInit(gopts GlobalOptions, args []string) error { + if gopts.Repo == "" { return errors.Fatal("Please specify repository location (-r)") } - be, err := create(cmd.global.Repo) + be, err := create(gopts.Repo) if err != nil { - cmd.global.Exitf(1, "creating backend at %s failed: %v\n", cmd.global.Repo, err) + return errors.Fatalf("create backend at %s failed: %v\n", gopts.Repo, err) } - if cmd.global.password == "" { - cmd.global.password, err = cmd.global.ReadPasswordTwice( + if gopts.password == "" { + gopts.password, err = ReadPasswordTwice(gopts, "enter password for new backend: ", "enter password again: ") if err != nil { @@ -30,26 +43,16 @@ func (cmd CmdInit) Execute(args []string) error { s := repository.New(be) - err = s.Init(cmd.global.password) + err = s.Init(gopts.password) if err != nil { - cmd.global.Exitf(1, "creating key in backend at %s failed: %v\n", cmd.global.Repo, err) + return errors.Fatalf("create key in backend at %s failed: %v\n", gopts.Repo, err) } - cmd.global.Verbosef("created restic backend %v at %s\n", s.Config().ID[:10], cmd.global.Repo) - cmd.global.Verbosef("\n") - cmd.global.Verbosef("Please note that knowledge of your password is required to access\n") - cmd.global.Verbosef("the repository. Losing your password means that your data is\n") - cmd.global.Verbosef("irrecoverably lost.\n") + Verbosef("created restic backend %v at %s\n", s.Config().ID[:10], gopts.Repo) + Verbosef("\n") + Verbosef("Please note that knowledge of your password is required to access\n") + Verbosef("the repository. Losing your password means that your data is\n") + Verbosef("irrecoverably lost.\n") return nil } - -func init() { - _, err := parser.AddCommand("init", - "create repository", - "The init command creates a new repository", - &CmdInit{global: &globalOpts}) - if err != nil { - panic(err) - } -} diff --git a/src/cmds/restic/cmd_key.go b/src/cmds/restic/cmd_key.go index f609ad2d1..4e99f0c86 100644 --- a/src/cmds/restic/cmd_key.go +++ b/src/cmds/restic/cmd_key.go @@ -4,42 +4,39 @@ import ( "fmt" "restic" + "github.com/spf13/cobra" + "restic/errors" "restic/repository" ) -type CmdKey struct { - global *GlobalOptions - newPassword string +var cmdKey = &cobra.Command{ + Use: "key [list|add|rm|passwd] [ID]", + Short: "manage keys (passwords)", + Long: ` +The "key" command manages keys (passwords) for accessing a repository. +`, + RunE: func(cmd *cobra.Command, args []string) error { + return runKey(globalOptions, args) + }, } func init() { - _, err := parser.AddCommand("key", - "manage keys", - "The key command manages keys (passwords) of a repository", - &CmdKey{global: &globalOpts}) - if err != nil { - panic(err) - } + cmdRoot.AddCommand(cmdKey) } -func (cmd CmdKey) listKeys(s *repository.Repository) error { +func listKeys(s *repository.Repository) error { tab := NewTable() tab.Header = fmt.Sprintf(" %-10s %-10s %-10s %s", "ID", "User", "Host", "Created") tab.RowFormat = "%s%-10s %-10s %-10s %s" - plen, err := s.PrefixLength(restic.KeyFile) - if err != nil { - return err - } - done := make(chan struct{}) defer close(done) for id := range s.List(restic.KeyFile, done) { k, err := repository.LoadKey(s, id.String()) if err != nil { - cmd.global.Warnf("LoadKey() failed: %v\n", err) + Warnf("LoadKey() failed: %v\n", err) continue } @@ -49,25 +46,28 @@ func (cmd CmdKey) listKeys(s *repository.Repository) error { } else { current = " " } - tab.Rows = append(tab.Rows, []interface{}{current, id.String()[:plen], + tab.Rows = append(tab.Rows, []interface{}{current, id.Str(), k.Username, k.Hostname, k.Created.Format(TimeFormat)}) } - return tab.Write(cmd.global.stdout) + return tab.Write(globalOptions.stdout) } -func (cmd CmdKey) getNewPassword() (string, error) { - if cmd.newPassword != "" { - return cmd.newPassword, nil +// testKeyNewPassword is used to set a new password during integration testing. +var testKeyNewPassword string + +func getNewPassword(gopts GlobalOptions) (string, error) { + if testKeyNewPassword != "" { + return testKeyNewPassword, nil } - return cmd.global.ReadPasswordTwice( + return ReadPasswordTwice(gopts, "enter password for new key: ", "enter password again: ") } -func (cmd CmdKey) addKey(repo *repository.Repository) error { - pw, err := cmd.getNewPassword() +func addKey(gopts GlobalOptions, repo *repository.Repository) error { + pw, err := getNewPassword(gopts) if err != nil { return err } @@ -77,12 +77,12 @@ func (cmd CmdKey) addKey(repo *repository.Repository) error { return errors.Fatalf("creating new key failed: %v\n", err) } - cmd.global.Verbosef("saved new key as %s\n", id) + Verbosef("saved new key as %s\n", id) return nil } -func (cmd CmdKey) deleteKey(repo *repository.Repository, name string) error { +func deleteKey(repo *repository.Repository, name string) error { if name == repo.KeyName() { return errors.Fatal("refusing to remove key currently used to access repository") } @@ -92,12 +92,12 @@ func (cmd CmdKey) deleteKey(repo *repository.Repository, name string) error { return err } - cmd.global.Verbosef("removed key %v\n", name) + Verbosef("removed key %v\n", name) return nil } -func (cmd CmdKey) changePassword(repo *repository.Repository) error { - pw, err := cmd.getNewPassword() +func changePassword(gopts GlobalOptions, repo *repository.Repository) error { + pw, err := getNewPassword(gopts) if err != nil { return err } @@ -112,21 +112,17 @@ func (cmd CmdKey) changePassword(repo *repository.Repository) error { return err } - cmd.global.Verbosef("saved new key as %s\n", id) + Verbosef("saved new key as %s\n", id) return nil } -func (cmd CmdKey) Usage() string { - return "[list|add|rm|passwd] [ID]" -} - -func (cmd CmdKey) Execute(args []string) error { +func runKey(gopts GlobalOptions, args []string) error { if len(args) < 1 || (args[0] == "rm" && len(args) != 2) { - return errors.Fatalf("wrong number of arguments, Usage: %s", cmd.Usage()) + return errors.Fatalf("wrong number of arguments") } - repo, err := cmd.global.OpenRepository() + repo, err := OpenRepository(gopts) if err != nil { return err } @@ -139,7 +135,7 @@ func (cmd CmdKey) Execute(args []string) error { return err } - return cmd.listKeys(repo) + return listKeys(repo) case "add": lock, err := lockRepo(repo) defer unlockRepo(lock) @@ -147,7 +143,7 @@ func (cmd CmdKey) Execute(args []string) error { return err } - return cmd.addKey(repo) + return addKey(gopts, repo) case "rm": lock, err := lockRepoExclusive(repo) defer unlockRepo(lock) @@ -160,7 +156,7 @@ func (cmd CmdKey) Execute(args []string) error { return err } - return cmd.deleteKey(repo, id) + return deleteKey(repo, id) case "passwd": lock, err := lockRepoExclusive(repo) defer unlockRepo(lock) @@ -168,7 +164,7 @@ func (cmd CmdKey) Execute(args []string) error { return err } - return cmd.changePassword(repo) + return changePassword(gopts, repo) } return nil diff --git a/src/cmds/restic/cmd_list.go b/src/cmds/restic/cmd_list.go index a17d5ce64..a37de8d9e 100644 --- a/src/cmds/restic/cmd_list.go +++ b/src/cmds/restic/cmd_list.go @@ -3,37 +3,36 @@ package main import ( "restic" "restic/errors" + + "github.com/spf13/cobra" ) -type CmdList struct { - global *GlobalOptions +var cmdList = &cobra.Command{ + Use: "list [blobs|packs|index|snapshots|keys|locks]", + Short: "list items in the repository", + Long: ` + +`, + RunE: func(cmd *cobra.Command, args []string) error { + return runList(globalOptions, args) + }, } func init() { - _, err := parser.AddCommand("list", - "lists data", - "The list command lists structures or data of a repository", - &CmdList{global: &globalOpts}) - if err != nil { - panic(err) - } + cmdRoot.AddCommand(cmdList) } -func (cmd CmdList) Usage() string { - return "[blobs|packs|index|snapshots|keys|locks]" -} - -func (cmd CmdList) Execute(args []string) error { +func runList(opts GlobalOptions, args []string) error { if len(args) != 1 { - return errors.Fatalf("type not specified, Usage: %s", cmd.Usage()) + return errors.Fatalf("type not specified") } - repo, err := cmd.global.OpenRepository() + repo, err := OpenRepository(opts) if err != nil { return err } - if !cmd.global.NoLock { + if !opts.NoLock { lock, err := lockRepo(repo) defer unlockRepo(lock) if err != nil { @@ -58,7 +57,7 @@ func (cmd CmdList) Execute(args []string) error { } for id := range repo.List(t, nil) { - cmd.global.Printf("%s\n", id) + Printf("%s\n", id) } return nil diff --git a/src/cmds/restic/cmd_ls.go b/src/cmds/restic/cmd_ls.go index 4e3b29e8a..431f814c1 100644 --- a/src/cmds/restic/cmd_ls.go +++ b/src/cmds/restic/cmd_ls.go @@ -5,29 +5,34 @@ import ( "os" "path/filepath" + "github.com/spf13/cobra" + "restic" "restic/errors" "restic/repository" ) -type CmdLs struct { - Long bool `short:"l" long:"long" description:"Use a long listing format showing size and mode"` - - global *GlobalOptions +var cmdLs = &cobra.Command{ + Use: "ls [flags] snapshot-ID", + Short: "list files in a snapshot", + Long: ` +The "ls" command allows listing files and directories in a snapshot. +`, + RunE: func(cmd *cobra.Command, args []string) error { + return runLs(globalOptions, args) + }, } +var listLong bool + func init() { - _, err := parser.AddCommand("ls", - "list files", - "The ls command lists all files and directories in a snapshot", - &CmdLs{global: &globalOpts}) - if err != nil { - panic(err) - } + cmdRoot.AddCommand(cmdLs) + + cmdLs.Flags().BoolVarP(&listLong, "long", "l", false, "use a long listing format showing size and mode") } -func (cmd CmdLs) printNode(prefix string, n *restic.Node) string { - if !cmd.Long { +func printNode(prefix string, n *restic.Node) string { + if !listLong { return filepath.Join(prefix, n.Name) } @@ -46,17 +51,17 @@ func (cmd CmdLs) printNode(prefix string, n *restic.Node) string { } } -func (cmd CmdLs) printTree(prefix string, repo *repository.Repository, id restic.ID) error { +func printTree(prefix string, repo *repository.Repository, id restic.ID) error { tree, err := repo.LoadTree(id) if err != nil { return err } for _, entry := range tree.Nodes { - cmd.global.Printf(cmd.printNode(prefix, entry) + "\n") + Printf(printNode(prefix, entry) + "\n") if entry.Type == "dir" && entry.Subtree != nil { - err = cmd.printTree(filepath.Join(prefix, entry.Name), repo, *entry.Subtree) + err = printTree(filepath.Join(prefix, entry.Name), repo, *entry.Subtree) if err != nil { return err } @@ -66,16 +71,12 @@ func (cmd CmdLs) printTree(prefix string, repo *repository.Repository, id restic return nil } -func (cmd CmdLs) Usage() string { - return "snapshot-ID [DIR]" -} - -func (cmd CmdLs) Execute(args []string) error { +func runLs(gopts GlobalOptions, args []string) error { if len(args) < 1 || len(args) > 2 { - return errors.Fatalf("wrong number of arguments, Usage: %s", cmd.Usage()) + return errors.Fatalf("no snapshot ID given") } - repo, err := cmd.global.OpenRepository() + repo, err := OpenRepository(gopts) if err != nil { return err } @@ -95,7 +96,7 @@ func (cmd CmdLs) Execute(args []string) error { return err } - cmd.global.Verbosef("snapshot of %v at %s:\n", sn.Paths, sn.Time) + Verbosef("snapshot of %v at %s:\n", sn.Paths, sn.Time) - return cmd.printTree("", repo, *sn.Tree) + return printTree("", repo, *sn.Tree) } diff --git a/src/cmds/restic/cmd_mount.go b/src/cmds/restic/cmd_mount.go index 1cae2e74e..bf1551554 100644 --- a/src/cmds/restic/cmd_mount.go +++ b/src/cmds/restic/cmd_mount.go @@ -6,6 +6,8 @@ package main import ( "os" + "github.com/spf13/cobra" + "restic/debug" "restic/errors" @@ -16,33 +18,36 @@ import ( "bazil.org/fuse/fs" ) -type CmdMount struct { - Root bool `long:"owner-root" description:"use 'root' as the owner of files and dirs"` - - global *GlobalOptions +var cmdMount = &cobra.Command{ + Use: "mount [flags] mountpoint", + Short: "mount the repository", + Long: ` +The "mount" command mounts the repository via fuse to a directory. This is a +read-only mount. +`, + RunE: func(cmd *cobra.Command, args []string) error { + return runMount(mountOptions, globalOptions, args) + }, } +// MountOptions collects all options for the mount command. +type MountOptions struct { + OwnerRoot bool +} + +var mountOptions MountOptions + func init() { - _, err := parser.AddCommand("mount", - "mount a repository", - "The mount command mounts a repository read-only to a given directory", - &CmdMount{ - global: &globalOpts, - }) - if err != nil { - panic(err) - } + cmdRoot.AddCommand(cmdMount) + + cmdMount.Flags().BoolVar(&mountOptions.OwnerRoot, "owner-root", false, "use 'root' as the owner of files and dirs") } -func (cmd CmdMount) Usage() string { - return "MOUNTPOINT" -} - -func (cmd CmdMount) Mount(mountpoint string) error { +func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error { debug.Log("mount", "start mount") defer debug.Log("mount", "finish mount") - repo, err := cmd.global.OpenRepository() + repo, err := OpenRepository(gopts) if err != nil { return err } @@ -53,7 +58,7 @@ func (cmd CmdMount) Mount(mountpoint string) error { } if _, err := resticfs.Stat(mountpoint); os.IsNotExist(errors.Cause(err)) { - cmd.global.Verbosef("Mountpoint %s doesn't exist, creating it\n", mountpoint) + Verbosef("Mountpoint %s doesn't exist, creating it\n", mountpoint) err = resticfs.Mkdir(mountpoint, os.ModeDir|0700) if err != nil { return err @@ -68,8 +73,11 @@ func (cmd CmdMount) Mount(mountpoint string) error { return err } + Printf("Now serving the repository at %s\n", mountpoint) + Printf("Don't forget to umount after quitting!\n") + root := fs.Tree{} - root.Add("snapshots", fuse.NewSnapshotsDir(repo, cmd.Root)) + root.Add("snapshots", fuse.NewSnapshotsDir(repo, opts.OwnerRoot)) debug.Log("mount", "serving mount at %v", mountpoint) err = fs.Serve(c, &root) @@ -81,28 +89,25 @@ func (cmd CmdMount) Mount(mountpoint string) error { return c.MountError } -func (cmd CmdMount) Umount(mountpoint string) error { +func umount(mountpoint string) error { return systemFuse.Unmount(mountpoint) } -func (cmd CmdMount) Execute(args []string) error { +func runMount(opts MountOptions, gopts GlobalOptions, args []string) error { if len(args) == 0 { - return errors.Fatalf("wrong number of parameters, Usage: %s", cmd.Usage()) + return errors.Fatalf("wrong number of parameters") } mountpoint := args[0] AddCleanupHandler(func() error { debug.Log("mount", "running umount cleanup handler for mount at %v", mountpoint) - err := cmd.Umount(mountpoint) + err := umount(mountpoint) if err != nil { - cmd.global.Warnf("unable to umount (maybe already umounted?): %v\n", err) + Warnf("unable to umount (maybe already umounted?): %v\n", err) } return nil }) - cmd.global.Printf("Now serving the repository at %s\n", mountpoint) - cmd.global.Printf("Don't forget to umount after quitting!\n") - - return cmd.Mount(mountpoint) + return mount(opts, gopts, mountpoint) } diff --git a/src/cmds/restic/cmd_prune.go b/src/cmds/restic/cmd_prune.go index d59e9ea80..52f72cca9 100644 --- a/src/cmds/restic/cmd_prune.go +++ b/src/cmds/restic/cmd_prune.go @@ -10,26 +10,25 @@ import ( "restic/repository" "time" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" ) -// CmdPrune implements the 'prune' command. -type CmdPrune struct { - global *GlobalOptions +var cmdPrune = &cobra.Command{ + Use: "prune [flags]", + Short: "remove unneeded data from the repository", + Long: ` +The "prune" command checks the repository and removes data that is not +referenced and therefore not needed any more. +`, + RunE: func(cmd *cobra.Command, args []string) error { + return runPrune(globalOptions) + }, } func init() { - _, err := parser.AddCommand("prune", - "removes content from a repository", - ` -The prune command removes rendundant and unneeded data from the repository. -For removing snapshots, please see the 'forget' command, then afterwards run -'prune'. -`, - &CmdPrune{global: &globalOpts}) - if err != nil { - panic(err) - } + cmdRoot.AddCommand(cmdPrune) } // newProgressMax returns a progress that counts blobs. @@ -64,9 +63,8 @@ func newProgressMax(show bool, max uint64, description string) *restic.Progress return p } -// Execute runs the 'prune' command. -func (cmd CmdPrune) Execute(args []string) error { - repo, err := cmd.global.OpenRepository() +func runPrune(gopts GlobalOptions) error { + repo, err := OpenRepository(gopts) if err != nil { return err } @@ -92,14 +90,14 @@ func (cmd CmdPrune) Execute(args []string) error { bytes int64 } - cmd.global.Verbosef("counting files in repo\n") + Verbosef("counting files in repo\n") for _ = range repo.List(restic.DataFile, done) { stats.packs++ } - cmd.global.Verbosef("building new index for repo\n") + Verbosef("building new index for repo\n") - bar := newProgressMax(cmd.global.ShowProgress(), uint64(stats.packs), "packs") + bar := newProgressMax(!gopts.Quiet, uint64(stats.packs), "packs") idx, err := index.New(repo, bar) if err != nil { return err @@ -108,7 +106,7 @@ func (cmd CmdPrune) Execute(args []string) error { for _, pack := range idx.Packs { stats.bytes += pack.Size } - cmd.global.Verbosef("repository contains %v packs (%v blobs) with %v bytes\n", + Verbosef("repository contains %v packs (%v blobs) with %v bytes\n", len(idx.Packs), len(idx.Blobs), formatBytes(uint64(stats.bytes))) blobCount := make(map[restic.BlobHandle]int) @@ -129,9 +127,9 @@ func (cmd CmdPrune) Execute(args []string) error { } } - cmd.global.Verbosef("processed %d blobs: %d duplicate blobs, %v duplicate\n", + Verbosef("processed %d blobs: %d duplicate blobs, %v duplicate\n", stats.blobs, duplicateBlobs, formatBytes(uint64(duplicateBytes))) - cmd.global.Verbosef("load all snapshots\n") + Verbosef("load all snapshots\n") // find referenced blobs snapshots, err := restic.LoadAllSnapshots(repo) @@ -141,12 +139,12 @@ func (cmd CmdPrune) Execute(args []string) error { stats.snapshots = len(snapshots) - cmd.global.Verbosef("find data that is still in use for %d snapshots\n", stats.snapshots) + Verbosef("find data that is still in use for %d snapshots\n", stats.snapshots) usedBlobs := restic.NewBlobSet() seenBlobs := restic.NewBlobSet() - bar = newProgressMax(cmd.global.ShowProgress(), uint64(len(snapshots)), "snapshots") + bar = newProgressMax(!gopts.Quiet, uint64(len(snapshots)), "snapshots") bar.Start() for _, sn := range snapshots { debug.Log("CmdPrune.Execute", "process snapshot %v", sn.ID().Str()) @@ -161,7 +159,7 @@ func (cmd CmdPrune) Execute(args []string) error { } bar.Done() - cmd.global.Verbosef("found %d of %d data blobs still in use, removing %d blobs\n", + Verbosef("found %d of %d data blobs still in use, removing %d blobs\n", len(usedBlobs), stats.blobs, stats.blobs-len(usedBlobs)) // find packs that need a rewrite @@ -207,7 +205,7 @@ func (cmd CmdPrune) Execute(args []string) error { rewritePacks.Delete(packID) } - cmd.global.Verbosef("will delete %d packs and rewrite %d packs, this frees %s\n", + Verbosef("will delete %d packs and rewrite %d packs, this frees %s\n", len(removePacks), len(rewritePacks), formatBytes(uint64(removeBytes))) err = repository.Repack(repo, rewritePacks, usedBlobs) @@ -218,17 +216,17 @@ func (cmd CmdPrune) Execute(args []string) error { for packID := range removePacks { err = repo.Backend().Remove(restic.DataFile, packID.String()) if err != nil { - cmd.global.Warnf("unable to remove file %v from the repository\n", packID.Str()) + Warnf("unable to remove file %v from the repository\n", packID.Str()) } } - cmd.global.Verbosef("creating new index\n") + Verbosef("creating new index\n") stats.packs = 0 for _ = range repo.List(restic.DataFile, done) { stats.packs++ } - bar = newProgressMax(cmd.global.ShowProgress(), uint64(stats.packs), "packs") + bar = newProgressMax(!gopts.Quiet, uint64(stats.packs), "packs") idx, err = index.New(repo, bar) if err != nil { return err @@ -248,8 +246,8 @@ func (cmd CmdPrune) Execute(args []string) error { if err != nil { return err } - cmd.global.Verbosef("saved new index as %v\n", id.Str()) + Verbosef("saved new index as %v\n", id.Str()) - cmd.global.Verbosef("done\n") + Verbosef("done\n") return nil } diff --git a/src/cmds/restic/cmd_rebuild_index.go b/src/cmds/restic/cmd_rebuild_index.go index cb50c1054..2dfac08f8 100644 --- a/src/cmds/restic/cmd_rebuild_index.go +++ b/src/cmds/restic/cmd_rebuild_index.go @@ -1,29 +1,32 @@ package main -import "restic/repository" +import ( + "restic/repository" -type CmdRebuildIndex struct { - global *GlobalOptions + "github.com/spf13/cobra" +) - repo *repository.Repository +var cmdRebuildIndex = &cobra.Command{ + Use: "rebuild-index [flags]", + Short: "build a new index file", + Long: ` +The "rebuild-index" command creates a new index by combining the index files +into a new one. +`, + RunE: func(cmd *cobra.Command, args []string) error { + return runRebuildIndex(globalOptions) + }, } func init() { - _, err := parser.AddCommand("rebuild-index", - "rebuild the index", - "The rebuild-index command builds a new index", - &CmdRebuildIndex{global: &globalOpts}) - if err != nil { - panic(err) - } + cmdRoot.AddCommand(cmdRebuildIndex) } -func (cmd CmdRebuildIndex) Execute(args []string) error { - repo, err := cmd.global.OpenRepository() +func runRebuildIndex(gopts GlobalOptions) error { + repo, err := OpenRepository(gopts) if err != nil { return err } - cmd.repo = repo lock, err := lockRepoExclusive(repo) defer unlockRepo(lock) diff --git a/src/cmds/restic/cmd_restore.go b/src/cmds/restic/cmd_restore.go index 88099b677..b6738a3dc 100644 --- a/src/cmds/restic/cmd_restore.go +++ b/src/cmds/restic/cmd_restore.go @@ -5,55 +5,71 @@ import ( "restic/debug" "restic/errors" "restic/filter" + + "github.com/spf13/cobra" ) -type CmdRestore struct { - Exclude []string `short:"e" long:"exclude" description:"Exclude a pattern (can be specified multiple times)"` - Include []string `short:"i" long:"include" description:"Include a pattern, exclude everything else (can be specified multiple times)"` - Target string `short:"t" long:"target" description:"Directory to restore to"` - Host string `short:"h" long:"host" description:"Source Filter (for id=latest)"` - Paths []string `short:"p" long:"path" description:"Path Filter (absolute path;for id=latest) (can be specified multiple times)"` +var cmdRestore = &cobra.Command{ + Use: "restore [flags] snapshotID", + Short: "extract the data from a snapshot", + Long: ` +The "restore" command extracts the data from a snapshot from the repository to +a directory. - global *GlobalOptions +The special snapshot "latest" can be used to restore the latest snapshot in the +repository. +`, + RunE: func(cmd *cobra.Command, args []string) error { + return runRestore(restoreOptions, globalOptions, args) + }, } +// RestoreOptions collects all options for the restore command. +type RestoreOptions struct { + Exclude []string + Include []string + Target string + Host string + Paths []string +} + +var restoreOptions RestoreOptions + func init() { - _, err := parser.AddCommand("restore", - "restore a snapshot", - "The restore command restores a snapshot to a directory", - &CmdRestore{global: &globalOpts}) - if err != nil { - panic(err) - } + cmdRoot.AddCommand(cmdRestore) + + flags := cmdRestore.Flags() + flags.StringSliceVarP(&restoreOptions.Exclude, "exclude", "e", nil, "exclude a pattern (can be specified multiple times)") + flags.StringSliceVarP(&restoreOptions.Include, "include", "i", nil, "include a pattern, exclude everything else (can be specified multiple times)") + flags.StringVarP(&restoreOptions.Target, "target", "t", "", "directory to extract data to") + + flags.StringVarP(&restoreOptions.Host, "host", "h", "", `only consider snapshots for this host when the snapshot ID is "latest"`) + flags.StringSliceVarP(&restoreOptions.Paths, "path", "p", nil, `only consider snapshots which include this (absolute) path for snapshot ID "latest"`) } -func (cmd CmdRestore) Usage() string { - return "snapshot-ID" -} - -func (cmd CmdRestore) Execute(args []string) error { +func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error { if len(args) != 1 { - return errors.Fatalf("wrong number of arguments, Usage: %s", cmd.Usage()) + return errors.Fatalf("no snapshot ID specified") } - if cmd.Target == "" { + if opts.Target == "" { return errors.Fatal("please specify a directory to restore to (--target)") } - if len(cmd.Exclude) > 0 && len(cmd.Include) > 0 { + if len(opts.Exclude) > 0 && len(opts.Include) > 0 { return errors.Fatal("exclude and include patterns are mutually exclusive") } snapshotIDString := args[0] - debug.Log("restore", "restore %v to %v", snapshotIDString, cmd.Target) + debug.Log("restore", "restore %v to %v", snapshotIDString, opts.Target) - repo, err := cmd.global.OpenRepository() + repo, err := OpenRepository(gopts) if err != nil { return err } - if !cmd.global.NoLock { + if !gopts.NoLock { lock, err := lockRepo(repo) defer unlockRepo(lock) if err != nil { @@ -69,57 +85,52 @@ func (cmd CmdRestore) Execute(args []string) error { var id restic.ID if snapshotIDString == "latest" { - id, err = restic.FindLatestSnapshot(repo, cmd.Paths, cmd.Host) + id, err = restic.FindLatestSnapshot(repo, opts.Paths, opts.Host) if err != nil { - cmd.global.Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, cmd.Paths, cmd.Host) + Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, opts.Paths, opts.Host) } } else { id, err = restic.FindSnapshot(repo, snapshotIDString) if err != nil { - cmd.global.Exitf(1, "invalid id %q: %v", snapshotIDString, err) + Exitf(1, "invalid id %q: %v", snapshotIDString, err) } } res, err := restic.NewRestorer(repo, id) if err != nil { - cmd.global.Exitf(2, "creating restorer failed: %v\n", err) + Exitf(2, "creating restorer failed: %v\n", err) } res.Error = func(dir string, node *restic.Node, err error) error { - cmd.global.Warnf("error for %s: %+v\n", dir, err) + Warnf("error for %s: %+v\n", dir, err) return nil } selectExcludeFilter := func(item string, dstpath string, node *restic.Node) bool { - matched, err := filter.List(cmd.Exclude, item) + matched, err := filter.List(opts.Exclude, item) if err != nil { - cmd.global.Warnf("error for exclude pattern: %v", err) + Warnf("error for exclude pattern: %v", err) } return !matched } selectIncludeFilter := func(item string, dstpath string, node *restic.Node) bool { - matched, err := filter.List(cmd.Include, item) + matched, err := filter.List(opts.Include, item) if err != nil { - cmd.global.Warnf("error for include pattern: %v", err) + Warnf("error for include pattern: %v", err) } return matched } - if len(cmd.Exclude) > 0 { + if len(opts.Exclude) > 0 { res.SelectFilter = selectExcludeFilter - } else if len(cmd.Include) > 0 { + } else if len(opts.Include) > 0 { res.SelectFilter = selectIncludeFilter } - cmd.global.Verbosef("restoring %s to %s\n", res.Snapshot(), cmd.Target) + Verbosef("restoring %s to %s\n", res.Snapshot(), opts.Target) - err = res.RestoreTo(cmd.Target) - if err != nil { - return err - } - - return nil + return res.RestoreTo(opts.Target) } diff --git a/src/cmds/restic/cmd_snapshots.go b/src/cmds/restic/cmd_snapshots.go index a3ba7a646..c838f4438 100644 --- a/src/cmds/restic/cmd_snapshots.go +++ b/src/cmds/restic/cmd_snapshots.go @@ -2,87 +2,60 @@ package main import ( "fmt" - "io" "os" "restic/errors" "sort" - "strings" + + "github.com/spf13/cobra" "restic" ) -type Table struct { - Header string - Rows [][]interface{} - - RowFormat string +var cmdSnapshots = &cobra.Command{ + Use: "snapshots", + Short: "list all snapshots", + Long: ` +The "snapshots" command lists all snapshots stored in a repository. +`, + RunE: func(cmd *cobra.Command, args []string) error { + return runSnapshots(snapshotOptions, globalOptions, args) + }, } -func NewTable() Table { - return Table{ - Rows: [][]interface{}{}, - } +// SnapshotOptions bundle all options for the snapshots command. +type SnapshotOptions struct { + Host string + Paths []string } -func (t Table) Write(w io.Writer) error { - _, err := fmt.Fprintln(w, t.Header) - if err != nil { - return err +var snapshotOptions SnapshotOptions + +func init() { + cmdRoot.AddCommand(cmdSnapshots) + + f := cmdSnapshots.Flags() + f.StringVar(&snapshotOptions.Host, "host", "", "only print snapshots for this host") + f.StringSliceVar(&snapshotOptions.Paths, "path", []string{}, "only print snapshots for this path (can be specified multiple times)") +} + +func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) error { + if len(args) != 0 { + return errors.Fatalf("wrong number of arguments") } - _, err = fmt.Fprintln(w, strings.Repeat("-", 70)) + + repo, err := OpenRepository(gopts) if err != nil { return err } - for _, row := range t.Rows { - _, err = fmt.Fprintf(w, t.RowFormat+"\n", row...) + if !gopts.NoLock { + lock, err := lockRepo(repo) + defer unlockRepo(lock) if err != nil { return err } } - return nil -} - -const TimeFormat = "2006-01-02 15:04:05" - -type CmdSnapshots struct { - Host string `short:"h" long:"host" description:"Host Filter"` - Paths []string `short:"p" long:"path" description:"Path Filter (absolute path) (can be specified multiple times)"` - - global *GlobalOptions -} - -func init() { - _, err := parser.AddCommand("snapshots", - "show snapshots", - "The snapshots command lists all snapshots stored in a repository", - &CmdSnapshots{global: &globalOpts}) - if err != nil { - panic(err) - } -} - -func (cmd CmdSnapshots) Usage() string { - return "" -} - -func (cmd CmdSnapshots) Execute(args []string) error { - if len(args) != 0 { - return errors.Fatalf("wrong number of arguments, usage: %s", cmd.Usage()) - } - - repo, err := cmd.global.OpenRepository() - if err != nil { - return err - } - - lock, err := lockRepo(repo) - defer unlockRepo(lock) - if err != nil { - return err - } - tab := NewTable() tab.Header = fmt.Sprintf("%-8s %-19s %-10s %-10s %s", "ID", "Date", "Host", "Tags", "Directory") tab.RowFormat = "%-8s %-19s %-10s %-10s %s" @@ -98,7 +71,7 @@ func (cmd CmdSnapshots) Execute(args []string) error { continue } - if restic.SamePaths(sn.Paths, cmd.Paths) && (cmd.Host == "" || cmd.Host == sn.Hostname) { + if restic.SamePaths(sn.Paths, opts.Paths) && (opts.Host == "" || opts.Host == sn.Hostname) { pos := sort.Search(len(list), func(i int) bool { return list[i].Time.After(sn.Time) }) diff --git a/src/cmds/restic/cmd_unlock.go b/src/cmds/restic/cmd_unlock.go index fa9a4c2a3..38004ea64 100644 --- a/src/cmds/restic/cmd_unlock.go +++ b/src/cmds/restic/cmd_unlock.go @@ -1,35 +1,43 @@ package main -import "restic" +import ( + "restic" -type CmdUnlock struct { - RemoveAll bool `long:"remove-all" description:"Remove all locks, even stale ones"` + "github.com/spf13/cobra" +) - global *GlobalOptions +var unlockCmd = &cobra.Command{ + Use: "unlock", + Short: "remove locks other processes created", + Long: ` +The "unlock" command removes stale locks that have been created by other restic processes. +`, + RunE: func(cmd *cobra.Command, args []string) error { + return runUnlock(unlockOptions, globalOptions) + }, } +// UnlockOptions collects all options for the unlock command. +type UnlockOptions struct { + RemoveAll bool +} + +var unlockOptions UnlockOptions + func init() { - _, err := parser.AddCommand("unlock", - "remove locks", - "The unlock command checks for stale locks and removes them", - &CmdUnlock{global: &globalOpts}) - if err != nil { - panic(err) - } + cmdRoot.AddCommand(unlockCmd) + + unlockCmd.Flags().BoolVar(&unlockOptions.RemoveAll, "remove-all", false, "Remove all locks, even non-stale ones") } -func (cmd CmdUnlock) Usage() string { - return "[unlock-options]" -} - -func (cmd CmdUnlock) Execute(args []string) error { - repo, err := cmd.global.OpenRepository() +func runUnlock(opts UnlockOptions, gopts GlobalOptions) error { + repo, err := OpenRepository(gopts) if err != nil { return err } fn := restic.RemoveStaleLocks - if cmd.RemoveAll { + if opts.RemoveAll { fn = restic.RemoveAllLocks } @@ -38,6 +46,6 @@ func (cmd CmdUnlock) Execute(args []string) error { return err } - cmd.global.Verbosef("successfully removed locks\n") + Verbosef("successfully removed locks\n") return nil } diff --git a/src/cmds/restic/cmd_version.go b/src/cmds/restic/cmd_version.go index 28f7633ef..1bc7b9925 100644 --- a/src/cmds/restic/cmd_version.go +++ b/src/cmds/restic/cmd_version.go @@ -3,23 +3,23 @@ package main import ( "fmt" "runtime" + + "github.com/spf13/cobra" ) -type CmdVersion struct{} +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print version information", + Long: ` +The "version" command prints detailed information about the build environment +and the version of this software. +`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("restic %s\ncompiled at %s with %v on %v/%v\n", + version, compiledAt, runtime.Version(), runtime.GOOS, runtime.GOARCH) + }, +} func init() { - _, err := parser.AddCommand("version", - "display version", - "The version command displays detailed information about the version", - &CmdVersion{}) - if err != nil { - panic(err) - } -} - -func (cmd CmdVersion) Execute(args []string) error { - fmt.Printf("restic %s\ncompiled at %s with %v on %v/%v\n", - version, compiledAt, runtime.Version(), runtime.GOOS, runtime.GOARCH) - - return nil + cmdRoot.AddCommand(versionCmd) } diff --git a/src/cmds/restic/format.go b/src/cmds/restic/format.go new file mode 100644 index 000000000..68fa29fb3 --- /dev/null +++ b/src/cmds/restic/format.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" + "time" +) + +func formatBytes(c uint64) string { + b := float64(c) + + switch { + case c > 1<<40: + return fmt.Sprintf("%.3f TiB", b/(1<<40)) + case c > 1<<30: + return fmt.Sprintf("%.3f GiB", b/(1<<30)) + case c > 1<<20: + return fmt.Sprintf("%.3f MiB", b/(1<<20)) + case c > 1<<10: + return fmt.Sprintf("%.3f KiB", b/(1<<10)) + default: + return fmt.Sprintf("%dB", c) + } +} + +func formatSeconds(sec uint64) string { + hours := sec / 3600 + sec -= hours * 3600 + min := sec / 60 + sec -= min * 60 + if hours > 0 { + return fmt.Sprintf("%d:%02d:%02d", hours, min, sec) + } + + return fmt.Sprintf("%d:%02d", min, sec) +} + +func formatPercent(numerator uint64, denominator uint64) string { + if denominator == 0 { + return "" + } + + percent := 100.0 * float64(numerator) / float64(denominator) + + if percent > 100 { + percent = 100 + } + + return fmt.Sprintf("%3.2f%%", percent) +} + +func formatRate(bytes uint64, duration time.Duration) string { + sec := float64(duration) / float64(time.Second) + rate := float64(bytes) / sec / (1 << 20) + return fmt.Sprintf("%.2fMiB/s", rate) +} + +func formatDuration(d time.Duration) string { + sec := uint64(d / time.Second) + return formatSeconds(sec) +} diff --git a/src/cmds/restic/global.go b/src/cmds/restic/global.go index 92c733b06..bd60021f1 100644 --- a/src/cmds/restic/global.go +++ b/src/cmds/restic/global.go @@ -10,6 +10,8 @@ import ( "strings" "syscall" + "github.com/spf13/cobra" + "restic/backend/local" "restic/backend/rest" "restic/backend/s3" @@ -20,28 +22,48 @@ import ( "restic/errors" - "github.com/jessevdk/go-flags" "golang.org/x/crypto/ssh/terminal" ) var version = "compiled manually" var compiledAt = "unknown time" -// GlobalOptions holds all those options that can be set for every command. +func parseEnvironment(cmd *cobra.Command, args []string) { + repo := os.Getenv("RESTIC_REPOSITORY") + if repo != "" { + globalOptions.Repo = repo + } + + pw := os.Getenv("RESTIC_PASSWORD") + if pw != "" { + globalOptions.password = pw + } +} + +// GlobalOptions hold all global options for restic. type GlobalOptions struct { - Repo string `short:"r" long:"repo" description:"Repository directory to backup to/restore from"` - PasswordFile string `short:"p" long:"password-file" description:"Read the repository password from a file"` - CacheDir string ` long:"cache-dir" description:"Directory to use as a local cache"` - Quiet bool `short:"q" long:"quiet" description:"Do not output comprehensive progress report"` - NoLock bool ` long:"no-lock" description:"Do not lock the repo, this allows some operations on read-only repos."` - Options []string `short:"o" long:"option" description:"Specify options in the form 'foo.key=value'"` + Repo string + PasswordFile string + Quiet bool + NoLock bool password string stdout io.Writer stderr io.Writer } +var globalOptions = GlobalOptions{ + stdout: os.Stdout, + stderr: os.Stderr, +} + func init() { + f := cmdRoot.PersistentFlags() + f.StringVarP(&globalOptions.Repo, "repo", "r", "", "repository to backup to or restore from (default: $RESTIC_REPOSITORY)") + f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", "", "read the repository password from a file") + f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not outputcomprehensive progress report") + f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos") + restoreTerminal() } @@ -91,9 +113,6 @@ func restoreTerminal() { }) } -var globalOpts = GlobalOptions{stdout: os.Stdout, stderr: os.Stderr} -var parser = flags.NewParser(&globalOpts, flags.HelpFlag|flags.PassDoubleDash) - // ClearLine creates a platform dependent string to clear the current // line, so it can be overwritten. ANSI sequences are not supported on // current windows cmd shell. @@ -109,8 +128,8 @@ func ClearLine() string { } // Printf writes the message to the configured stdout stream. -func (o GlobalOptions) Printf(format string, args ...interface{}) { - _, err := fmt.Fprintf(o.stdout, format, args...) +func Printf(format string, args ...interface{}) { + _, err := fmt.Fprintf(globalOptions.stdout, format, args...) if err != nil { fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err) os.Exit(100) @@ -118,22 +137,12 @@ func (o GlobalOptions) Printf(format string, args ...interface{}) { } // Verbosef calls Printf to write the message when the verbose flag is set. -func (o GlobalOptions) Verbosef(format string, args ...interface{}) { - if o.Quiet { +func Verbosef(format string, args ...interface{}) { + if globalOptions.Quiet { return } - o.Printf(format, args...) -} - -// ShowProgress returns true iff the progress status should be written, i.e. -// the quiet flag is not set. -func (o GlobalOptions) ShowProgress() bool { - if o.Quiet { - return false - } - - return true + Printf(format, args...) } // PrintProgress wraps fmt.Printf to handle the difference in writing progress @@ -162,8 +171,8 @@ func PrintProgress(format string, args ...interface{}) { } // Warnf writes the message to the configured stderr stream. -func (o GlobalOptions) Warnf(format string, args ...interface{}) { - _, err := fmt.Fprintf(o.stderr, format, args...) +func Warnf(format string, args ...interface{}) { + _, err := fmt.Fprintf(globalOptions.stderr, format, args...) if err != nil { fmt.Fprintf(os.Stderr, "unable to write to stderr: %v\n", err) os.Exit(100) @@ -171,12 +180,12 @@ func (o GlobalOptions) Warnf(format string, args ...interface{}) { } // Exitf uses Warnf to write the message and then calls os.Exit(exitcode). -func (o GlobalOptions) Exitf(exitcode int, format string, args ...interface{}) { +func Exitf(exitcode int, format string, args ...interface{}) { if format[len(format)-1] != '\n' { format += "\n" } - o.Warnf(format, args...) + Warnf(format, args...) os.Exit(exitcode) } @@ -210,9 +219,9 @@ func readPasswordTerminal(in *os.File, out io.Writer, prompt string) (password s // ReadPassword reads the password from a password file, the environment // variable RESTIC_PASSWORD or prompts the user. -func (o GlobalOptions) ReadPassword(prompt string) (string, error) { - if o.PasswordFile != "" { - s, err := ioutil.ReadFile(o.PasswordFile) +func ReadPassword(opts GlobalOptions, prompt string) (string, error) { + if opts.PasswordFile != "" { + s, err := ioutil.ReadFile(opts.PasswordFile) return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile") } @@ -244,12 +253,12 @@ func (o GlobalOptions) ReadPassword(prompt string) (string, error) { // ReadPasswordTwice calls ReadPassword two times and returns an error when the // passwords don't match. -func (o GlobalOptions) ReadPasswordTwice(prompt1, prompt2 string) (string, error) { - pw1, err := o.ReadPassword(prompt1) +func ReadPasswordTwice(gopts GlobalOptions, prompt1, prompt2 string) (string, error) { + pw1, err := ReadPassword(gopts, prompt1) if err != nil { return "", err } - pw2, err := o.ReadPassword(prompt2) + pw2, err := ReadPassword(gopts, prompt2) if err != nil { return "", err } @@ -264,26 +273,26 @@ func (o GlobalOptions) ReadPasswordTwice(prompt1, prompt2 string) (string, error const maxKeys = 20 // OpenRepository reads the password and opens the repository. -func (o GlobalOptions) OpenRepository() (*repository.Repository, error) { - if o.Repo == "" { +func OpenRepository(opts GlobalOptions) (*repository.Repository, error) { + if opts.Repo == "" { return nil, errors.Fatal("Please specify repository location (-r)") } - be, err := open(o.Repo) + be, err := open(opts.Repo) if err != nil { return nil, err } s := repository.New(be) - if o.password == "" { - o.password, err = o.ReadPassword("enter password for repository: ") + if opts.password == "" { + opts.password, err = ReadPassword(opts, "enter password for repository: ") if err != nil { return nil, err } } - err = s.SearchKey(o.password, maxKeys) + err = s.SearchKey(opts.password, maxKeys) if err != nil { return nil, errors.Fatalf("unable to open repo: %v", err) } diff --git a/src/cmds/restic/integration_fuse_test.go b/src/cmds/restic/integration_fuse_test.go index 8a4aa4860..23c601858 100644 --- a/src/cmds/restic/integration_fuse_test.go +++ b/src/cmds/restic/integration_fuse_test.go @@ -1,3 +1,4 @@ +// +build ignore // +build !openbsd // +build !windows diff --git a/src/cmds/restic/integration_helpers_test.go b/src/cmds/restic/integration_helpers_test.go index cac3e3592..72fb09f44 100644 --- a/src/cmds/restic/integration_helpers_test.go +++ b/src/cmds/restic/integration_helpers_test.go @@ -166,18 +166,6 @@ type testEnvironment struct { base, cache, repo, testdata string } -func configureRestic(t testing.TB, cache, repo string) GlobalOptions { - return GlobalOptions{ - CacheDir: cache, - Repo: repo, - Quiet: true, - - password: TestPassword, - stdout: os.Stdout, - stderr: os.Stderr, - } -} - // withTestEnvironment creates a test environment and calls f with it. After f has // returned, the temporary directory is removed. func withTestEnvironment(t testing.TB, f func(*testEnvironment, GlobalOptions)) { @@ -201,7 +189,18 @@ func withTestEnvironment(t testing.TB, f func(*testEnvironment, GlobalOptions)) OK(t, os.MkdirAll(env.cache, 0700)) OK(t, os.MkdirAll(env.repo, 0700)) - f(&env, configureRestic(t, env.cache, env.repo)) + gopts := GlobalOptions{ + Repo: env.repo, + Quiet: true, + password: TestPassword, + stdout: os.Stdout, + stderr: os.Stderr, + } + + // always overwrite global options + globalOptions = gopts + + f(&env, gopts) if !TestCleanupTempDirs { t.Logf("leaving temporary directory %v used for test", tempdir) diff --git a/src/cmds/restic/integration_test.go b/src/cmds/restic/integration_test.go index 56e0fc3b2..0987a47ad 100644 --- a/src/cmds/restic/integration_test.go +++ b/src/cmds/restic/integration_test.go @@ -41,107 +41,126 @@ func parseIDsFromReader(t testing.TB, rd io.Reader) restic.IDs { return IDs } -func cmdInit(t testing.TB, global GlobalOptions) { +func testRunInit(t testing.TB, opts GlobalOptions) { repository.TestUseLowSecurityKDFParameters(t) restic.TestSetLockTimeout(t, 0) - cmd := &CmdInit{global: &global} - OK(t, cmd.Execute(nil)) - - t.Logf("repository initialized at %v", global.Repo) + OK(t, runInit(opts, nil)) + t.Logf("repository initialized at %v", opts.Repo) } -func cmdBackup(t testing.TB, global GlobalOptions, target []string, parentID *restic.ID) { - cmdBackupExcludes(t, global, target, parentID, nil) -} - -func cmdBackupExcludes(t testing.TB, global GlobalOptions, target []string, parentID *restic.ID, excludes []string) { - cmd := &CmdBackup{global: &global, Excludes: excludes} - if parentID != nil { - cmd.Parent = parentID.String() - } - +func testRunBackup(t testing.TB, target []string, opts BackupOptions, gopts GlobalOptions) { t.Logf("backing up %v", target) - - OK(t, cmd.Execute(target)) + OK(t, runBackup(opts, gopts, target)) } -func cmdList(t testing.TB, global GlobalOptions, tpe string) restic.IDs { - cmd := &CmdList{global: &global} - return executeAndParseIDs(t, cmd, tpe) -} - -func executeAndParseIDs(t testing.TB, cmd *CmdList, args ...string) restic.IDs { +func testRunList(t testing.TB, tpe string, opts GlobalOptions) restic.IDs { buf := bytes.NewBuffer(nil) - cmd.global.stdout = buf - OK(t, cmd.Execute(args)) + globalOptions.stdout = buf + defer func() { + globalOptions.stdout = os.Stdout + }() + + OK(t, runList(opts, []string{tpe})) return parseIDsFromReader(t, buf) } -func cmdRestore(t testing.TB, global GlobalOptions, dir string, snapshotID restic.ID) { - cmdRestoreExcludes(t, global, dir, snapshotID, nil) +func testRunRestore(t testing.TB, opts GlobalOptions, dir string, snapshotID restic.ID) { + testRunRestoreExcludes(t, opts, dir, snapshotID, nil) } -func cmdRestoreLatest(t testing.TB, global GlobalOptions, dir string, paths []string, host string) { - cmd := &CmdRestore{global: &global, Target: dir, Host: host, Paths: paths} - OK(t, cmd.Execute([]string{"latest"})) +func testRunRestoreLatest(t testing.TB, gopts GlobalOptions, dir string, paths []string, host string) { + opts := RestoreOptions{ + Target: dir, + Host: host, + Paths: paths, + } + + OK(t, runRestore(opts, gopts, []string{"latest"})) } -func cmdRestoreExcludes(t testing.TB, global GlobalOptions, dir string, snapshotID restic.ID, excludes []string) { - cmd := &CmdRestore{global: &global, Target: dir, Exclude: excludes} - OK(t, cmd.Execute([]string{snapshotID.String()})) +func testRunRestoreExcludes(t testing.TB, gopts GlobalOptions, dir string, snapshotID restic.ID, excludes []string) { + opts := RestoreOptions{ + Target: dir, + Exclude: excludes, + } + + OK(t, runRestore(opts, gopts, []string{snapshotID.String()})) } -func cmdRestoreIncludes(t testing.TB, global GlobalOptions, dir string, snapshotID restic.ID, includes []string) { - cmd := &CmdRestore{global: &global, Target: dir, Include: includes} - OK(t, cmd.Execute([]string{snapshotID.String()})) +func testRunRestoreIncludes(t testing.TB, gopts GlobalOptions, dir string, snapshotID restic.ID, includes []string) { + opts := RestoreOptions{ + Target: dir, + Include: includes, + } + + OK(t, runRestore(opts, gopts, []string{snapshotID.String()})) } -func cmdCheck(t testing.TB, global GlobalOptions) { - cmd := &CmdCheck{ - global: &global, +func testRunCheck(t testing.TB, gopts GlobalOptions) { + opts := CheckOptions{ ReadData: true, CheckUnused: true, } - OK(t, cmd.Execute(nil)) + OK(t, runCheck(opts, gopts, nil)) } -func cmdCheckOutput(t testing.TB, global GlobalOptions) string { +func testRunCheckOutput(gopts GlobalOptions) (string, error) { buf := bytes.NewBuffer(nil) - global.stdout = buf - cmd := &CmdCheck{global: &global, ReadData: true} - OK(t, cmd.Execute(nil)) - return string(buf.Bytes()) + + globalOptions.stdout = buf + defer func() { + globalOptions.stdout = os.Stdout + }() + + opts := CheckOptions{ + ReadData: true, + } + + err := runCheck(opts, gopts, nil) + return string(buf.Bytes()), err } -func cmdRebuildIndex(t testing.TB, global GlobalOptions) { - global.stdout = ioutil.Discard - cmd := &CmdRebuildIndex{global: &global} - OK(t, cmd.Execute(nil)) +func testRunRebuildIndex(t testing.TB, gopts GlobalOptions) { + globalOptions.stdout = ioutil.Discard + defer func() { + globalOptions.stdout = os.Stdout + }() + + OK(t, runRebuildIndex(gopts)) } -func cmdLs(t testing.TB, global GlobalOptions, snapshotID string) []string { - var buf bytes.Buffer - global.stdout = &buf +func testRunLs(t testing.TB, gopts GlobalOptions, snapshotID string) []string { + buf := bytes.NewBuffer(nil) + globalOptions.stdout = buf + quiet := globalOptions.Quiet + globalOptions.Quiet = true + defer func() { + globalOptions.stdout = os.Stdout + globalOptions.Quiet = quiet + }() - cmd := &CmdLs{global: &global} - OK(t, cmd.Execute([]string{snapshotID})) + OK(t, runLs(gopts, []string{snapshotID})) return strings.Split(string(buf.Bytes()), "\n") } -func cmdFind(t testing.TB, global GlobalOptions, pattern string) []string { - var buf bytes.Buffer - global.stdout = &buf +func testRunFind(t testing.TB, gopts GlobalOptions, pattern string) []string { + buf := bytes.NewBuffer(nil) + globalOptions.stdout = buf + defer func() { + globalOptions.stdout = os.Stdout + }() - cmd := &CmdFind{global: &global} - OK(t, cmd.Execute([]string{pattern})) + opts := FindOptions{} + + OK(t, runFind(opts, gopts, []string{pattern})) return strings.Split(string(buf.Bytes()), "\n") } func TestBackup(t *testing.T) { - withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { + withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) { datafile := filepath.Join("testdata", "backup-data.tar.gz") fd, err := os.Open(datafile) if os.IsNotExist(errors.Cause(err)) { @@ -151,22 +170,23 @@ func TestBackup(t *testing.T) { OK(t, err) OK(t, fd.Close()) - cmdInit(t, global) + testRunInit(t, gopts) SetupTarTestFixture(t, env.testdata, datafile) + opts := BackupOptions{} // first backup - cmdBackup(t, global, []string{env.testdata}, nil) - snapshotIDs := cmdList(t, global, "snapshots") + testRunBackup(t, []string{env.testdata}, opts, gopts) + snapshotIDs := testRunList(t, "snapshots", gopts) Assert(t, len(snapshotIDs) == 1, "expected one snapshot, got %v", snapshotIDs) - cmdCheck(t, global) + testRunCheck(t, gopts) stat1 := dirStats(env.repo) // second backup, implicit incremental - cmdBackup(t, global, []string{env.testdata}, nil) - snapshotIDs = cmdList(t, global, "snapshots") + testRunBackup(t, []string{env.testdata}, opts, gopts) + snapshotIDs = testRunList(t, "snapshots", gopts) Assert(t, len(snapshotIDs) == 2, "expected two snapshots, got %v", snapshotIDs) @@ -176,10 +196,11 @@ func TestBackup(t *testing.T) { } t.Logf("repository grown by %d bytes", stat2.size-stat1.size) - cmdCheck(t, global) + testRunCheck(t, gopts) // third backup, explicit incremental - cmdBackup(t, global, []string{env.testdata}, &snapshotIDs[0]) - snapshotIDs = cmdList(t, global, "snapshots") + opts.Parent = snapshotIDs[0].String() + testRunBackup(t, []string{env.testdata}, opts, gopts) + snapshotIDs = testRunList(t, "snapshots", gopts) Assert(t, len(snapshotIDs) == 3, "expected three snapshots, got %v", snapshotIDs) @@ -193,17 +214,17 @@ func TestBackup(t *testing.T) { for i, snapshotID := range snapshotIDs { restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i)) t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir) - cmdRestore(t, global, restoredir, snapshotIDs[0]) + testRunRestore(t, gopts, restoredir, snapshotIDs[0]) Assert(t, directoriesEqualContents(env.testdata, filepath.Join(restoredir, "testdata")), "directories are not equal") } - cmdCheck(t, global) + testRunCheck(t, gopts) }) } func TestBackupNonExistingFile(t *testing.T) { - withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { + withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) { datafile := filepath.Join("testdata", "backup-data.tar.gz") fd, err := os.Open(datafile) if os.IsNotExist(errors.Cause(err)) { @@ -215,9 +236,11 @@ func TestBackupNonExistingFile(t *testing.T) { SetupTarTestFixture(t, env.testdata, datafile) - cmdInit(t, global) - - global.stderr = ioutil.Discard + testRunInit(t, gopts) + globalOptions.stderr = ioutil.Discard + defer func() { + globalOptions.stderr = os.Stderr + }() p := filepath.Join(env.testdata, "0", "0") dirs := []string{ @@ -226,12 +249,15 @@ func TestBackupNonExistingFile(t *testing.T) { filepath.Join(p, "nonexisting"), filepath.Join(p, "5"), } - cmdBackup(t, global, dirs, nil) + + opts := BackupOptions{} + + testRunBackup(t, dirs, opts, gopts) }) } func TestBackupMissingFile1(t *testing.T) { - withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { + withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) { datafile := filepath.Join("testdata", "backup-data.tar.gz") fd, err := os.Open(datafile) if os.IsNotExist(errors.Cause(err)) { @@ -243,9 +269,12 @@ func TestBackupMissingFile1(t *testing.T) { SetupTarTestFixture(t, env.testdata, datafile) - cmdInit(t, global) + testRunInit(t, gopts) + globalOptions.stderr = ioutil.Discard + defer func() { + globalOptions.stderr = os.Stderr + }() - global.stderr = ioutil.Discard ranHook := false debug.Hook("pipe.walk1", func(context interface{}) { pathname := context.(string) @@ -260,8 +289,10 @@ func TestBackupMissingFile1(t *testing.T) { OK(t, os.Remove(filepath.Join(env.testdata, "0", "0", "9", "37"))) }) - cmdBackup(t, global, []string{env.testdata}, nil) - cmdCheck(t, global) + opts := BackupOptions{} + + testRunBackup(t, []string{env.testdata}, opts, gopts) + testRunCheck(t, gopts) Assert(t, ranHook, "hook did not run") debug.RemoveHook("pipe.walk1") @@ -269,7 +300,7 @@ func TestBackupMissingFile1(t *testing.T) { } func TestBackupMissingFile2(t *testing.T) { - withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { + withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) { datafile := filepath.Join("testdata", "backup-data.tar.gz") fd, err := os.Open(datafile) if os.IsNotExist(errors.Cause(err)) { @@ -281,9 +312,13 @@ func TestBackupMissingFile2(t *testing.T) { SetupTarTestFixture(t, env.testdata, datafile) - cmdInit(t, global) + testRunInit(t, gopts) + + globalOptions.stderr = ioutil.Discard + defer func() { + globalOptions.stderr = os.Stderr + }() - global.stderr = ioutil.Discard ranHook := false debug.Hook("pipe.walk2", func(context interface{}) { pathname := context.(string) @@ -298,8 +333,10 @@ func TestBackupMissingFile2(t *testing.T) { OK(t, os.Remove(filepath.Join(env.testdata, "0", "0", "9", "37"))) }) - cmdBackup(t, global, []string{env.testdata}, nil) - cmdCheck(t, global) + opts := BackupOptions{} + + testRunBackup(t, []string{env.testdata}, opts, gopts) + testRunCheck(t, gopts) Assert(t, ranHook, "hook did not run") debug.RemoveHook("pipe.walk2") @@ -307,7 +344,7 @@ func TestBackupMissingFile2(t *testing.T) { } func TestBackupDirectoryError(t *testing.T) { - withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { + withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) { datafile := filepath.Join("testdata", "backup-data.tar.gz") fd, err := os.Open(datafile) if os.IsNotExist(errors.Cause(err)) { @@ -319,9 +356,13 @@ func TestBackupDirectoryError(t *testing.T) { SetupTarTestFixture(t, env.testdata, datafile) - cmdInit(t, global) + testRunInit(t, gopts) + + globalOptions.stderr = ioutil.Discard + defer func() { + globalOptions.stderr = os.Stderr + }() - global.stderr = ioutil.Discard ranHook := false testdir := filepath.Join(env.testdata, "0", "0", "9") @@ -340,17 +381,17 @@ func TestBackupDirectoryError(t *testing.T) { OK(t, os.RemoveAll(testdir)) }) - cmdBackup(t, global, []string{filepath.Join(env.testdata, "0", "0")}, nil) - cmdCheck(t, global) + testRunBackup(t, []string{filepath.Join(env.testdata, "0", "0")}, BackupOptions{}, gopts) + testRunCheck(t, gopts) Assert(t, ranHook, "hook did not run") debug.RemoveHook("pipe.walk2") - snapshots := cmdList(t, global, "snapshots") + snapshots := testRunList(t, "snapshots", gopts) Assert(t, len(snapshots) > 0, "no snapshots found in repo (%v)", datafile) - files := cmdLs(t, global, snapshots[0].String()) + files := testRunLs(t, gopts, snapshots[0].String()) Assert(t, len(files) > 1, "snapshot is empty") }) @@ -366,8 +407,8 @@ func includes(haystack []string, needle string) bool { return false } -func loadSnapshotMap(t testing.TB, global GlobalOptions) map[string]struct{} { - snapshotIDs := cmdList(t, global, "snapshots") +func loadSnapshotMap(t testing.TB, gopts GlobalOptions) map[string]struct{} { + snapshotIDs := testRunList(t, "snapshots", gopts) m := make(map[string]struct{}) for _, id := range snapshotIDs { @@ -396,8 +437,8 @@ var backupExcludeFilenames = []string{ } func TestBackupExclude(t *testing.T) { - withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { - cmdInit(t, global) + withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) { + testRunInit(t, gopts) datadir := filepath.Join(env.base, "testdata") @@ -414,21 +455,25 @@ func TestBackupExclude(t *testing.T) { snapshots := make(map[string]struct{}) - cmdBackup(t, global, []string{datadir}, nil) - snapshots, snapshotID := lastSnapshot(snapshots, loadSnapshotMap(t, global)) - files := cmdLs(t, global, snapshotID) + opts := BackupOptions{} + + testRunBackup(t, []string{datadir}, opts, gopts) + snapshots, snapshotID := lastSnapshot(snapshots, loadSnapshotMap(t, gopts)) + files := testRunLs(t, gopts, snapshotID) Assert(t, includes(files, filepath.Join("testdata", "foo.tar.gz")), "expected file %q in first snapshot, but it's not included", "foo.tar.gz") - cmdBackupExcludes(t, global, []string{datadir}, nil, []string{"*.tar.gz"}) - snapshots, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, global)) - files = cmdLs(t, global, snapshotID) + opts.Excludes = []string{"*.tar.gz"} + testRunBackup(t, []string{datadir}, opts, gopts) + snapshots, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, gopts)) + files = testRunLs(t, gopts, snapshotID) Assert(t, !includes(files, filepath.Join("testdata", "foo.tar.gz")), "expected file %q not in first snapshot, but it's included", "foo.tar.gz") - cmdBackupExcludes(t, global, []string{datadir}, nil, []string{"*.tar.gz", "private/secret"}) - snapshots, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, global)) - files = cmdLs(t, global, snapshotID) + opts.Excludes = []string{"*.tar.gz", "private/secret"} + testRunBackup(t, []string{datadir}, opts, gopts) + snapshots, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, gopts)) + files = testRunLs(t, gopts, snapshotID) Assert(t, !includes(files, filepath.Join("testdata", "foo.tar.gz")), "expected file %q not in first snapshot, but it's included", "foo.tar.gz") Assert(t, !includes(files, filepath.Join("testdata", "private", "secret", "passwords.txt")), @@ -465,22 +510,24 @@ func appendRandomData(filename string, bytes uint) error { } func TestIncrementalBackup(t *testing.T) { - withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { - cmdInit(t, global) + withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) { + testRunInit(t, gopts) datadir := filepath.Join(env.base, "testdata") testfile := filepath.Join(datadir, "testfile") OK(t, appendRandomData(testfile, incrementalFirstWrite)) - cmdBackup(t, global, []string{datadir}, nil) - cmdCheck(t, global) + opts := BackupOptions{} + + testRunBackup(t, []string{datadir}, opts, gopts) + testRunCheck(t, gopts) stat1 := dirStats(env.repo) OK(t, appendRandomData(testfile, incrementalSecondWrite)) - cmdBackup(t, global, []string{datadir}, nil) - cmdCheck(t, global) + testRunBackup(t, []string{datadir}, opts, gopts) + testRunCheck(t, gopts) stat2 := dirStats(env.repo) if stat2.size-stat1.size > incrementalFirstWrite { t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite) @@ -489,8 +536,8 @@ func TestIncrementalBackup(t *testing.T) { OK(t, appendRandomData(testfile, incrementalThirdWrite)) - cmdBackup(t, global, []string{datadir}, nil) - cmdCheck(t, global) + testRunBackup(t, []string{datadir}, opts, gopts) + testRunCheck(t, gopts) stat3 := dirStats(env.repo) if stat3.size-stat2.size > incrementalFirstWrite { t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite) @@ -499,24 +546,17 @@ func TestIncrementalBackup(t *testing.T) { }) } -func cmdKey(t testing.TB, global GlobalOptions, args ...string) string { - var buf bytes.Buffer +func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string { + buf := bytes.NewBuffer(nil) - global.stdout = &buf - cmd := &CmdKey{global: &global} - OK(t, cmd.Execute(args)) + globalOptions.stdout = buf + defer func() { + globalOptions.stdout = os.Stdout + }() - return buf.String() -} + OK(t, runKey(gopts, []string{"list"})) -func cmdKeyListOtherIDs(t testing.TB, global GlobalOptions) []string { - var buf bytes.Buffer - - global.stdout = &buf - cmd := &CmdKey{global: &global} - OK(t, cmd.Execute([]string{"list"})) - - scanner := bufio.NewScanner(&buf) + scanner := bufio.NewScanner(buf) exp := regexp.MustCompile(`^ ([a-f0-9]+) `) IDs := []string{} @@ -529,21 +569,28 @@ func cmdKeyListOtherIDs(t testing.TB, global GlobalOptions) []string { return IDs } -func cmdKeyAddNewKey(t testing.TB, global GlobalOptions, newPassword string) { - cmd := &CmdKey{global: &global, newPassword: newPassword} - OK(t, cmd.Execute([]string{"add"})) +func testRunKeyAddNewKey(t testing.TB, newPassword string, gopts GlobalOptions) { + testKeyNewPassword = newPassword + defer func() { + testKeyNewPassword = "" + }() + + OK(t, runKey(gopts, []string{"add"})) } -func cmdKeyPasswd(t testing.TB, global GlobalOptions, newPassword string) { - cmd := &CmdKey{global: &global, newPassword: newPassword} - OK(t, cmd.Execute([]string{"passwd"})) +func testRunKeyPasswd(t testing.TB, newPassword string, gopts GlobalOptions) { + testKeyNewPassword = newPassword + defer func() { + testKeyNewPassword = "" + }() + + OK(t, runKey(gopts, []string{"passwd"})) } -func cmdKeyRemove(t testing.TB, global GlobalOptions, IDs []string) { - cmd := &CmdKey{global: &global} +func testRunKeyRemove(t testing.TB, gopts GlobalOptions, IDs []string) { t.Logf("remove %d keys: %q\n", len(IDs), IDs) for _, id := range IDs { - OK(t, cmd.Execute([]string{"rm", id})) + OK(t, runKey(gopts, []string{"rm", id})) } } @@ -553,25 +600,24 @@ func TestKeyAddRemove(t *testing.T) { "raicneirvOjEfEigonOmLasOd", } - withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { - cmdInit(t, global) + withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) { + testRunInit(t, gopts) - cmdKeyPasswd(t, global, "geheim2") - global.password = "geheim2" - t.Logf("changed password to %q", global.password) + testRunKeyPasswd(t, "geheim2", gopts) + gopts.password = "geheim2" + t.Logf("changed password to %q", gopts.password) for _, newPassword := range passwordList { - cmdKeyAddNewKey(t, global, newPassword) + testRunKeyAddNewKey(t, newPassword, gopts) t.Logf("added new password %q", newPassword) - global.password = newPassword - cmdKeyRemove(t, global, cmdKeyListOtherIDs(t, global)) + gopts.password = newPassword + testRunKeyRemove(t, gopts, testRunKeyListOtherIDs(t, gopts)) } - global.password = passwordList[len(passwordList)-1] - t.Logf("testing access with last password %q\n", global.password) - cmdKey(t, global, "list") - - cmdCheck(t, global) + gopts.password = passwordList[len(passwordList)-1] + t.Logf("testing access with last password %q\n", gopts.password) + OK(t, runKey(gopts, []string{"list"})) + testRunCheck(t, gopts) }) } @@ -599,8 +645,8 @@ func TestRestoreFilter(t *testing.T) { {"subdir1/subdir2/testfile4.c", 102}, } - withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { - cmdInit(t, global) + withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) { + testRunInit(t, gopts) for _, test := range testfiles { p := filepath.Join(env.testdata, test.name) @@ -608,20 +654,22 @@ func TestRestoreFilter(t *testing.T) { OK(t, appendRandomData(p, test.size)) } - cmdBackup(t, global, []string{env.testdata}, nil) - cmdCheck(t, global) + opts := BackupOptions{} - snapshotID := cmdList(t, global, "snapshots")[0] + testRunBackup(t, []string{env.testdata}, opts, gopts) + testRunCheck(t, gopts) + + snapshotID := testRunList(t, "snapshots", gopts)[0] // no restore filter should restore all files - cmdRestore(t, global, filepath.Join(env.base, "restore0"), snapshotID) + testRunRestore(t, gopts, filepath.Join(env.base, "restore0"), snapshotID) for _, test := range testfiles { OK(t, testFileSize(filepath.Join(env.base, "restore0", "testdata", test.name), int64(test.size))) } for i, pat := range []string{"*.c", "*.exe", "*", "*file3*"} { base := filepath.Join(env.base, fmt.Sprintf("restore%d", i+1)) - cmdRestoreExcludes(t, global, base, snapshotID, []string{pat}) + testRunRestoreExcludes(t, gopts, base, snapshotID, []string{pat}) for _, test := range testfiles { err := testFileSize(filepath.Join(base, "testdata", test.name), int64(test.size)) if ok, _ := filter.Match(pat, filepath.Base(test.name)); !ok { @@ -638,49 +686,51 @@ func TestRestoreFilter(t *testing.T) { func TestRestoreLatest(t *testing.T) { - withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { - cmdInit(t, global) + withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) { + testRunInit(t, gopts) p := filepath.Join(env.testdata, "testfile.c") OK(t, os.MkdirAll(filepath.Dir(p), 0755)) OK(t, appendRandomData(p, 100)) - cmdBackup(t, global, []string{env.testdata}, nil) - cmdCheck(t, global) + opts := BackupOptions{} + + testRunBackup(t, []string{env.testdata}, opts, gopts) + testRunCheck(t, gopts) os.Remove(p) OK(t, appendRandomData(p, 101)) - cmdBackup(t, global, []string{env.testdata}, nil) - cmdCheck(t, global) + testRunBackup(t, []string{env.testdata}, opts, gopts) + testRunCheck(t, gopts) // Restore latest without any filters - cmdRestoreLatest(t, global, filepath.Join(env.base, "restore0"), nil, "") + testRunRestoreLatest(t, gopts, filepath.Join(env.base, "restore0"), nil, "") 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") OK(t, os.MkdirAll(filepath.Dir(p1), 0755)) OK(t, appendRandomData(p1, 102)) - cmdBackup(t, global, []string{filepath.Dir(p1)}, nil) - cmdCheck(t, global) + testRunBackup(t, []string{filepath.Dir(p1)}, opts, gopts) + testRunCheck(t, gopts) p2 := filepath.Join(env.testdata, "p2/testfile.c") OK(t, os.MkdirAll(filepath.Dir(p2), 0755)) OK(t, appendRandomData(p2, 103)) - cmdBackup(t, global, []string{filepath.Dir(p2)}, nil) - cmdCheck(t, global) + testRunBackup(t, []string{filepath.Dir(p2)}, opts, gopts) + testRunCheck(t, gopts) p1rAbs := filepath.Join(env.base, "restore1", "p1/testfile.c") p2rAbs := filepath.Join(env.base, "restore2", "p2/testfile.c") - cmdRestoreLatest(t, global, filepath.Join(env.base, "restore1"), []string{filepath.Dir(p1)}, "") + testRunRestoreLatest(t, gopts, filepath.Join(env.base, "restore1"), []string{filepath.Dir(p1)}, "") OK(t, testFileSize(p1rAbs, int64(102))) if _, err := os.Stat(p2rAbs); os.IsNotExist(errors.Cause(err)) { Assert(t, os.IsNotExist(errors.Cause(err)), "expected %v to not exist in restore, but it exists, err %v", p2rAbs, err) } - cmdRestoreLatest(t, global, filepath.Join(env.base, "restore2"), []string{filepath.Dir(p2)}, "") + testRunRestoreLatest(t, gopts, filepath.Join(env.base, "restore2"), []string{filepath.Dir(p2)}, "") OK(t, testFileSize(p2rAbs, int64(103))) if _, err := os.Stat(p1rAbs); os.IsNotExist(errors.Cause(err)) { Assert(t, os.IsNotExist(errors.Cause(err)), @@ -691,20 +741,24 @@ func TestRestoreLatest(t *testing.T) { } func TestRestoreWithPermissionFailure(t *testing.T) { - withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { + withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) { datafile := filepath.Join("testdata", "repo-restore-permissions-test.tar.gz") SetupTarTestFixture(t, env.base, datafile) - snapshots := cmdList(t, global, "snapshots") + snapshots := testRunList(t, "snapshots", gopts) Assert(t, len(snapshots) > 0, "no snapshots found in repo (%v)", datafile) - global.stderr = ioutil.Discard - cmdRestore(t, global, filepath.Join(env.base, "restore"), snapshots[0]) + globalOptions.stderr = ioutil.Discard + defer func() { + globalOptions.stderr = os.Stderr + }() + + testRunRestore(t, gopts, filepath.Join(env.base, "restore"), snapshots[0]) // make sure that all files have been restored, regardeless of any // permission errors - files := cmdLs(t, global, snapshots[0].String()) + files := testRunLs(t, gopts, snapshots[0].String()) for _, filename := range files { fi, err := os.Lstat(filepath.Join(env.base, "restore", filename)) OK(t, err) @@ -725,23 +779,25 @@ func setZeroModTime(filename string) error { } func TestRestoreNoMetadataOnIgnoredIntermediateDirs(t *testing.T) { - withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { - cmdInit(t, global) + withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) { + testRunInit(t, gopts) p := filepath.Join(env.testdata, "subdir1", "subdir2", "subdir3", "file.ext") OK(t, os.MkdirAll(filepath.Dir(p), 0755)) OK(t, appendRandomData(p, 200)) OK(t, setZeroModTime(filepath.Join(env.testdata, "subdir1", "subdir2"))) - cmdBackup(t, global, []string{env.testdata}, nil) - cmdCheck(t, global) + opts := BackupOptions{} - snapshotID := cmdList(t, global, "snapshots")[0] + testRunBackup(t, []string{env.testdata}, opts, gopts) + testRunCheck(t, gopts) + + snapshotID := testRunList(t, "snapshots", gopts)[0] // restore with filter "*.ext", this should restore "file.ext", but // since the directories are ignored and only created because of // "file.ext", no meta data should be restored for them. - cmdRestoreIncludes(t, global, filepath.Join(env.base, "restore0"), snapshotID, []string{"*.ext"}) + testRunRestoreIncludes(t, gopts, filepath.Join(env.base, "restore0"), snapshotID, []string{"*.ext"}) f1 := filepath.Join(env.base, "restore0", "testdata", "subdir1", "subdir2") fi, err := os.Stat(f1) @@ -751,7 +807,7 @@ func TestRestoreNoMetadataOnIgnoredIntermediateDirs(t *testing.T) { "meta data of intermediate directory has been restore although it was ignored") // restore with filter "*", this should restore meta data on everything. - cmdRestoreIncludes(t, global, filepath.Join(env.base, "restore1"), snapshotID, []string{"*"}) + testRunRestoreIncludes(t, gopts, filepath.Join(env.base, "restore1"), snapshotID, []string{"*"}) f2 := filepath.Join(env.base, "restore1", "testdata", "subdir1", "subdir2") fi, err = os.Stat(f2) @@ -763,44 +819,55 @@ func TestRestoreNoMetadataOnIgnoredIntermediateDirs(t *testing.T) { } func TestFind(t *testing.T) { - withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { + withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) { datafile := filepath.Join("testdata", "backup-data.tar.gz") - cmdInit(t, global) + testRunInit(t, gopts) SetupTarTestFixture(t, env.testdata, datafile) - cmdBackup(t, global, []string{env.testdata}, nil) - cmdCheck(t, global) - results := cmdFind(t, global, "unexistingfile") + opts := BackupOptions{} + + testRunBackup(t, []string{env.testdata}, opts, gopts) + testRunCheck(t, gopts) + + results := testRunFind(t, gopts, "unexistingfile") Assert(t, len(results) != 0, "unexisting file found in repo (%v)", datafile) - results = cmdFind(t, global, "testfile") + results = testRunFind(t, gopts, "testfile") Assert(t, len(results) != 1, "file not found in repo (%v)", datafile) - results = cmdFind(t, global, "test") + results = testRunFind(t, gopts, "test") Assert(t, len(results) < 2, "less than two file found in repo (%v)", datafile) }) } func TestRebuildIndex(t *testing.T) { - withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { + withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) { datafile := filepath.Join("..", "..", "restic", "checker", "testdata", "duplicate-packs-in-index-test-repo.tar.gz") SetupTarTestFixture(t, env.base, datafile) - out := cmdCheckOutput(t, global) + out, err := testRunCheckOutput(gopts) if !strings.Contains(out, "contained in several indexes") { t.Fatalf("did not find checker hint for packs in several indexes") } + if err != nil { + t.Fatalf("expected no error from checker for test repository, got %v", err) + } + if !strings.Contains(out, "restic rebuild-index") { t.Fatalf("did not find hint for rebuild-index comman") } - cmdRebuildIndex(t, global) + testRunRebuildIndex(t, gopts) - out = cmdCheckOutput(t, global) + out, err = testRunCheckOutput(gopts) if len(out) != 0 { t.Fatalf("expected no output from the checker, got: %v", out) } + + if err != nil { + t.Fatalf("expected no error from checker after rebuild-index, got: %v", err) + } }) } @@ -810,7 +877,7 @@ func TestRebuildIndexAlwaysFull(t *testing.T) { } func TestCheckRestoreNoLock(t *testing.T) { - withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { + withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) { datafile := filepath.Join("testdata", "small-repo.tar.gz") SetupTarTestFixture(t, env.base, datafile) @@ -822,14 +889,15 @@ func TestCheckRestoreNoLock(t *testing.T) { }) OK(t, err) - global.NoLock = true - cmdCheck(t, global) + gopts.NoLock = true - snapshotIDs := cmdList(t, global, "snapshots") + testRunCheck(t, gopts) + + snapshotIDs := testRunList(t, "snapshots", gopts) if len(snapshotIDs) == 0 { t.Fatalf("found no snapshots") } - cmdRestore(t, global, filepath.Join(env.base, "restore"), snapshotIDs[0]) + testRunRestore(t, gopts, filepath.Join(env.base, "restore"), snapshotIDs[0]) }) } diff --git a/src/cmds/restic/main.go b/src/cmds/restic/main.go index c28f51eeb..b1782695e 100644 --- a/src/cmds/restic/main.go +++ b/src/cmds/restic/main.go @@ -7,11 +7,24 @@ import ( "restic/debug" "runtime" - "restic/errors" + "github.com/spf13/cobra" - "github.com/jessevdk/go-flags" + "restic/errors" ) +// cmdRoot is the base command when no other command has been specified. +var cmdRoot = &cobra.Command{ + Use: "restic", + Short: "backup and restore files", + Long: ` +restic is a backup program which allows saving multiple revisions of files and +directories in an encrypted repository stored on different backends. +`, + SilenceErrors: true, + SilenceUsage: true, + PersistentPreRun: parseEnvironment, +} + func init() { // set GOMAXPROCS to number of CPUs if runtime.Version() < "go1.5" { @@ -21,23 +34,11 @@ func init() { runtime.GOMAXPROCS(runtime.NumCPU()) } } - } func main() { - // defer profile.Start(profile.MemProfileRate(100000), profile.ProfilePath(".")).Stop() - // defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop() - globalOpts.Repo = os.Getenv("RESTIC_REPOSITORY") - debug.Log("restic", "main %#v", os.Args) - - _, err := parser.Parse() - if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp { - parser.WriteHelp(os.Stdout) - os.Exit(0) - } - - debug.Log("main", "command returned error: %#v", err) + err := cmdRoot.Execute() switch { case restic.IsAlreadyLocked(errors.Cause(err)): diff --git a/src/cmds/restic/table.go b/src/cmds/restic/table.go new file mode 100644 index 000000000..00da32198 --- /dev/null +++ b/src/cmds/restic/table.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "io" + "strings" +) + +type Table struct { + Header string + Rows [][]interface{} + + RowFormat string +} + +func NewTable() Table { + return Table{ + Rows: [][]interface{}{}, + } +} + +func (t Table) Write(w io.Writer) error { + _, err := fmt.Fprintln(w, t.Header) + if err != nil { + return err + } + _, err = fmt.Fprintln(w, strings.Repeat("-", 70)) + if err != nil { + return err + } + + for _, row := range t.Rows { + _, err = fmt.Fprintf(w, t.RowFormat+"\n", row...) + if err != nil { + return err + } + } + + return nil +} + +const TimeFormat = "2006-01-02 15:04:05" diff --git a/src/restic/snapshot.go b/src/restic/snapshot.go index c8fc12079..3bc0c8732 100644 --- a/src/restic/snapshot.go +++ b/src/restic/snapshot.go @@ -122,7 +122,7 @@ nextTag: // SamePaths compares the Snapshot's paths and provided paths are exactly the same func SamePaths(expected, actual []string) bool { - if expected == nil || actual == nil { + if len(expected) == 0 || len(actual) == 0 { return true } diff --git a/src/restic/snapshot_filter.go b/src/restic/snapshot_filter.go index d616468ea..707a68e09 100644 --- a/src/restic/snapshot_filter.go +++ b/src/restic/snapshot_filter.go @@ -82,7 +82,7 @@ func (e ExpirePolicy) Empty() bool { return false } - empty := ExpirePolicy{} + empty := ExpirePolicy{Tags: e.Tags} return reflect.DeepEqual(e, empty) }