2
2
mirror of https://github.com/octoleo/restic.git synced 2024-11-27 07:16:40 +00:00

Use cobra for all commands

This commit is contained in:
Alexander Neumann 2016-09-17 12:36:05 +02:00
parent 3806623c23
commit 565d72ef36
26 changed files with 1071 additions and 899 deletions

View File

@ -6,101 +6,66 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"restic" "restic"
"restic/archiver"
"restic/debug"
"restic/filter"
"restic/fs"
"strings" "strings"
"time" "time"
"restic/errors"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
"github.com/spf13/cobra"
"restic/archiver"
"restic/debug"
"restic/errors"
"restic/filter"
"restic/fs"
) )
type CmdBackup struct { var cmdBackup = &cobra.Command{
Parent string `short:"p" long:"parent" description:"use this parent snapshot (default: last snapshot in repo that has the same target)"` Use: "backup [flags] FILE/DIR [FILE/DIR] ...",
Force bool `short:"f" long:"force" description:"Force re-reading the target. Overrides the \"parent\" flag"` Short: "create a new backup of files and/or directories",
Excludes []string `short:"e" long:"exclude" description:"Exclude a pattern (can be specified multiple times)"` Long: `
ExcludeOtherFS bool `short:"x" long:"one-file-system" description:"Exclude other file systems"` The "backup" command creates a new snapshot and saves the files and directories
ExcludeFile string `long:"exclude-file" description:"Read exclude-patterns from file"` given as the arguments.
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"` RunE: func(cmd *cobra.Command, args []string) error {
Tags []string `long:"tag" description:"Add a tag (can be specified multiple times)"` 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() { func init() {
_, err := parser.AddCommand("backup", cmdRoot.AddCommand(cmdBackup)
"save file/directory",
"The backup command creates a snapshot of a file or directory", f := cmdBackup.Flags()
&CmdBackup{global: &globalOpts}) f.StringVar(&backupOptions.Parent, "parent", "", "use this parent snapshot (default: last snapshot in the repo that has the same target files/directories)")
if err != nil { f.BoolVarP(&backupOptions.Force, "force", "f", false, `force re-reading the target files/directories. Overrides the "parent" flag`)
panic(err) 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 { func newScanProgress(gopts GlobalOptions) *restic.Progress {
b := float64(c) if gopts.Quiet {
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() {
return nil return nil
} }
@ -115,8 +80,8 @@ func (cmd CmdBackup) newScanProgress() *restic.Progress {
return p return p
} }
func (cmd CmdBackup) newArchiveProgress(todo restic.Stat) *restic.Progress { func newArchiveProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress {
if !cmd.global.ShowProgress() { if gopts.Quiet {
return nil return nil
} }
@ -169,8 +134,8 @@ func (cmd CmdBackup) newArchiveProgress(todo restic.Stat) *restic.Progress {
return archiveProgress return archiveProgress
} }
func (cmd CmdBackup) newArchiveStdinProgress() *restic.Progress { func newArchiveStdinProgress(gopts GlobalOptions) *restic.Progress {
if !cmd.global.ShowProgress() { if gopts.Quiet {
return nil return nil
} }
@ -250,12 +215,12 @@ func gatherDevices(items []string) (deviceMap map[uint64]struct{}, err error) {
return deviceMap, nil return deviceMap, nil
} }
func (cmd CmdBackup) readFromStdin(args []string) error { func readBackupFromStdin(opts BackupOptions, gopts GlobalOptions, args []string) error {
if len(args) != 0 { if len(args) != 0 {
return errors.Fatalf("when reading from stdin, no additional files can be specified") 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 { if err != nil {
return err return err
} }
@ -271,7 +236,7 @@ func (cmd CmdBackup) readFromStdin(args []string) error {
return err 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 { if err != nil {
return err return err
} }
@ -280,13 +245,9 @@ func (cmd CmdBackup) readFromStdin(args []string) error {
return nil return nil
} }
func (cmd CmdBackup) Execute(args []string) error { func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error {
if cmd.Stdin {
return cmd.readFromStdin(args)
}
if len(args) == 0 { 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)) target := make([]string, 0, len(args))
@ -304,7 +265,7 @@ func (cmd CmdBackup) Execute(args []string) error {
// allowed devices // allowed devices
var allowedDevs map[uint64]struct{} var allowedDevs map[uint64]struct{}
if cmd.ExcludeOtherFS { if opts.ExcludeOtherFS {
allowedDevs, err = gatherDevices(target) allowedDevs, err = gatherDevices(target)
if err != nil { if err != nil {
return err return err
@ -312,7 +273,7 @@ func (cmd CmdBackup) Execute(args []string) error {
debug.Log("backup.Execute", "allowed devices: %v\n", allowedDevs) debug.Log("backup.Execute", "allowed devices: %v\n", allowedDevs)
} }
repo, err := cmd.global.OpenRepository() repo, err := OpenRepository(gopts)
if err != nil { if err != nil {
return err return err
} }
@ -331,17 +292,17 @@ func (cmd CmdBackup) Execute(args []string) error {
var parentSnapshotID *restic.ID var parentSnapshotID *restic.ID
// Force using a parent // Force using a parent
if !cmd.Force && cmd.Parent != "" { if !opts.Force && opts.Parent != "" {
id, err := restic.FindSnapshot(repo, cmd.Parent) id, err := restic.FindSnapshot(repo, opts.Parent)
if err != nil { 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 parentSnapshotID = &id
} }
// Find last snapshot to set it as parent, if not already set // 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, "") id, err := restic.FindLatestSnapshot(repo, target, "")
if err == nil { if err == nil {
parentSnapshotID = &id parentSnapshotID = &id
@ -351,16 +312,16 @@ func (cmd CmdBackup) Execute(args []string) error {
} }
if parentSnapshotID != nil { 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 // add patterns from file
if cmd.ExcludeFile != "" { if opts.ExcludeFile != "" {
file, err := fs.Open(cmd.ExcludeFile) file, err := fs.Open(opts.ExcludeFile)
if err != nil { if err != nil {
cmd.global.Warnf("error reading exclude patterns: %v", err) Warnf("error reading exclude patterns: %v", err)
return nil return nil
} }
@ -369,15 +330,15 @@ func (cmd CmdBackup) Execute(args []string) error {
line := scanner.Text() line := scanner.Text()
if !strings.HasPrefix(line, "#") { if !strings.HasPrefix(line, "#") {
line = os.ExpandEnv(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 { 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 { if err != nil {
cmd.global.Warnf("error for exclude pattern: %v", err) Warnf("error for exclude pattern: %v", err)
} }
if matched { if matched {
@ -385,7 +346,7 @@ func (cmd CmdBackup) Execute(args []string) error {
return false return false
} }
if !cmd.ExcludeOtherFS { if !opts.ExcludeOtherFS {
return true return true
} }
@ -404,27 +365,27 @@ func (cmd CmdBackup) Execute(args []string) error {
return true return true
} }
stat, err := archiver.Scan(target, selectFilter, cmd.newScanProgress()) stat, err := archiver.Scan(target, selectFilter, newScanProgress(gopts))
if err != nil { if err != nil {
return err return err
} }
arch := archiver.New(repo) arch := archiver.New(repo)
arch.Excludes = cmd.Excludes arch.Excludes = opts.Excludes
arch.SelectFilter = selectFilter arch.SelectFilter = selectFilter
arch.Error = func(dir string, fi os.FileInfo, err error) error { arch.Error = func(dir string, fi os.FileInfo, err error) error {
// TODO: make ignoring errors configurable // 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 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 { if err != nil {
return err return err
} }
cmd.global.Verbosef("snapshot %s saved\n", id.Str()) Verbosef("snapshot %s saved\n", id.Str())
return nil return nil
} }

View File

@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/spf13/cobra"
"restic" "restic"
"restic/backend" "restic/backend"
"restic/debug" "restic/debug"
@ -12,30 +14,27 @@ import (
"restic/repository" "restic/repository"
) )
type CmdCat struct { var cmdCat = &cobra.Command{
global *GlobalOptions 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() { func init() {
_, err := parser.AddCommand("cat", cmdRoot.AddCommand(cmdCat)
"dump something",
"The cat command dumps data structures or data from a repository",
&CmdCat{global: &globalOpts})
if err != nil {
panic(err)
}
} }
func (cmd CmdCat) Usage() string { func runCat(gopts GlobalOptions, args []string) error {
return "[pack|blob|tree|snapshot|key|masterkey|config|lock] ID"
}
func (cmd CmdCat) Execute(args []string) error {
if len(args) < 1 || (args[0] != "masterkey" && args[0] != "config" && len(args) != 2) { 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 { if err != nil {
return err return err
} }
@ -158,7 +157,7 @@ func (cmd CmdCat) Execute(args []string) error {
hash := restic.Hash(buf) hash := restic.Hash(buf)
if !hash.Equal(id) { 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) _, err = os.Stdout.Write(buf)

View File

@ -5,6 +5,8 @@ import (
"os" "os"
"time" "time"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
"restic" "restic"
@ -12,29 +14,36 @@ import (
"restic/errors" "restic/errors"
) )
type CmdCheck struct { var cmdCheck = &cobra.Command{
ReadData bool `long:"read-data" description:"Read data blobs"` Use: "check [flags]",
CheckUnused bool `long:"check-unused" description:"Check for unused blobs"` Short: "check the repository for errors",
Long: `
global *GlobalOptions 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() { func init() {
_, err := parser.AddCommand("check", cmdRoot.AddCommand(cmdCheck)
"check the repository",
"The check command check the integrity and consistency of the repository", f := cmdCheck.Flags()
&CmdCheck{global: &globalOpts}) f.BoolVar(&checkOptions.ReadData, "read-data", false, "Read all data blobs")
if err != nil { f.BoolVar(&checkOptions.CheckUnused, "check-unused", false, "Find unused blobs")
panic(err)
}
} }
func (cmd CmdCheck) Usage() string { func newReadProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress {
return "[check-options]" if gopts.Quiet {
}
func (cmd CmdCheck) newReadProgress(todo restic.Stat) *restic.Progress {
if !cmd.global.ShowProgress() {
return nil return nil
} }
@ -64,18 +73,18 @@ func (cmd CmdCheck) newReadProgress(todo restic.Stat) *restic.Progress {
return readProgress return readProgress
} }
func (cmd CmdCheck) Execute(args []string) error { func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
if len(args) != 0 { if len(args) != 0 {
return errors.Fatal("check has no arguments") return errors.Fatal("check has no arguments")
} }
repo, err := cmd.global.OpenRepository() repo, err := OpenRepository(gopts)
if err != nil { if err != nil {
return err return err
} }
if !cmd.global.NoLock { if !gopts.NoLock {
cmd.global.Verbosef("Create exclusive lock for repository\n") Verbosef("Create exclusive lock for repository\n")
lock, err := lockRepoExclusive(repo) lock, err := lockRepoExclusive(repo)
defer unlockRepo(lock) defer unlockRepo(lock)
if err != nil { if err != nil {
@ -85,24 +94,24 @@ func (cmd CmdCheck) Execute(args []string) error {
chkr := checker.New(repo) chkr := checker.New(repo)
cmd.global.Verbosef("Load indexes\n") Verbosef("Load indexes\n")
hints, errs := chkr.LoadIndex() hints, errs := chkr.LoadIndex()
dupFound := false dupFound := false
for _, hint := range hints { for _, hint := range hints {
cmd.global.Printf("%v\n", hint) Printf("%v\n", hint)
if _, ok := hint.(checker.ErrDuplicatePacks); ok { if _, ok := hint.(checker.ErrDuplicatePacks); ok {
dupFound = true dupFound = true
} }
} }
if dupFound { 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 { if len(errs) > 0 {
for _, err := range errs { for _, err := range errs {
cmd.global.Warnf("error: %v\n", err) Warnf("error: %v\n", err)
} }
return errors.Fatal("LoadIndex returned errors") return errors.Fatal("LoadIndex returned errors")
} }
@ -113,7 +122,7 @@ func (cmd CmdCheck) Execute(args []string) error {
errorsFound := false errorsFound := false
errChan := make(chan error) errChan := make(chan error)
cmd.global.Verbosef("Check all packs\n") Verbosef("Check all packs\n")
go chkr.Packs(errChan, done) go chkr.Packs(errChan, done)
for err := range errChan { for err := range errChan {
@ -121,7 +130,7 @@ func (cmd CmdCheck) Execute(args []string) error {
fmt.Fprintf(os.Stderr, "%v\n", err) 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) errChan = make(chan error)
go chkr.Structure(errChan, done) 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() { for _, id := range chkr.UnusedBlobs() {
cmd.global.Verbosef("unused blob %v\n", id.Str()) Verbosef("unused blob %v\n", id.Str())
errorsFound = true errorsFound = true
} }
} }
if cmd.ReadData { if opts.ReadData {
cmd.global.Verbosef("Read all data\n") 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) errChan := make(chan error)
go chkr.ReadData(p, errChan, done) go chkr.ReadData(p, errChan, done)

View File

@ -8,6 +8,8 @@ import (
"io" "io"
"os" "os"
"github.com/spf13/cobra"
"restic" "restic"
"restic/errors" "restic/errors"
"restic/pack" "restic/pack"
@ -16,24 +18,19 @@ import (
"restic/worker" "restic/worker"
) )
type CmdDump struct { var cmdDump = &cobra.Command{
global *GlobalOptions Use: "dump [indexes|snapshots|trees|all|packs]",
Short: "dump data structures",
repo *repository.Repository 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() { func init() {
_, err := parser.AddCommand("dump", cmdRoot.AddCommand(cmdDump)
"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]"
} }
func prettyPrintJSON(wr io.Writer, item interface{}) error { func prettyPrintJSON(wr io.Writer, item interface{}) error {
@ -148,14 +145,14 @@ func printPacks(repo *repository.Repository, wr io.Writer) error {
return nil return nil
} }
func (cmd CmdDump) DumpIndexes() error { func dumpIndexes(repo restic.Repository) error {
done := make(chan struct{}) done := make(chan struct{})
defer close(done) 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) fmt.Printf("index_id: %v\n", id)
idx, err := repository.LoadIndex(cmd.repo, id) idx, err := repository.LoadIndex(repo, id)
if err != nil { if err != nil {
return err return err
} }
@ -169,21 +166,22 @@ func (cmd CmdDump) DumpIndexes() error {
return nil return nil
} }
func (cmd CmdDump) Execute(args []string) error { func runDump(gopts GlobalOptions, args []string) error {
if len(args) != 1 { 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 { if err != nil {
return err return err
} }
cmd.repo = repo
lock, err := lockRepo(repo) if !gopts.NoLock {
defer unlockRepo(lock) lock, err := lockRepo(repo)
if err != nil { defer unlockRepo(lock)
return err if err != nil {
return err
}
} }
err = repo.LoadIndex() err = repo.LoadIndex()
@ -195,7 +193,7 @@ func (cmd CmdDump) Execute(args []string) error {
switch tpe { switch tpe {
case "indexes": case "indexes":
return cmd.DumpIndexes() return dumpIndexes(repo)
case "snapshots": case "snapshots":
return debugPrintSnapshots(repo, os.Stdout) return debugPrintSnapshots(repo, os.Stdout)
case "packs": case "packs":
@ -208,7 +206,7 @@ func (cmd CmdDump) Execute(args []string) error {
} }
fmt.Printf("\nindexes:\n") fmt.Printf("\nindexes:\n")
err = cmd.DumpIndexes() err = dumpIndexes(repo)
if err != nil { if err != nil {
return err return err
} }

View File

@ -4,27 +4,53 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"github.com/spf13/cobra"
"restic" "restic"
"restic/debug" "restic/debug"
"restic/errors" "restic/errors"
"restic/repository" "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 { type findResult struct {
node *restic.Node node *restic.Node
path string 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{ var timeFormats = []string{
"2006-01-02", "2006-01-02",
"2006-01-02 15:04", "2006-01-02 15:04",
@ -39,16 +65,6 @@ var timeFormats = []string{
"Mon Jan 2 15:04:05 -0700 MST 2006", "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) { func parseTime(str string) (time.Time, error) {
for _, fmt := range timeFormats { for _, fmt := range timeFormats {
if t, err := time.ParseInLocation(fmt, str, time.Local); err == nil { 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) 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) debug.Log("restic.find", "checking tree %v\n", id)
tree, err := repo.LoadTree(id) tree, err := repo.LoadTree(id)
if err != nil { if err != nil {
@ -70,20 +86,20 @@ func (c CmdFind) findInTree(repo *repository.Repository, id restic.ID, path stri
for _, node := range tree.Nodes { for _, node := range tree.Nodes {
debug.Log("restic.find", " testing entry %q\n", node.Name) 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 { if err != nil {
return nil, err return nil, err
} }
if m { if m {
debug.Log("restic.find", " pattern matches\n") debug.Log("restic.find", " pattern matches\n")
if !c.oldest.IsZero() && node.ModTime.Before(c.oldest) { if !pat.oldest.IsZero() && node.ModTime.Before(pat.oldest) {
debug.Log("restic.find", " ModTime is older than %s\n", c.oldest) debug.Log("restic.find", " ModTime is older than %s\n", pat.oldest)
continue continue
} }
if !c.newest.IsZero() && node.ModTime.After(c.newest) { if !pat.newest.IsZero() && node.ModTime.After(pat.newest) {
debug.Log("restic.find", " ModTime is newer than %s\n", c.newest) debug.Log("restic.find", " ModTime is newer than %s\n", pat.newest)
continue continue
} }
@ -93,7 +109,7 @@ func (c CmdFind) findInTree(repo *repository.Repository, id restic.ID, path stri
} }
if node.Type == "dir" { 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 { if err != nil {
return nil, err return nil, err
} }
@ -105,15 +121,15 @@ func (c CmdFind) findInTree(repo *repository.Repository, id restic.ID, path stri
return results, nil return results, nil
} }
func (c CmdFind) findInSnapshot(repo *repository.Repository, id restic.ID) error { 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(), c.oldest, c.newest) 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) sn, err := restic.LoadSnapshot(repo, id)
if err != nil { if err != nil {
return err return err
} }
results, err := c.findInTree(repo, *sn.Tree, "") results, err := findInTree(repo, pat, *sn.Tree, "")
if err != nil { if err != nil {
return err return err
} }
@ -121,49 +137,50 @@ func (c CmdFind) findInSnapshot(repo *repository.Repository, id restic.ID) error
if len(results) == 0 { if len(results) == 0 {
return nil 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 { for _, res := range results {
res.node.Name = filepath.Join(res.path, res.node.Name) res.node.Name = filepath.Join(res.path, res.node.Name)
c.global.Printf(" %s\n", res.node) Printf(" %s\n", res.node)
} }
return nil return nil
} }
func (CmdFind) Usage() string { func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
return "[find-OPTIONS] PATTERN"
}
func (c CmdFind) Execute(args []string) error {
if len(args) != 1 { 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 != "" { if opts.Oldest != "" {
c.oldest, err = parseTime(c.Oldest) pat.oldest, err = parseTime(opts.Oldest)
if err != nil { if err != nil {
return err return err
} }
} }
if c.Newest != "" { if opts.Newest != "" {
c.newest, err = parseTime(c.Newest) pat.newest, err = parseTime(opts.Newest)
if err != nil { if err != nil {
return err return err
} }
} }
repo, err := c.global.OpenRepository() repo, err := OpenRepository(gopts)
if err != nil { if err != nil {
return err return err
} }
lock, err := lockRepo(repo) if !gopts.NoLock {
defer unlockRepo(lock) lock, err := lockRepo(repo)
if err != nil { defer unlockRepo(lock)
return err if err != nil {
return err
}
} }
err = repo.LoadIndex() err = repo.LoadIndex()
@ -171,21 +188,21 @@ func (c CmdFind) Execute(args []string) error {
return err return err
} }
c.pattern = args[0] pat.pattern = args[0]
if c.Snapshot != "" { if opts.Snapshot != "" {
snapshotID, err := restic.FindSnapshot(repo, c.Snapshot) snapshotID, err := restic.FindSnapshot(repo, opts.Snapshot)
if err != nil { if err != nil {
return errors.Fatalf("invalid id %q: %v", args[1], err) return errors.Fatalf("invalid id %q: %v", args[1], err)
} }
return c.findInSnapshot(repo, snapshotID) return findInSnapshot(repo, pat, snapshotID)
} }
done := make(chan struct{}) done := make(chan struct{})
defer close(done) defer close(done)
for snapshotID := range repo.List(restic.SnapshotFile, done) { for snapshotID := range repo.List(restic.SnapshotFile, done) {
err := c.findInSnapshot(repo, snapshotID) err := findInSnapshot(repo, pat, snapshotID)
if err != nil { if err != nil {
return err return err

View File

@ -5,46 +5,58 @@ import (
"io" "io"
"restic" "restic"
"strings" "strings"
"github.com/spf13/cobra"
) )
// CmdForget implements the 'forget' command. var cmdForget = &cobra.Command{
type CmdForget struct { Use: "forget [flags] [snapshot ID] [...]",
Last int `short:"l" long:"keep-last" description:"keep the last n snapshots"` Short: "forget removes snapshots from the repository",
Hourly int `short:"H" long:"keep-hourly" description:"keep the last n hourly snapshots"` Long: `
Daily int `short:"d" long:"keep-daily" description:"keep the last n daily snapshots"` The "forget" command removes snapshots according to a policy. Please note that
Weekly int `short:"w" long:"keep-weekly" description:"keep the last n weekly snapshots"` this command really only deletes the snapshot object in the repository, which
Monthly int `short:"m" long:"keep-monthly"description:"keep the last n monthly snapshots"` is a reference to data stored there. In order to remove this (now unreferenced)
Yearly int `short:"y" long:"keep-yearly" description:"keep the last n yearly snapshots"` data after 'forget' was run successfully, see the 'prune' command. `,
RunE: func(cmd *cobra.Command, args []string) error {
KeepTags []string `long:"keep-tag" description:"alwaps keep snapshots with this tag (can be specified multiple times)"` return runForget(forgetOptions, globalOptions, args)
},
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
} }
// 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() { func init() {
_, err := parser.AddCommand("forget", cmdRoot.AddCommand(cmdForget)
"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)
}
}
// Usage returns usage information for 'forget'. f := cmdForget.Flags()
func (cmd CmdForget) Usage() string { f.IntVarP(&forgetOptions.Last, "keep-last", "l", 0, "keep the last n snapshots")
return "[snapshot ID] ..." 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) { func printSnapshots(w io.Writer, snapshots restic.Snapshots) {
@ -87,9 +99,8 @@ func printSnapshots(w io.Writer, snapshots restic.Snapshots) {
tab.Write(w) tab.Write(w)
} }
// Execute runs the 'forget' command. func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
func (cmd CmdForget) Execute(args []string) error { repo, err := OpenRepository(gopts)
repo, err := cmd.global.OpenRepository()
if err != nil { if err != nil {
return err return err
} }
@ -112,26 +123,26 @@ func (cmd CmdForget) Execute(args []string) error {
return err return err
} }
if !cmd.DryRun { if !opts.DryRun {
err = repo.Backend().Remove(restic.SnapshotFile, id.String()) err = repo.Backend().Remove(restic.SnapshotFile, id.String())
if err != nil { if err != nil {
return err return err
} }
cmd.global.Verbosef("removed snapshot %v\n", id.Str()) Verbosef("removed snapshot %v\n", id.Str())
} else { } else {
cmd.global.Verbosef("would removed snapshot %v\n", id.Str()) Verbosef("would removed snapshot %v\n", id.Str())
} }
} }
policy := restic.ExpirePolicy{ policy := restic.ExpirePolicy{
Last: cmd.Last, Last: opts.Last,
Hourly: cmd.Hourly, Hourly: opts.Hourly,
Daily: cmd.Daily, Daily: opts.Daily,
Weekly: cmd.Weekly, Weekly: opts.Weekly,
Monthly: cmd.Monthly, Monthly: opts.Monthly,
Yearly: cmd.Yearly, Yearly: opts.Yearly,
Tags: cmd.KeepTags, Tags: opts.KeepTags,
} }
if policy.Empty() { if policy.Empty() {
@ -153,11 +164,11 @@ func (cmd CmdForget) Execute(args []string) error {
snapshotGroups := make(map[key]restic.Snapshots) snapshotGroups := make(map[key]restic.Snapshots)
for _, sn := range snapshots { for _, sn := range snapshots {
if cmd.Hostname != "" && sn.Hostname != cmd.Hostname { if opts.Hostname != "" && sn.Hostname != opts.Hostname {
continue continue
} }
if !sn.HasTags(cmd.Tags) { if !sn.HasTags(opts.Tags) {
continue continue
} }
@ -168,18 +179,18 @@ func (cmd CmdForget) Execute(args []string) error {
} }
for key, snapshotGroup := range snapshotGroups { 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) keep, remove := restic.ApplyPolicy(snapshotGroup, policy)
cmd.global.Printf("keep %d snapshots:\n", len(keep)) Printf("keep %d snapshots:\n", len(keep))
printSnapshots(cmd.global.stdout, keep) printSnapshots(globalOptions.stdout, keep)
cmd.global.Printf("\n") Printf("\n")
cmd.global.Printf("remove %d snapshots:\n", len(remove)) Printf("remove %d snapshots:\n", len(remove))
printSnapshots(cmd.global.stdout, remove) printSnapshots(globalOptions.stdout, remove)
cmd.global.Printf("\n") Printf("\n")
if !cmd.DryRun { if !opts.DryRun {
for _, sn := range remove { for _, sn := range remove {
err = repo.Backend().Remove(restic.SnapshotFile, sn.ID().String()) err = repo.Backend().Remove(restic.SnapshotFile, sn.ID().String())
if err != nil { if err != nil {

View File

@ -3,24 +3,37 @@ package main
import ( import (
"restic/errors" "restic/errors"
"restic/repository" "restic/repository"
"github.com/spf13/cobra"
) )
type CmdInit struct { var cmdInit = &cobra.Command{
global *GlobalOptions 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 { func init() {
if cmd.global.Repo == "" { cmdRoot.AddCommand(cmdInit)
}
func runInit(gopts GlobalOptions, args []string) error {
if gopts.Repo == "" {
return errors.Fatal("Please specify repository location (-r)") return errors.Fatal("Please specify repository location (-r)")
} }
be, err := create(cmd.global.Repo) be, err := create(gopts.Repo)
if err != nil { 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 == "" { if gopts.password == "" {
cmd.global.password, err = cmd.global.ReadPasswordTwice( gopts.password, err = ReadPasswordTwice(gopts,
"enter password for new backend: ", "enter password for new backend: ",
"enter password again: ") "enter password again: ")
if err != nil { if err != nil {
@ -30,26 +43,16 @@ func (cmd CmdInit) Execute(args []string) error {
s := repository.New(be) s := repository.New(be)
err = s.Init(cmd.global.password) err = s.Init(gopts.password)
if err != nil { 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) Verbosef("created restic backend %v at %s\n", s.Config().ID[:10], gopts.Repo)
cmd.global.Verbosef("\n") Verbosef("\n")
cmd.global.Verbosef("Please note that knowledge of your password is required to access\n") 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") Verbosef("the repository. Losing your password means that your data is\n")
cmd.global.Verbosef("irrecoverably lost.\n") Verbosef("irrecoverably lost.\n")
return nil 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)
}
}

View File

@ -4,42 +4,39 @@ import (
"fmt" "fmt"
"restic" "restic"
"github.com/spf13/cobra"
"restic/errors" "restic/errors"
"restic/repository" "restic/repository"
) )
type CmdKey struct { var cmdKey = &cobra.Command{
global *GlobalOptions Use: "key [list|add|rm|passwd] [ID]",
newPassword string 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() { func init() {
_, err := parser.AddCommand("key", cmdRoot.AddCommand(cmdKey)
"manage keys",
"The key command manages keys (passwords) of a repository",
&CmdKey{global: &globalOpts})
if err != nil {
panic(err)
}
} }
func (cmd CmdKey) listKeys(s *repository.Repository) error { func listKeys(s *repository.Repository) error {
tab := NewTable() tab := NewTable()
tab.Header = fmt.Sprintf(" %-10s %-10s %-10s %s", "ID", "User", "Host", "Created") tab.Header = fmt.Sprintf(" %-10s %-10s %-10s %s", "ID", "User", "Host", "Created")
tab.RowFormat = "%s%-10s %-10s %-10s %s" tab.RowFormat = "%s%-10s %-10s %-10s %s"
plen, err := s.PrefixLength(restic.KeyFile)
if err != nil {
return err
}
done := make(chan struct{}) done := make(chan struct{})
defer close(done) defer close(done)
for id := range s.List(restic.KeyFile, done) { for id := range s.List(restic.KeyFile, done) {
k, err := repository.LoadKey(s, id.String()) k, err := repository.LoadKey(s, id.String())
if err != nil { if err != nil {
cmd.global.Warnf("LoadKey() failed: %v\n", err) Warnf("LoadKey() failed: %v\n", err)
continue continue
} }
@ -49,25 +46,28 @@ func (cmd CmdKey) listKeys(s *repository.Repository) error {
} else { } else {
current = " " 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)}) 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) { // testKeyNewPassword is used to set a new password during integration testing.
if cmd.newPassword != "" { var testKeyNewPassword string
return cmd.newPassword, nil
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 for new key: ",
"enter password again: ") "enter password again: ")
} }
func (cmd CmdKey) addKey(repo *repository.Repository) error { func addKey(gopts GlobalOptions, repo *repository.Repository) error {
pw, err := cmd.getNewPassword() pw, err := getNewPassword(gopts)
if err != nil { if err != nil {
return err return err
} }
@ -77,12 +77,12 @@ func (cmd CmdKey) addKey(repo *repository.Repository) error {
return errors.Fatalf("creating new key failed: %v\n", err) 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 return nil
} }
func (cmd CmdKey) deleteKey(repo *repository.Repository, name string) error { func deleteKey(repo *repository.Repository, name string) error {
if name == repo.KeyName() { if name == repo.KeyName() {
return errors.Fatal("refusing to remove key currently used to access repository") 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 return err
} }
cmd.global.Verbosef("removed key %v\n", name) Verbosef("removed key %v\n", name)
return nil return nil
} }
func (cmd CmdKey) changePassword(repo *repository.Repository) error { func changePassword(gopts GlobalOptions, repo *repository.Repository) error {
pw, err := cmd.getNewPassword() pw, err := getNewPassword(gopts)
if err != nil { if err != nil {
return err return err
} }
@ -112,21 +112,17 @@ func (cmd CmdKey) changePassword(repo *repository.Repository) error {
return err return err
} }
cmd.global.Verbosef("saved new key as %s\n", id) Verbosef("saved new key as %s\n", id)
return nil return nil
} }
func (cmd CmdKey) Usage() string { func runKey(gopts GlobalOptions, args []string) error {
return "[list|add|rm|passwd] [ID]"
}
func (cmd CmdKey) Execute(args []string) error {
if len(args) < 1 || (args[0] == "rm" && len(args) != 2) { 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 { if err != nil {
return err return err
} }
@ -139,7 +135,7 @@ func (cmd CmdKey) Execute(args []string) error {
return err return err
} }
return cmd.listKeys(repo) return listKeys(repo)
case "add": case "add":
lock, err := lockRepo(repo) lock, err := lockRepo(repo)
defer unlockRepo(lock) defer unlockRepo(lock)
@ -147,7 +143,7 @@ func (cmd CmdKey) Execute(args []string) error {
return err return err
} }
return cmd.addKey(repo) return addKey(gopts, repo)
case "rm": case "rm":
lock, err := lockRepoExclusive(repo) lock, err := lockRepoExclusive(repo)
defer unlockRepo(lock) defer unlockRepo(lock)
@ -160,7 +156,7 @@ func (cmd CmdKey) Execute(args []string) error {
return err return err
} }
return cmd.deleteKey(repo, id) return deleteKey(repo, id)
case "passwd": case "passwd":
lock, err := lockRepoExclusive(repo) lock, err := lockRepoExclusive(repo)
defer unlockRepo(lock) defer unlockRepo(lock)
@ -168,7 +164,7 @@ func (cmd CmdKey) Execute(args []string) error {
return err return err
} }
return cmd.changePassword(repo) return changePassword(gopts, repo)
} }
return nil return nil

View File

@ -3,37 +3,36 @@ package main
import ( import (
"restic" "restic"
"restic/errors" "restic/errors"
"github.com/spf13/cobra"
) )
type CmdList struct { var cmdList = &cobra.Command{
global *GlobalOptions 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() { func init() {
_, err := parser.AddCommand("list", cmdRoot.AddCommand(cmdList)
"lists data",
"The list command lists structures or data of a repository",
&CmdList{global: &globalOpts})
if err != nil {
panic(err)
}
} }
func (cmd CmdList) Usage() string { func runList(opts GlobalOptions, args []string) error {
return "[blobs|packs|index|snapshots|keys|locks]"
}
func (cmd CmdList) Execute(args []string) error {
if len(args) != 1 { 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 { if err != nil {
return err return err
} }
if !cmd.global.NoLock { if !opts.NoLock {
lock, err := lockRepo(repo) lock, err := lockRepo(repo)
defer unlockRepo(lock) defer unlockRepo(lock)
if err != nil { if err != nil {
@ -58,7 +57,7 @@ func (cmd CmdList) Execute(args []string) error {
} }
for id := range repo.List(t, nil) { for id := range repo.List(t, nil) {
cmd.global.Printf("%s\n", id) Printf("%s\n", id)
} }
return nil return nil

View File

@ -5,29 +5,34 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/spf13/cobra"
"restic" "restic"
"restic/errors" "restic/errors"
"restic/repository" "restic/repository"
) )
type CmdLs struct { var cmdLs = &cobra.Command{
Long bool `short:"l" long:"long" description:"Use a long listing format showing size and mode"` Use: "ls [flags] snapshot-ID",
Short: "list files in a snapshot",
global *GlobalOptions 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() { func init() {
_, err := parser.AddCommand("ls", cmdRoot.AddCommand(cmdLs)
"list files",
"The ls command lists all files and directories in a snapshot", cmdLs.Flags().BoolVarP(&listLong, "long", "l", false, "use a long listing format showing size and mode")
&CmdLs{global: &globalOpts})
if err != nil {
panic(err)
}
} }
func (cmd CmdLs) printNode(prefix string, n *restic.Node) string { func printNode(prefix string, n *restic.Node) string {
if !cmd.Long { if !listLong {
return filepath.Join(prefix, n.Name) 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) tree, err := repo.LoadTree(id)
if err != nil { if err != nil {
return err return err
} }
for _, entry := range tree.Nodes { 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 { 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 { if err != nil {
return err return err
} }
@ -66,16 +71,12 @@ func (cmd CmdLs) printTree(prefix string, repo *repository.Repository, id restic
return nil return nil
} }
func (cmd CmdLs) Usage() string { func runLs(gopts GlobalOptions, args []string) error {
return "snapshot-ID [DIR]"
}
func (cmd CmdLs) Execute(args []string) error {
if len(args) < 1 || len(args) > 2 { 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 { if err != nil {
return err return err
} }
@ -95,7 +96,7 @@ func (cmd CmdLs) Execute(args []string) error {
return err 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)
} }

View File

@ -6,6 +6,8 @@ package main
import ( import (
"os" "os"
"github.com/spf13/cobra"
"restic/debug" "restic/debug"
"restic/errors" "restic/errors"
@ -16,33 +18,36 @@ import (
"bazil.org/fuse/fs" "bazil.org/fuse/fs"
) )
type CmdMount struct { var cmdMount = &cobra.Command{
Root bool `long:"owner-root" description:"use 'root' as the owner of files and dirs"` Use: "mount [flags] mountpoint",
Short: "mount the repository",
global *GlobalOptions 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() { func init() {
_, err := parser.AddCommand("mount", cmdRoot.AddCommand(cmdMount)
"mount a repository",
"The mount command mounts a repository read-only to a given directory", cmdMount.Flags().BoolVar(&mountOptions.OwnerRoot, "owner-root", false, "use 'root' as the owner of files and dirs")
&CmdMount{
global: &globalOpts,
})
if err != nil {
panic(err)
}
} }
func (cmd CmdMount) Usage() string { func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
return "MOUNTPOINT"
}
func (cmd CmdMount) Mount(mountpoint string) error {
debug.Log("mount", "start mount") debug.Log("mount", "start mount")
defer debug.Log("mount", "finish mount") defer debug.Log("mount", "finish mount")
repo, err := cmd.global.OpenRepository() repo, err := OpenRepository(gopts)
if err != nil { if err != nil {
return err return err
} }
@ -53,7 +58,7 @@ func (cmd CmdMount) Mount(mountpoint string) error {
} }
if _, err := resticfs.Stat(mountpoint); os.IsNotExist(errors.Cause(err)) { 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) err = resticfs.Mkdir(mountpoint, os.ModeDir|0700)
if err != nil { if err != nil {
return err return err
@ -68,8 +73,11 @@ func (cmd CmdMount) Mount(mountpoint string) error {
return err return err
} }
Printf("Now serving the repository at %s\n", mountpoint)
Printf("Don't forget to umount after quitting!\n")
root := fs.Tree{} 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) debug.Log("mount", "serving mount at %v", mountpoint)
err = fs.Serve(c, &root) err = fs.Serve(c, &root)
@ -81,28 +89,25 @@ func (cmd CmdMount) Mount(mountpoint string) error {
return c.MountError return c.MountError
} }
func (cmd CmdMount) Umount(mountpoint string) error { func umount(mountpoint string) error {
return systemFuse.Unmount(mountpoint) return systemFuse.Unmount(mountpoint)
} }
func (cmd CmdMount) Execute(args []string) error { func runMount(opts MountOptions, gopts GlobalOptions, args []string) error {
if len(args) == 0 { 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] mountpoint := args[0]
AddCleanupHandler(func() error { AddCleanupHandler(func() error {
debug.Log("mount", "running umount cleanup handler for mount at %v", mountpoint) debug.Log("mount", "running umount cleanup handler for mount at %v", mountpoint)
err := cmd.Umount(mountpoint) err := umount(mountpoint)
if err != nil { 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 return nil
}) })
cmd.global.Printf("Now serving the repository at %s\n", mountpoint) return mount(opts, gopts, mountpoint)
cmd.global.Printf("Don't forget to umount after quitting!\n")
return cmd.Mount(mountpoint)
} }

View File

@ -10,26 +10,25 @@ import (
"restic/repository" "restic/repository"
"time" "time"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
) )
// CmdPrune implements the 'prune' command. var cmdPrune = &cobra.Command{
type CmdPrune struct { Use: "prune [flags]",
global *GlobalOptions 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() { func init() {
_, err := parser.AddCommand("prune", cmdRoot.AddCommand(cmdPrune)
"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)
}
} }
// newProgressMax returns a progress that counts blobs. // newProgressMax returns a progress that counts blobs.
@ -64,9 +63,8 @@ func newProgressMax(show bool, max uint64, description string) *restic.Progress
return p return p
} }
// Execute runs the 'prune' command. func runPrune(gopts GlobalOptions) error {
func (cmd CmdPrune) Execute(args []string) error { repo, err := OpenRepository(gopts)
repo, err := cmd.global.OpenRepository()
if err != nil { if err != nil {
return err return err
} }
@ -92,14 +90,14 @@ func (cmd CmdPrune) Execute(args []string) error {
bytes int64 bytes int64
} }
cmd.global.Verbosef("counting files in repo\n") Verbosef("counting files in repo\n")
for _ = range repo.List(restic.DataFile, done) { for _ = range repo.List(restic.DataFile, done) {
stats.packs++ 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) idx, err := index.New(repo, bar)
if err != nil { if err != nil {
return err return err
@ -108,7 +106,7 @@ func (cmd CmdPrune) Execute(args []string) error {
for _, pack := range idx.Packs { for _, pack := range idx.Packs {
stats.bytes += pack.Size 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))) len(idx.Packs), len(idx.Blobs), formatBytes(uint64(stats.bytes)))
blobCount := make(map[restic.BlobHandle]int) 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))) stats.blobs, duplicateBlobs, formatBytes(uint64(duplicateBytes)))
cmd.global.Verbosef("load all snapshots\n") Verbosef("load all snapshots\n")
// find referenced blobs // find referenced blobs
snapshots, err := restic.LoadAllSnapshots(repo) snapshots, err := restic.LoadAllSnapshots(repo)
@ -141,12 +139,12 @@ func (cmd CmdPrune) Execute(args []string) error {
stats.snapshots = len(snapshots) 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() usedBlobs := restic.NewBlobSet()
seenBlobs := restic.NewBlobSet() seenBlobs := restic.NewBlobSet()
bar = newProgressMax(cmd.global.ShowProgress(), uint64(len(snapshots)), "snapshots") bar = newProgressMax(!gopts.Quiet, uint64(len(snapshots)), "snapshots")
bar.Start() bar.Start()
for _, sn := range snapshots { for _, sn := range snapshots {
debug.Log("CmdPrune.Execute", "process snapshot %v", sn.ID().Str()) debug.Log("CmdPrune.Execute", "process snapshot %v", sn.ID().Str())
@ -161,7 +159,7 @@ func (cmd CmdPrune) Execute(args []string) error {
} }
bar.Done() 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)) len(usedBlobs), stats.blobs, stats.blobs-len(usedBlobs))
// find packs that need a rewrite // find packs that need a rewrite
@ -207,7 +205,7 @@ func (cmd CmdPrune) Execute(args []string) error {
rewritePacks.Delete(packID) 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))) len(removePacks), len(rewritePacks), formatBytes(uint64(removeBytes)))
err = repository.Repack(repo, rewritePacks, usedBlobs) err = repository.Repack(repo, rewritePacks, usedBlobs)
@ -218,17 +216,17 @@ func (cmd CmdPrune) Execute(args []string) error {
for packID := range removePacks { for packID := range removePacks {
err = repo.Backend().Remove(restic.DataFile, packID.String()) err = repo.Backend().Remove(restic.DataFile, packID.String())
if err != nil { 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 stats.packs = 0
for _ = range repo.List(restic.DataFile, done) { for _ = range repo.List(restic.DataFile, done) {
stats.packs++ 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) idx, err = index.New(repo, bar)
if err != nil { if err != nil {
return err return err
@ -248,8 +246,8 @@ func (cmd CmdPrune) Execute(args []string) error {
if err != nil { if err != nil {
return err 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 return nil
} }

View File

@ -1,29 +1,32 @@
package main package main
import "restic/repository" import (
"restic/repository"
type CmdRebuildIndex struct { "github.com/spf13/cobra"
global *GlobalOptions )
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() { func init() {
_, err := parser.AddCommand("rebuild-index", cmdRoot.AddCommand(cmdRebuildIndex)
"rebuild the index",
"The rebuild-index command builds a new index",
&CmdRebuildIndex{global: &globalOpts})
if err != nil {
panic(err)
}
} }
func (cmd CmdRebuildIndex) Execute(args []string) error { func runRebuildIndex(gopts GlobalOptions) error {
repo, err := cmd.global.OpenRepository() repo, err := OpenRepository(gopts)
if err != nil { if err != nil {
return err return err
} }
cmd.repo = repo
lock, err := lockRepoExclusive(repo) lock, err := lockRepoExclusive(repo)
defer unlockRepo(lock) defer unlockRepo(lock)

View File

@ -5,55 +5,71 @@ import (
"restic/debug" "restic/debug"
"restic/errors" "restic/errors"
"restic/filter" "restic/filter"
"github.com/spf13/cobra"
) )
type CmdRestore struct { var cmdRestore = &cobra.Command{
Exclude []string `short:"e" long:"exclude" description:"Exclude a pattern (can be specified multiple times)"` Use: "restore [flags] snapshotID",
Include []string `short:"i" long:"include" description:"Include a pattern, exclude everything else (can be specified multiple times)"` Short: "extract the data from a snapshot",
Target string `short:"t" long:"target" description:"Directory to restore to"` Long: `
Host string `short:"h" long:"host" description:"Source Filter (for id=latest)"` The "restore" command extracts the data from a snapshot from the repository to
Paths []string `short:"p" long:"path" description:"Path Filter (absolute path;for id=latest) (can be specified multiple times)"` 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() { func init() {
_, err := parser.AddCommand("restore", cmdRoot.AddCommand(cmdRestore)
"restore a snapshot",
"The restore command restores a snapshot to a directory", flags := cmdRestore.Flags()
&CmdRestore{global: &globalOpts}) flags.StringSliceVarP(&restoreOptions.Exclude, "exclude", "e", nil, "exclude a pattern (can be specified multiple times)")
if err != nil { flags.StringSliceVarP(&restoreOptions.Include, "include", "i", nil, "include a pattern, exclude everything else (can be specified multiple times)")
panic(err) 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 { func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
return "snapshot-ID"
}
func (cmd CmdRestore) Execute(args []string) error {
if len(args) != 1 { 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)") 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") return errors.Fatal("exclude and include patterns are mutually exclusive")
} }
snapshotIDString := args[0] 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 { if err != nil {
return err return err
} }
if !cmd.global.NoLock { if !gopts.NoLock {
lock, err := lockRepo(repo) lock, err := lockRepo(repo)
defer unlockRepo(lock) defer unlockRepo(lock)
if err != nil { if err != nil {
@ -69,57 +85,52 @@ func (cmd CmdRestore) Execute(args []string) error {
var id restic.ID var id restic.ID
if snapshotIDString == "latest" { if snapshotIDString == "latest" {
id, err = restic.FindLatestSnapshot(repo, cmd.Paths, cmd.Host) id, err = restic.FindLatestSnapshot(repo, opts.Paths, opts.Host)
if err != nil { 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 { } else {
id, err = restic.FindSnapshot(repo, snapshotIDString) id, err = restic.FindSnapshot(repo, snapshotIDString)
if err != nil { 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) res, err := restic.NewRestorer(repo, id)
if err != nil { 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 { 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 return nil
} }
selectExcludeFilter := func(item string, dstpath string, node *restic.Node) bool { 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 { if err != nil {
cmd.global.Warnf("error for exclude pattern: %v", err) Warnf("error for exclude pattern: %v", err)
} }
return !matched return !matched
} }
selectIncludeFilter := func(item string, dstpath string, node *restic.Node) bool { 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 { if err != nil {
cmd.global.Warnf("error for include pattern: %v", err) Warnf("error for include pattern: %v", err)
} }
return matched return matched
} }
if len(cmd.Exclude) > 0 { if len(opts.Exclude) > 0 {
res.SelectFilter = selectExcludeFilter res.SelectFilter = selectExcludeFilter
} else if len(cmd.Include) > 0 { } else if len(opts.Include) > 0 {
res.SelectFilter = selectIncludeFilter 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) return res.RestoreTo(opts.Target)
if err != nil {
return err
}
return nil
} }

View File

@ -2,87 +2,60 @@ package main
import ( import (
"fmt" "fmt"
"io"
"os" "os"
"restic/errors" "restic/errors"
"sort" "sort"
"strings"
"github.com/spf13/cobra"
"restic" "restic"
) )
type Table struct { var cmdSnapshots = &cobra.Command{
Header string Use: "snapshots",
Rows [][]interface{} Short: "list all snapshots",
Long: `
RowFormat string 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 { // SnapshotOptions bundle all options for the snapshots command.
return Table{ type SnapshotOptions struct {
Rows: [][]interface{}{}, Host string
} Paths []string
} }
func (t Table) Write(w io.Writer) error { var snapshotOptions SnapshotOptions
_, err := fmt.Fprintln(w, t.Header)
if err != nil { func init() {
return err 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 { if err != nil {
return err return err
} }
for _, row := range t.Rows { if !gopts.NoLock {
_, err = fmt.Fprintf(w, t.RowFormat+"\n", row...) lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil { if err != nil {
return err 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 := NewTable()
tab.Header = fmt.Sprintf("%-8s %-19s %-10s %-10s %s", "ID", "Date", "Host", "Tags", "Directory") tab.Header = fmt.Sprintf("%-8s %-19s %-10s %-10s %s", "ID", "Date", "Host", "Tags", "Directory")
tab.RowFormat = "%-8s %-19s %-10s %-10s %s" tab.RowFormat = "%-8s %-19s %-10s %-10s %s"
@ -98,7 +71,7 @@ func (cmd CmdSnapshots) Execute(args []string) error {
continue 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 { pos := sort.Search(len(list), func(i int) bool {
return list[i].Time.After(sn.Time) return list[i].Time.After(sn.Time)
}) })

View File

@ -1,35 +1,43 @@
package main package main
import "restic" import (
"restic"
type CmdUnlock struct { "github.com/spf13/cobra"
RemoveAll bool `long:"remove-all" description:"Remove all locks, even stale ones"` )
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() { func init() {
_, err := parser.AddCommand("unlock", cmdRoot.AddCommand(unlockCmd)
"remove locks",
"The unlock command checks for stale locks and removes them", unlockCmd.Flags().BoolVar(&unlockOptions.RemoveAll, "remove-all", false, "Remove all locks, even non-stale ones")
&CmdUnlock{global: &globalOpts})
if err != nil {
panic(err)
}
} }
func (cmd CmdUnlock) Usage() string { func runUnlock(opts UnlockOptions, gopts GlobalOptions) error {
return "[unlock-options]" repo, err := OpenRepository(gopts)
}
func (cmd CmdUnlock) Execute(args []string) error {
repo, err := cmd.global.OpenRepository()
if err != nil { if err != nil {
return err return err
} }
fn := restic.RemoveStaleLocks fn := restic.RemoveStaleLocks
if cmd.RemoveAll { if opts.RemoveAll {
fn = restic.RemoveAllLocks fn = restic.RemoveAllLocks
} }
@ -38,6 +46,6 @@ func (cmd CmdUnlock) Execute(args []string) error {
return err return err
} }
cmd.global.Verbosef("successfully removed locks\n") Verbosef("successfully removed locks\n")
return nil return nil
} }

View File

@ -3,23 +3,23 @@ package main
import ( import (
"fmt" "fmt"
"runtime" "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() { func init() {
_, err := parser.AddCommand("version", cmdRoot.AddCommand(versionCmd)
"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
} }

60
src/cmds/restic/format.go Normal file
View File

@ -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)
}

View File

@ -10,6 +10,8 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/spf13/cobra"
"restic/backend/local" "restic/backend/local"
"restic/backend/rest" "restic/backend/rest"
"restic/backend/s3" "restic/backend/s3"
@ -20,28 +22,48 @@ import (
"restic/errors" "restic/errors"
"github.com/jessevdk/go-flags"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
) )
var version = "compiled manually" var version = "compiled manually"
var compiledAt = "unknown time" 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 { type GlobalOptions struct {
Repo string `short:"r" long:"repo" description:"Repository directory to backup to/restore from"` Repo string
PasswordFile string `short:"p" long:"password-file" description:"Read the repository password from a file"` PasswordFile string
CacheDir string ` long:"cache-dir" description:"Directory to use as a local cache"` Quiet bool
Quiet bool `short:"q" long:"quiet" description:"Do not output comprehensive progress report"` NoLock bool
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'"`
password string password string
stdout io.Writer stdout io.Writer
stderr io.Writer stderr io.Writer
} }
var globalOptions = GlobalOptions{
stdout: os.Stdout,
stderr: os.Stderr,
}
func init() { 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() 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 // ClearLine creates a platform dependent string to clear the current
// line, so it can be overwritten. ANSI sequences are not supported on // line, so it can be overwritten. ANSI sequences are not supported on
// current windows cmd shell. // current windows cmd shell.
@ -109,8 +128,8 @@ func ClearLine() string {
} }
// Printf writes the message to the configured stdout stream. // Printf writes the message to the configured stdout stream.
func (o GlobalOptions) Printf(format string, args ...interface{}) { func Printf(format string, args ...interface{}) {
_, err := fmt.Fprintf(o.stdout, format, args...) _, err := fmt.Fprintf(globalOptions.stdout, format, args...)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err) fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err)
os.Exit(100) 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. // Verbosef calls Printf to write the message when the verbose flag is set.
func (o GlobalOptions) Verbosef(format string, args ...interface{}) { func Verbosef(format string, args ...interface{}) {
if o.Quiet { if globalOptions.Quiet {
return return
} }
o.Printf(format, args...) 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
} }
// PrintProgress wraps fmt.Printf to handle the difference in writing progress // 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. // Warnf writes the message to the configured stderr stream.
func (o GlobalOptions) Warnf(format string, args ...interface{}) { func Warnf(format string, args ...interface{}) {
_, err := fmt.Fprintf(o.stderr, format, args...) _, err := fmt.Fprintf(globalOptions.stderr, format, args...)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "unable to write to stderr: %v\n", err) fmt.Fprintf(os.Stderr, "unable to write to stderr: %v\n", err)
os.Exit(100) 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). // 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' { if format[len(format)-1] != '\n' {
format += "\n" format += "\n"
} }
o.Warnf(format, args...) Warnf(format, args...)
os.Exit(exitcode) 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 // ReadPassword reads the password from a password file, the environment
// variable RESTIC_PASSWORD or prompts the user. // variable RESTIC_PASSWORD or prompts the user.
func (o GlobalOptions) ReadPassword(prompt string) (string, error) { func ReadPassword(opts GlobalOptions, prompt string) (string, error) {
if o.PasswordFile != "" { if opts.PasswordFile != "" {
s, err := ioutil.ReadFile(o.PasswordFile) s, err := ioutil.ReadFile(opts.PasswordFile)
return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile") 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 // ReadPasswordTwice calls ReadPassword two times and returns an error when the
// passwords don't match. // passwords don't match.
func (o GlobalOptions) ReadPasswordTwice(prompt1, prompt2 string) (string, error) { func ReadPasswordTwice(gopts GlobalOptions, prompt1, prompt2 string) (string, error) {
pw1, err := o.ReadPassword(prompt1) pw1, err := ReadPassword(gopts, prompt1)
if err != nil { if err != nil {
return "", err return "", err
} }
pw2, err := o.ReadPassword(prompt2) pw2, err := ReadPassword(gopts, prompt2)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -264,26 +273,26 @@ func (o GlobalOptions) ReadPasswordTwice(prompt1, prompt2 string) (string, error
const maxKeys = 20 const maxKeys = 20
// OpenRepository reads the password and opens the repository. // OpenRepository reads the password and opens the repository.
func (o GlobalOptions) OpenRepository() (*repository.Repository, error) { func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
if o.Repo == "" { if opts.Repo == "" {
return nil, errors.Fatal("Please specify repository location (-r)") return nil, errors.Fatal("Please specify repository location (-r)")
} }
be, err := open(o.Repo) be, err := open(opts.Repo)
if err != nil { if err != nil {
return nil, err return nil, err
} }
s := repository.New(be) s := repository.New(be)
if o.password == "" { if opts.password == "" {
o.password, err = o.ReadPassword("enter password for repository: ") opts.password, err = ReadPassword(opts, "enter password for repository: ")
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
err = s.SearchKey(o.password, maxKeys) err = s.SearchKey(opts.password, maxKeys)
if err != nil { if err != nil {
return nil, errors.Fatalf("unable to open repo: %v", err) return nil, errors.Fatalf("unable to open repo: %v", err)
} }

View File

@ -1,3 +1,4 @@
// +build ignore
// +build !openbsd // +build !openbsd
// +build !windows // +build !windows

View File

@ -166,18 +166,6 @@ type testEnvironment struct {
base, cache, repo, testdata string 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 // withTestEnvironment creates a test environment and calls f with it. After f has
// returned, the temporary directory is removed. // returned, the temporary directory is removed.
func withTestEnvironment(t testing.TB, f func(*testEnvironment, GlobalOptions)) { 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.cache, 0700))
OK(t, os.MkdirAll(env.repo, 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 { if !TestCleanupTempDirs {
t.Logf("leaving temporary directory %v used for test", tempdir) t.Logf("leaving temporary directory %v used for test", tempdir)

View File

@ -41,107 +41,126 @@ func parseIDsFromReader(t testing.TB, rd io.Reader) restic.IDs {
return IDs return IDs
} }
func cmdInit(t testing.TB, global GlobalOptions) { func testRunInit(t testing.TB, opts GlobalOptions) {
repository.TestUseLowSecurityKDFParameters(t) repository.TestUseLowSecurityKDFParameters(t)
restic.TestSetLockTimeout(t, 0) restic.TestSetLockTimeout(t, 0)
cmd := &CmdInit{global: &global} OK(t, runInit(opts, nil))
OK(t, cmd.Execute(nil)) t.Logf("repository initialized at %v", opts.Repo)
t.Logf("repository initialized at %v", global.Repo)
} }
func cmdBackup(t testing.TB, global GlobalOptions, target []string, parentID *restic.ID) { func testRunBackup(t testing.TB, target []string, opts BackupOptions, gopts GlobalOptions) {
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()
}
t.Logf("backing up %v", target) t.Logf("backing up %v", target)
OK(t, runBackup(opts, gopts, target))
OK(t, cmd.Execute(target))
} }
func cmdList(t testing.TB, global GlobalOptions, tpe string) restic.IDs { func testRunList(t testing.TB, tpe string, opts GlobalOptions) restic.IDs {
cmd := &CmdList{global: &global}
return executeAndParseIDs(t, cmd, tpe)
}
func executeAndParseIDs(t testing.TB, cmd *CmdList, args ...string) restic.IDs {
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
cmd.global.stdout = buf globalOptions.stdout = buf
OK(t, cmd.Execute(args)) defer func() {
globalOptions.stdout = os.Stdout
}()
OK(t, runList(opts, []string{tpe}))
return parseIDsFromReader(t, buf) return parseIDsFromReader(t, buf)
} }
func cmdRestore(t testing.TB, global GlobalOptions, dir string, snapshotID restic.ID) { func testRunRestore(t testing.TB, opts GlobalOptions, dir string, snapshotID restic.ID) {
cmdRestoreExcludes(t, global, dir, snapshotID, nil) testRunRestoreExcludes(t, opts, dir, snapshotID, nil)
} }
func cmdRestoreLatest(t testing.TB, global GlobalOptions, dir string, paths []string, host string) { func testRunRestoreLatest(t testing.TB, gopts GlobalOptions, dir string, paths []string, host string) {
cmd := &CmdRestore{global: &global, Target: dir, Host: host, Paths: paths} opts := RestoreOptions{
OK(t, cmd.Execute([]string{"latest"})) 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) { func testRunRestoreExcludes(t testing.TB, gopts GlobalOptions, dir string, snapshotID restic.ID, excludes []string) {
cmd := &CmdRestore{global: &global, Target: dir, Exclude: excludes} opts := RestoreOptions{
OK(t, cmd.Execute([]string{snapshotID.String()})) 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) { func testRunRestoreIncludes(t testing.TB, gopts GlobalOptions, dir string, snapshotID restic.ID, includes []string) {
cmd := &CmdRestore{global: &global, Target: dir, Include: includes} opts := RestoreOptions{
OK(t, cmd.Execute([]string{snapshotID.String()})) Target: dir,
Include: includes,
}
OK(t, runRestore(opts, gopts, []string{snapshotID.String()}))
} }
func cmdCheck(t testing.TB, global GlobalOptions) { func testRunCheck(t testing.TB, gopts GlobalOptions) {
cmd := &CmdCheck{ opts := CheckOptions{
global: &global,
ReadData: true, ReadData: true,
CheckUnused: 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) buf := bytes.NewBuffer(nil)
global.stdout = buf
cmd := &CmdCheck{global: &global, ReadData: true} globalOptions.stdout = buf
OK(t, cmd.Execute(nil)) defer func() {
return string(buf.Bytes()) 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) { func testRunRebuildIndex(t testing.TB, gopts GlobalOptions) {
global.stdout = ioutil.Discard globalOptions.stdout = ioutil.Discard
cmd := &CmdRebuildIndex{global: &global} defer func() {
OK(t, cmd.Execute(nil)) globalOptions.stdout = os.Stdout
}()
OK(t, runRebuildIndex(gopts))
} }
func cmdLs(t testing.TB, global GlobalOptions, snapshotID string) []string { func testRunLs(t testing.TB, gopts GlobalOptions, snapshotID string) []string {
var buf bytes.Buffer buf := bytes.NewBuffer(nil)
global.stdout = &buf globalOptions.stdout = buf
quiet := globalOptions.Quiet
globalOptions.Quiet = true
defer func() {
globalOptions.stdout = os.Stdout
globalOptions.Quiet = quiet
}()
cmd := &CmdLs{global: &global} OK(t, runLs(gopts, []string{snapshotID}))
OK(t, cmd.Execute([]string{snapshotID}))
return strings.Split(string(buf.Bytes()), "\n") return strings.Split(string(buf.Bytes()), "\n")
} }
func cmdFind(t testing.TB, global GlobalOptions, pattern string) []string { func testRunFind(t testing.TB, gopts GlobalOptions, pattern string) []string {
var buf bytes.Buffer buf := bytes.NewBuffer(nil)
global.stdout = &buf globalOptions.stdout = buf
defer func() {
globalOptions.stdout = os.Stdout
}()
cmd := &CmdFind{global: &global} opts := FindOptions{}
OK(t, cmd.Execute([]string{pattern}))
OK(t, runFind(opts, gopts, []string{pattern}))
return strings.Split(string(buf.Bytes()), "\n") return strings.Split(string(buf.Bytes()), "\n")
} }
func TestBackup(t *testing.T) { 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") datafile := filepath.Join("testdata", "backup-data.tar.gz")
fd, err := os.Open(datafile) fd, err := os.Open(datafile)
if os.IsNotExist(errors.Cause(err)) { if os.IsNotExist(errors.Cause(err)) {
@ -151,22 +170,23 @@ func TestBackup(t *testing.T) {
OK(t, err) OK(t, err)
OK(t, fd.Close()) OK(t, fd.Close())
cmdInit(t, global) testRunInit(t, gopts)
SetupTarTestFixture(t, env.testdata, datafile) SetupTarTestFixture(t, env.testdata, datafile)
opts := BackupOptions{}
// first backup // first backup
cmdBackup(t, global, []string{env.testdata}, nil) testRunBackup(t, []string{env.testdata}, opts, gopts)
snapshotIDs := cmdList(t, global, "snapshots") snapshotIDs := testRunList(t, "snapshots", gopts)
Assert(t, len(snapshotIDs) == 1, Assert(t, len(snapshotIDs) == 1,
"expected one snapshot, got %v", snapshotIDs) "expected one snapshot, got %v", snapshotIDs)
cmdCheck(t, global) testRunCheck(t, gopts)
stat1 := dirStats(env.repo) stat1 := dirStats(env.repo)
// second backup, implicit incremental // second backup, implicit incremental
cmdBackup(t, global, []string{env.testdata}, nil) testRunBackup(t, []string{env.testdata}, opts, gopts)
snapshotIDs = cmdList(t, global, "snapshots") snapshotIDs = testRunList(t, "snapshots", gopts)
Assert(t, len(snapshotIDs) == 2, Assert(t, len(snapshotIDs) == 2,
"expected two snapshots, got %v", snapshotIDs) "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) t.Logf("repository grown by %d bytes", stat2.size-stat1.size)
cmdCheck(t, global) testRunCheck(t, gopts)
// third backup, explicit incremental // third backup, explicit incremental
cmdBackup(t, global, []string{env.testdata}, &snapshotIDs[0]) opts.Parent = snapshotIDs[0].String()
snapshotIDs = cmdList(t, global, "snapshots") testRunBackup(t, []string{env.testdata}, opts, gopts)
snapshotIDs = testRunList(t, "snapshots", gopts)
Assert(t, len(snapshotIDs) == 3, Assert(t, len(snapshotIDs) == 3,
"expected three snapshots, got %v", snapshotIDs) "expected three snapshots, got %v", snapshotIDs)
@ -193,17 +214,17 @@ func TestBackup(t *testing.T) {
for i, snapshotID := range snapshotIDs { for i, snapshotID := range snapshotIDs {
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i)) restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir) 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")), Assert(t, directoriesEqualContents(env.testdata, filepath.Join(restoredir, "testdata")),
"directories are not equal") "directories are not equal")
} }
cmdCheck(t, global) testRunCheck(t, gopts)
}) })
} }
func TestBackupNonExistingFile(t *testing.T) { 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") datafile := filepath.Join("testdata", "backup-data.tar.gz")
fd, err := os.Open(datafile) fd, err := os.Open(datafile)
if os.IsNotExist(errors.Cause(err)) { if os.IsNotExist(errors.Cause(err)) {
@ -215,9 +236,11 @@ func TestBackupNonExistingFile(t *testing.T) {
SetupTarTestFixture(t, env.testdata, datafile) SetupTarTestFixture(t, env.testdata, datafile)
cmdInit(t, global) testRunInit(t, gopts)
globalOptions.stderr = ioutil.Discard
global.stderr = ioutil.Discard defer func() {
globalOptions.stderr = os.Stderr
}()
p := filepath.Join(env.testdata, "0", "0") p := filepath.Join(env.testdata, "0", "0")
dirs := []string{ dirs := []string{
@ -226,12 +249,15 @@ func TestBackupNonExistingFile(t *testing.T) {
filepath.Join(p, "nonexisting"), filepath.Join(p, "nonexisting"),
filepath.Join(p, "5"), filepath.Join(p, "5"),
} }
cmdBackup(t, global, dirs, nil)
opts := BackupOptions{}
testRunBackup(t, dirs, opts, gopts)
}) })
} }
func TestBackupMissingFile1(t *testing.T) { 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") datafile := filepath.Join("testdata", "backup-data.tar.gz")
fd, err := os.Open(datafile) fd, err := os.Open(datafile)
if os.IsNotExist(errors.Cause(err)) { if os.IsNotExist(errors.Cause(err)) {
@ -243,9 +269,12 @@ func TestBackupMissingFile1(t *testing.T) {
SetupTarTestFixture(t, env.testdata, datafile) 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 ranHook := false
debug.Hook("pipe.walk1", func(context interface{}) { debug.Hook("pipe.walk1", func(context interface{}) {
pathname := context.(string) pathname := context.(string)
@ -260,8 +289,10 @@ func TestBackupMissingFile1(t *testing.T) {
OK(t, os.Remove(filepath.Join(env.testdata, "0", "0", "9", "37"))) OK(t, os.Remove(filepath.Join(env.testdata, "0", "0", "9", "37")))
}) })
cmdBackup(t, global, []string{env.testdata}, nil) opts := BackupOptions{}
cmdCheck(t, global)
testRunBackup(t, []string{env.testdata}, opts, gopts)
testRunCheck(t, gopts)
Assert(t, ranHook, "hook did not run") Assert(t, ranHook, "hook did not run")
debug.RemoveHook("pipe.walk1") debug.RemoveHook("pipe.walk1")
@ -269,7 +300,7 @@ func TestBackupMissingFile1(t *testing.T) {
} }
func TestBackupMissingFile2(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") datafile := filepath.Join("testdata", "backup-data.tar.gz")
fd, err := os.Open(datafile) fd, err := os.Open(datafile)
if os.IsNotExist(errors.Cause(err)) { if os.IsNotExist(errors.Cause(err)) {
@ -281,9 +312,13 @@ func TestBackupMissingFile2(t *testing.T) {
SetupTarTestFixture(t, env.testdata, datafile) 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 ranHook := false
debug.Hook("pipe.walk2", func(context interface{}) { debug.Hook("pipe.walk2", func(context interface{}) {
pathname := context.(string) pathname := context.(string)
@ -298,8 +333,10 @@ func TestBackupMissingFile2(t *testing.T) {
OK(t, os.Remove(filepath.Join(env.testdata, "0", "0", "9", "37"))) OK(t, os.Remove(filepath.Join(env.testdata, "0", "0", "9", "37")))
}) })
cmdBackup(t, global, []string{env.testdata}, nil) opts := BackupOptions{}
cmdCheck(t, global)
testRunBackup(t, []string{env.testdata}, opts, gopts)
testRunCheck(t, gopts)
Assert(t, ranHook, "hook did not run") Assert(t, ranHook, "hook did not run")
debug.RemoveHook("pipe.walk2") debug.RemoveHook("pipe.walk2")
@ -307,7 +344,7 @@ func TestBackupMissingFile2(t *testing.T) {
} }
func TestBackupDirectoryError(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") datafile := filepath.Join("testdata", "backup-data.tar.gz")
fd, err := os.Open(datafile) fd, err := os.Open(datafile)
if os.IsNotExist(errors.Cause(err)) { if os.IsNotExist(errors.Cause(err)) {
@ -319,9 +356,13 @@ func TestBackupDirectoryError(t *testing.T) {
SetupTarTestFixture(t, env.testdata, datafile) 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 ranHook := false
testdir := filepath.Join(env.testdata, "0", "0", "9") testdir := filepath.Join(env.testdata, "0", "0", "9")
@ -340,17 +381,17 @@ func TestBackupDirectoryError(t *testing.T) {
OK(t, os.RemoveAll(testdir)) OK(t, os.RemoveAll(testdir))
}) })
cmdBackup(t, global, []string{filepath.Join(env.testdata, "0", "0")}, nil) testRunBackup(t, []string{filepath.Join(env.testdata, "0", "0")}, BackupOptions{}, gopts)
cmdCheck(t, global) testRunCheck(t, gopts)
Assert(t, ranHook, "hook did not run") Assert(t, ranHook, "hook did not run")
debug.RemoveHook("pipe.walk2") debug.RemoveHook("pipe.walk2")
snapshots := cmdList(t, global, "snapshots") snapshots := testRunList(t, "snapshots", gopts)
Assert(t, len(snapshots) > 0, Assert(t, len(snapshots) > 0,
"no snapshots found in repo (%v)", datafile) "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") Assert(t, len(files) > 1, "snapshot is empty")
}) })
@ -366,8 +407,8 @@ func includes(haystack []string, needle string) bool {
return false return false
} }
func loadSnapshotMap(t testing.TB, global GlobalOptions) map[string]struct{} { func loadSnapshotMap(t testing.TB, gopts GlobalOptions) map[string]struct{} {
snapshotIDs := cmdList(t, global, "snapshots") snapshotIDs := testRunList(t, "snapshots", gopts)
m := make(map[string]struct{}) m := make(map[string]struct{})
for _, id := range snapshotIDs { for _, id := range snapshotIDs {
@ -396,8 +437,8 @@ var backupExcludeFilenames = []string{
} }
func TestBackupExclude(t *testing.T) { func TestBackupExclude(t *testing.T) {
withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
cmdInit(t, global) testRunInit(t, gopts)
datadir := filepath.Join(env.base, "testdata") datadir := filepath.Join(env.base, "testdata")
@ -414,21 +455,25 @@ func TestBackupExclude(t *testing.T) {
snapshots := make(map[string]struct{}) snapshots := make(map[string]struct{})
cmdBackup(t, global, []string{datadir}, nil) opts := BackupOptions{}
snapshots, snapshotID := lastSnapshot(snapshots, loadSnapshotMap(t, global))
files := cmdLs(t, global, snapshotID) 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")), Assert(t, includes(files, filepath.Join("testdata", "foo.tar.gz")),
"expected file %q in first snapshot, but it's not included", "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"}) opts.Excludes = []string{"*.tar.gz"}
snapshots, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, global)) testRunBackup(t, []string{datadir}, opts, gopts)
files = cmdLs(t, global, snapshotID) snapshots, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, gopts))
files = testRunLs(t, gopts, snapshotID)
Assert(t, !includes(files, filepath.Join("testdata", "foo.tar.gz")), Assert(t, !includes(files, filepath.Join("testdata", "foo.tar.gz")),
"expected file %q not in first snapshot, but it's included", "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"}) opts.Excludes = []string{"*.tar.gz", "private/secret"}
snapshots, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, global)) testRunBackup(t, []string{datadir}, opts, gopts)
files = cmdLs(t, global, snapshotID) snapshots, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, gopts))
files = testRunLs(t, gopts, snapshotID)
Assert(t, !includes(files, filepath.Join("testdata", "foo.tar.gz")), Assert(t, !includes(files, filepath.Join("testdata", "foo.tar.gz")),
"expected file %q not in first snapshot, but it's included", "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")), 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) { func TestIncrementalBackup(t *testing.T) {
withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
cmdInit(t, global) testRunInit(t, gopts)
datadir := filepath.Join(env.base, "testdata") datadir := filepath.Join(env.base, "testdata")
testfile := filepath.Join(datadir, "testfile") testfile := filepath.Join(datadir, "testfile")
OK(t, appendRandomData(testfile, incrementalFirstWrite)) OK(t, appendRandomData(testfile, incrementalFirstWrite))
cmdBackup(t, global, []string{datadir}, nil) opts := BackupOptions{}
cmdCheck(t, global)
testRunBackup(t, []string{datadir}, opts, gopts)
testRunCheck(t, gopts)
stat1 := dirStats(env.repo) stat1 := dirStats(env.repo)
OK(t, appendRandomData(testfile, incrementalSecondWrite)) OK(t, appendRandomData(testfile, incrementalSecondWrite))
cmdBackup(t, global, []string{datadir}, nil) testRunBackup(t, []string{datadir}, opts, gopts)
cmdCheck(t, global) testRunCheck(t, gopts)
stat2 := dirStats(env.repo) stat2 := dirStats(env.repo)
if stat2.size-stat1.size > incrementalFirstWrite { if stat2.size-stat1.size > incrementalFirstWrite {
t.Errorf("repository size has grown by more than %d bytes", 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)) OK(t, appendRandomData(testfile, incrementalThirdWrite))
cmdBackup(t, global, []string{datadir}, nil) testRunBackup(t, []string{datadir}, opts, gopts)
cmdCheck(t, global) testRunCheck(t, gopts)
stat3 := dirStats(env.repo) stat3 := dirStats(env.repo)
if stat3.size-stat2.size > incrementalFirstWrite { if stat3.size-stat2.size > incrementalFirstWrite {
t.Errorf("repository size has grown by more than %d bytes", 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 { func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string {
var buf bytes.Buffer buf := bytes.NewBuffer(nil)
global.stdout = &buf globalOptions.stdout = buf
cmd := &CmdKey{global: &global} defer func() {
OK(t, cmd.Execute(args)) globalOptions.stdout = os.Stdout
}()
return buf.String() OK(t, runKey(gopts, []string{"list"}))
}
func cmdKeyListOtherIDs(t testing.TB, global GlobalOptions) []string { scanner := bufio.NewScanner(buf)
var buf bytes.Buffer
global.stdout = &buf
cmd := &CmdKey{global: &global}
OK(t, cmd.Execute([]string{"list"}))
scanner := bufio.NewScanner(&buf)
exp := regexp.MustCompile(`^ ([a-f0-9]+) `) exp := regexp.MustCompile(`^ ([a-f0-9]+) `)
IDs := []string{} IDs := []string{}
@ -529,21 +569,28 @@ func cmdKeyListOtherIDs(t testing.TB, global GlobalOptions) []string {
return IDs return IDs
} }
func cmdKeyAddNewKey(t testing.TB, global GlobalOptions, newPassword string) { func testRunKeyAddNewKey(t testing.TB, newPassword string, gopts GlobalOptions) {
cmd := &CmdKey{global: &global, newPassword: newPassword} testKeyNewPassword = newPassword
OK(t, cmd.Execute([]string{"add"})) defer func() {
testKeyNewPassword = ""
}()
OK(t, runKey(gopts, []string{"add"}))
} }
func cmdKeyPasswd(t testing.TB, global GlobalOptions, newPassword string) { func testRunKeyPasswd(t testing.TB, newPassword string, gopts GlobalOptions) {
cmd := &CmdKey{global: &global, newPassword: newPassword} testKeyNewPassword = newPassword
OK(t, cmd.Execute([]string{"passwd"})) defer func() {
testKeyNewPassword = ""
}()
OK(t, runKey(gopts, []string{"passwd"}))
} }
func cmdKeyRemove(t testing.TB, global GlobalOptions, IDs []string) { func testRunKeyRemove(t testing.TB, gopts GlobalOptions, IDs []string) {
cmd := &CmdKey{global: &global}
t.Logf("remove %d keys: %q\n", len(IDs), IDs) t.Logf("remove %d keys: %q\n", len(IDs), IDs)
for _, id := range 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", "raicneirvOjEfEigonOmLasOd",
} }
withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
cmdInit(t, global) testRunInit(t, gopts)
cmdKeyPasswd(t, global, "geheim2") testRunKeyPasswd(t, "geheim2", gopts)
global.password = "geheim2" gopts.password = "geheim2"
t.Logf("changed password to %q", global.password) t.Logf("changed password to %q", gopts.password)
for _, newPassword := range passwordList { for _, newPassword := range passwordList {
cmdKeyAddNewKey(t, global, newPassword) testRunKeyAddNewKey(t, newPassword, gopts)
t.Logf("added new password %q", newPassword) t.Logf("added new password %q", newPassword)
global.password = newPassword gopts.password = newPassword
cmdKeyRemove(t, global, cmdKeyListOtherIDs(t, global)) testRunKeyRemove(t, gopts, testRunKeyListOtherIDs(t, gopts))
} }
global.password = passwordList[len(passwordList)-1] gopts.password = passwordList[len(passwordList)-1]
t.Logf("testing access with last password %q\n", global.password) t.Logf("testing access with last password %q\n", gopts.password)
cmdKey(t, global, "list") OK(t, runKey(gopts, []string{"list"}))
testRunCheck(t, gopts)
cmdCheck(t, global)
}) })
} }
@ -599,8 +645,8 @@ func TestRestoreFilter(t *testing.T) {
{"subdir1/subdir2/testfile4.c", 102}, {"subdir1/subdir2/testfile4.c", 102},
} }
withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
cmdInit(t, global) testRunInit(t, gopts)
for _, test := range testfiles { for _, test := range testfiles {
p := filepath.Join(env.testdata, test.name) p := filepath.Join(env.testdata, test.name)
@ -608,20 +654,22 @@ func TestRestoreFilter(t *testing.T) {
OK(t, appendRandomData(p, test.size)) OK(t, appendRandomData(p, test.size))
} }
cmdBackup(t, global, []string{env.testdata}, nil) opts := BackupOptions{}
cmdCheck(t, global)
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 // 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 { for _, test := range testfiles {
OK(t, testFileSize(filepath.Join(env.base, "restore0", "testdata", test.name), int64(test.size))) OK(t, testFileSize(filepath.Join(env.base, "restore0", "testdata", test.name), int64(test.size)))
} }
for i, pat := range []string{"*.c", "*.exe", "*", "*file3*"} { for i, pat := range []string{"*.c", "*.exe", "*", "*file3*"} {
base := filepath.Join(env.base, fmt.Sprintf("restore%d", i+1)) 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 { for _, test := range testfiles {
err := testFileSize(filepath.Join(base, "testdata", test.name), int64(test.size)) err := testFileSize(filepath.Join(base, "testdata", test.name), int64(test.size))
if ok, _ := filter.Match(pat, filepath.Base(test.name)); !ok { if ok, _ := filter.Match(pat, filepath.Base(test.name)); !ok {
@ -638,49 +686,51 @@ func TestRestoreFilter(t *testing.T) {
func TestRestoreLatest(t *testing.T) { func TestRestoreLatest(t *testing.T) {
withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
cmdInit(t, global) testRunInit(t, gopts)
p := filepath.Join(env.testdata, "testfile.c") p := filepath.Join(env.testdata, "testfile.c")
OK(t, os.MkdirAll(filepath.Dir(p), 0755)) OK(t, os.MkdirAll(filepath.Dir(p), 0755))
OK(t, appendRandomData(p, 100)) OK(t, appendRandomData(p, 100))
cmdBackup(t, global, []string{env.testdata}, nil) opts := BackupOptions{}
cmdCheck(t, global)
testRunBackup(t, []string{env.testdata}, opts, gopts)
testRunCheck(t, gopts)
os.Remove(p) os.Remove(p)
OK(t, appendRandomData(p, 101)) OK(t, appendRandomData(p, 101))
cmdBackup(t, global, []string{env.testdata}, nil) testRunBackup(t, []string{env.testdata}, opts, gopts)
cmdCheck(t, global) testRunCheck(t, gopts)
// Restore latest without any filters // 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))) OK(t, testFileSize(filepath.Join(env.base, "restore0", "testdata", "testfile.c"), int64(101)))
// Setup test files in different directories backed up in different snapshots // Setup test files in different directories backed up in different snapshots
p1 := filepath.Join(env.testdata, "p1/testfile.c") p1 := filepath.Join(env.testdata, "p1/testfile.c")
OK(t, os.MkdirAll(filepath.Dir(p1), 0755)) OK(t, os.MkdirAll(filepath.Dir(p1), 0755))
OK(t, appendRandomData(p1, 102)) OK(t, appendRandomData(p1, 102))
cmdBackup(t, global, []string{filepath.Dir(p1)}, nil) testRunBackup(t, []string{filepath.Dir(p1)}, opts, gopts)
cmdCheck(t, global) testRunCheck(t, gopts)
p2 := filepath.Join(env.testdata, "p2/testfile.c") p2 := filepath.Join(env.testdata, "p2/testfile.c")
OK(t, os.MkdirAll(filepath.Dir(p2), 0755)) OK(t, os.MkdirAll(filepath.Dir(p2), 0755))
OK(t, appendRandomData(p2, 103)) OK(t, appendRandomData(p2, 103))
cmdBackup(t, global, []string{filepath.Dir(p2)}, nil) testRunBackup(t, []string{filepath.Dir(p2)}, opts, gopts)
cmdCheck(t, global) testRunCheck(t, gopts)
p1rAbs := filepath.Join(env.base, "restore1", "p1/testfile.c") p1rAbs := filepath.Join(env.base, "restore1", "p1/testfile.c")
p2rAbs := filepath.Join(env.base, "restore2", "p2/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))) OK(t, testFileSize(p1rAbs, int64(102)))
if _, err := os.Stat(p2rAbs); os.IsNotExist(errors.Cause(err)) { if _, err := os.Stat(p2rAbs); os.IsNotExist(errors.Cause(err)) {
Assert(t, 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) "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))) OK(t, testFileSize(p2rAbs, int64(103)))
if _, err := os.Stat(p1rAbs); os.IsNotExist(errors.Cause(err)) { if _, err := os.Stat(p1rAbs); os.IsNotExist(errors.Cause(err)) {
Assert(t, 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) { 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") datafile := filepath.Join("testdata", "repo-restore-permissions-test.tar.gz")
SetupTarTestFixture(t, env.base, datafile) SetupTarTestFixture(t, env.base, datafile)
snapshots := cmdList(t, global, "snapshots") snapshots := testRunList(t, "snapshots", gopts)
Assert(t, len(snapshots) > 0, Assert(t, len(snapshots) > 0,
"no snapshots found in repo (%v)", datafile) "no snapshots found in repo (%v)", datafile)
global.stderr = ioutil.Discard globalOptions.stderr = ioutil.Discard
cmdRestore(t, global, filepath.Join(env.base, "restore"), snapshots[0]) 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 // make sure that all files have been restored, regardeless of any
// permission errors // permission errors
files := cmdLs(t, global, snapshots[0].String()) files := testRunLs(t, gopts, snapshots[0].String())
for _, filename := range files { for _, filename := range files {
fi, err := os.Lstat(filepath.Join(env.base, "restore", filename)) fi, err := os.Lstat(filepath.Join(env.base, "restore", filename))
OK(t, err) OK(t, err)
@ -725,23 +779,25 @@ func setZeroModTime(filename string) error {
} }
func TestRestoreNoMetadataOnIgnoredIntermediateDirs(t *testing.T) { func TestRestoreNoMetadataOnIgnoredIntermediateDirs(t *testing.T) {
withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
cmdInit(t, global) testRunInit(t, gopts)
p := filepath.Join(env.testdata, "subdir1", "subdir2", "subdir3", "file.ext") p := filepath.Join(env.testdata, "subdir1", "subdir2", "subdir3", "file.ext")
OK(t, os.MkdirAll(filepath.Dir(p), 0755)) OK(t, os.MkdirAll(filepath.Dir(p), 0755))
OK(t, appendRandomData(p, 200)) OK(t, appendRandomData(p, 200))
OK(t, setZeroModTime(filepath.Join(env.testdata, "subdir1", "subdir2"))) OK(t, setZeroModTime(filepath.Join(env.testdata, "subdir1", "subdir2")))
cmdBackup(t, global, []string{env.testdata}, nil) opts := BackupOptions{}
cmdCheck(t, global)
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 // restore with filter "*.ext", this should restore "file.ext", but
// since the directories are ignored and only created because of // since the directories are ignored and only created because of
// "file.ext", no meta data should be restored for them. // "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") f1 := filepath.Join(env.base, "restore0", "testdata", "subdir1", "subdir2")
fi, err := os.Stat(f1) 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") "meta data of intermediate directory has been restore although it was ignored")
// restore with filter "*", this should restore meta data on everything. // 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") f2 := filepath.Join(env.base, "restore1", "testdata", "subdir1", "subdir2")
fi, err = os.Stat(f2) fi, err = os.Stat(f2)
@ -763,44 +819,55 @@ func TestRestoreNoMetadataOnIgnoredIntermediateDirs(t *testing.T) {
} }
func TestFind(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") datafile := filepath.Join("testdata", "backup-data.tar.gz")
cmdInit(t, global) testRunInit(t, gopts)
SetupTarTestFixture(t, env.testdata, datafile) 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) 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) 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) Assert(t, len(results) < 2, "less than two file found in repo (%v)", datafile)
}) })
} }
func TestRebuildIndex(t *testing.T) { 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") datafile := filepath.Join("..", "..", "restic", "checker", "testdata", "duplicate-packs-in-index-test-repo.tar.gz")
SetupTarTestFixture(t, env.base, datafile) SetupTarTestFixture(t, env.base, datafile)
out := cmdCheckOutput(t, global) out, err := testRunCheckOutput(gopts)
if !strings.Contains(out, "contained in several indexes") { if !strings.Contains(out, "contained in several indexes") {
t.Fatalf("did not find checker hint for packs 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") { if !strings.Contains(out, "restic rebuild-index") {
t.Fatalf("did not find hint for rebuild-index comman") 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 { if len(out) != 0 {
t.Fatalf("expected no output from the checker, got: %v", out) 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) { 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") datafile := filepath.Join("testdata", "small-repo.tar.gz")
SetupTarTestFixture(t, env.base, datafile) SetupTarTestFixture(t, env.base, datafile)
@ -822,14 +889,15 @@ func TestCheckRestoreNoLock(t *testing.T) {
}) })
OK(t, err) OK(t, err)
global.NoLock = true gopts.NoLock = true
cmdCheck(t, global)
snapshotIDs := cmdList(t, global, "snapshots") testRunCheck(t, gopts)
snapshotIDs := testRunList(t, "snapshots", gopts)
if len(snapshotIDs) == 0 { if len(snapshotIDs) == 0 {
t.Fatalf("found no snapshots") 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])
}) })
} }

View File

@ -7,11 +7,24 @@ import (
"restic/debug" "restic/debug"
"runtime" "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() { func init() {
// set GOMAXPROCS to number of CPUs // set GOMAXPROCS to number of CPUs
if runtime.Version() < "go1.5" { if runtime.Version() < "go1.5" {
@ -21,23 +34,11 @@ func init() {
runtime.GOMAXPROCS(runtime.NumCPU()) runtime.GOMAXPROCS(runtime.NumCPU())
} }
} }
} }
func main() { 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) debug.Log("restic", "main %#v", os.Args)
err := cmdRoot.Execute()
_, 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)
switch { switch {
case restic.IsAlreadyLocked(errors.Cause(err)): case restic.IsAlreadyLocked(errors.Cause(err)):

42
src/cmds/restic/table.go Normal file
View File

@ -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"

View File

@ -122,7 +122,7 @@ nextTag:
// SamePaths compares the Snapshot's paths and provided paths are exactly the same // SamePaths compares the Snapshot's paths and provided paths are exactly the same
func SamePaths(expected, actual []string) bool { func SamePaths(expected, actual []string) bool {
if expected == nil || actual == nil { if len(expected) == 0 || len(actual) == 0 {
return true return true
} }

View File

@ -82,7 +82,7 @@ func (e ExpirePolicy) Empty() bool {
return false return false
} }
empty := ExpirePolicy{} empty := ExpirePolicy{Tags: e.Tags}
return reflect.DeepEqual(e, empty) return reflect.DeepEqual(e, empty)
} }