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"
"path/filepath"
"restic"
"restic/archiver"
"restic/debug"
"restic/filter"
"restic/fs"
"strings"
"time"
"restic/errors"
"golang.org/x/crypto/ssh/terminal"
"github.com/spf13/cobra"
"restic/archiver"
"restic/debug"
"restic/errors"
"restic/filter"
"restic/fs"
)
type CmdBackup struct {
Parent string `short:"p" long:"parent" description:"use this parent snapshot (default: last snapshot in repo that has the same target)"`
Force bool `short:"f" long:"force" description:"Force re-reading the target. Overrides the \"parent\" flag"`
Excludes []string `short:"e" long:"exclude" description:"Exclude a pattern (can be specified multiple times)"`
ExcludeOtherFS bool `short:"x" long:"one-file-system" description:"Exclude other file systems"`
ExcludeFile string `long:"exclude-file" description:"Read exclude-patterns from file"`
Stdin bool `long:"stdin" description:"read backup data from stdin"`
StdinFilename string `long:"stdin-filename" default:"stdin" description:"file name to use when reading from stdin"`
Tags []string `long:"tag" description:"Add a tag (can be specified multiple times)"`
var cmdBackup = &cobra.Command{
Use: "backup [flags] FILE/DIR [FILE/DIR] ...",
Short: "create a new backup of files and/or directories",
Long: `
The "backup" command creates a new snapshot and saves the files and directories
given as the arguments.
`,
RunE: func(cmd *cobra.Command, args []string) error {
if backupOptions.Stdin {
return readBackupFromStdin(backupOptions, globalOptions, args)
}
global *GlobalOptions
return runBackup(backupOptions, globalOptions, args)
},
}
// BackupOptions bundles all options for the backup command.
type BackupOptions struct {
Parent string
Force bool
Excludes []string
ExcludeFile string
ExcludeOtherFS bool
Stdin bool
StdinFilename string
Tags []string
}
var backupOptions BackupOptions
func init() {
_, err := parser.AddCommand("backup",
"save file/directory",
"The backup command creates a snapshot of a file or directory",
&CmdBackup{global: &globalOpts})
if err != nil {
panic(err)
}
cmdRoot.AddCommand(cmdBackup)
f := cmdBackup.Flags()
f.StringVar(&backupOptions.Parent, "parent", "", "use this parent snapshot (default: last snapshot in the repo that has the same target files/directories)")
f.BoolVarP(&backupOptions.Force, "force", "f", false, `force re-reading the target files/directories. Overrides the "parent" flag`)
f.StringSliceVarP(&backupOptions.Excludes, "exclude", "e", []string{}, "exclude a pattern (can be specified multiple times)")
f.StringVar(&backupOptions.ExcludeFile, "exclude-file", "", "read exclude patterns from a file")
f.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "Exclude other file systems")
f.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin")
f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "", "file name to use when reading from stdin")
f.StringSliceVar(&backupOptions.Tags, "tag", []string{}, "add a tag for the new snapshot (can be specified multiple times)")
}
func formatBytes(c uint64) string {
b := float64(c)
switch {
case c > 1<<40:
return fmt.Sprintf("%.3f TiB", b/(1<<40))
case c > 1<<30:
return fmt.Sprintf("%.3f GiB", b/(1<<30))
case c > 1<<20:
return fmt.Sprintf("%.3f MiB", b/(1<<20))
case c > 1<<10:
return fmt.Sprintf("%.3f KiB", b/(1<<10))
default:
return fmt.Sprintf("%dB", c)
}
}
func formatSeconds(sec uint64) string {
hours := sec / 3600
sec -= hours * 3600
min := sec / 60
sec -= min * 60
if hours > 0 {
return fmt.Sprintf("%d:%02d:%02d", hours, min, sec)
}
return fmt.Sprintf("%d:%02d", min, sec)
}
func formatPercent(numerator uint64, denominator uint64) string {
if denominator == 0 {
return ""
}
percent := 100.0 * float64(numerator) / float64(denominator)
if percent > 100 {
percent = 100
}
return fmt.Sprintf("%3.2f%%", percent)
}
func formatRate(bytes uint64, duration time.Duration) string {
sec := float64(duration) / float64(time.Second)
rate := float64(bytes) / sec / (1 << 20)
return fmt.Sprintf("%.2fMiB/s", rate)
}
func formatDuration(d time.Duration) string {
sec := uint64(d / time.Second)
return formatSeconds(sec)
}
func (cmd CmdBackup) Usage() string {
return "DIR/FILE [DIR/FILE] [...]"
}
func (cmd CmdBackup) newScanProgress() *restic.Progress {
if !cmd.global.ShowProgress() {
func newScanProgress(gopts GlobalOptions) *restic.Progress {
if gopts.Quiet {
return nil
}
@ -115,8 +80,8 @@ func (cmd CmdBackup) newScanProgress() *restic.Progress {
return p
}
func (cmd CmdBackup) newArchiveProgress(todo restic.Stat) *restic.Progress {
if !cmd.global.ShowProgress() {
func newArchiveProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress {
if gopts.Quiet {
return nil
}
@ -169,8 +134,8 @@ func (cmd CmdBackup) newArchiveProgress(todo restic.Stat) *restic.Progress {
return archiveProgress
}
func (cmd CmdBackup) newArchiveStdinProgress() *restic.Progress {
if !cmd.global.ShowProgress() {
func newArchiveStdinProgress(gopts GlobalOptions) *restic.Progress {
if gopts.Quiet {
return nil
}
@ -250,12 +215,12 @@ func gatherDevices(items []string) (deviceMap map[uint64]struct{}, err error) {
return deviceMap, nil
}
func (cmd CmdBackup) readFromStdin(args []string) error {
func readBackupFromStdin(opts BackupOptions, gopts GlobalOptions, args []string) error {
if len(args) != 0 {
return errors.Fatalf("when reading from stdin, no additional files can be specified")
}
repo, err := cmd.global.OpenRepository()
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
@ -271,7 +236,7 @@ func (cmd CmdBackup) readFromStdin(args []string) error {
return err
}
_, id, err := archiver.ArchiveReader(repo, cmd.newArchiveStdinProgress(), os.Stdin, cmd.StdinFilename, cmd.Tags)
_, id, err := archiver.ArchiveReader(repo, newArchiveStdinProgress(gopts), os.Stdin, opts.StdinFilename, opts.Tags)
if err != nil {
return err
}
@ -280,13 +245,9 @@ func (cmd CmdBackup) readFromStdin(args []string) error {
return nil
}
func (cmd CmdBackup) Execute(args []string) error {
if cmd.Stdin {
return cmd.readFromStdin(args)
}
func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error {
if len(args) == 0 {
return errors.Fatalf("wrong number of parameters, Usage: %s", cmd.Usage())
return errors.Fatalf("wrong number of parameters")
}
target := make([]string, 0, len(args))
@ -304,7 +265,7 @@ func (cmd CmdBackup) Execute(args []string) error {
// allowed devices
var allowedDevs map[uint64]struct{}
if cmd.ExcludeOtherFS {
if opts.ExcludeOtherFS {
allowedDevs, err = gatherDevices(target)
if err != nil {
return err
@ -312,7 +273,7 @@ func (cmd CmdBackup) Execute(args []string) error {
debug.Log("backup.Execute", "allowed devices: %v\n", allowedDevs)
}
repo, err := cmd.global.OpenRepository()
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
@ -331,17 +292,17 @@ func (cmd CmdBackup) Execute(args []string) error {
var parentSnapshotID *restic.ID
// Force using a parent
if !cmd.Force && cmd.Parent != "" {
id, err := restic.FindSnapshot(repo, cmd.Parent)
if !opts.Force && opts.Parent != "" {
id, err := restic.FindSnapshot(repo, opts.Parent)
if err != nil {
return errors.Fatalf("invalid id %q: %v", cmd.Parent, err)
return errors.Fatalf("invalid id %q: %v", opts.Parent, err)
}
parentSnapshotID = &id
}
// Find last snapshot to set it as parent, if not already set
if !cmd.Force && parentSnapshotID == nil {
if !opts.Force && parentSnapshotID == nil {
id, err := restic.FindLatestSnapshot(repo, target, "")
if err == nil {
parentSnapshotID = &id
@ -351,16 +312,16 @@ func (cmd CmdBackup) Execute(args []string) error {
}
if parentSnapshotID != nil {
cmd.global.Verbosef("using parent snapshot %v\n", parentSnapshotID.Str())
Verbosef("using parent snapshot %v\n", parentSnapshotID.Str())
}
cmd.global.Verbosef("scan %v\n", target)
Verbosef("scan %v\n", target)
// add patterns from file
if cmd.ExcludeFile != "" {
file, err := fs.Open(cmd.ExcludeFile)
if opts.ExcludeFile != "" {
file, err := fs.Open(opts.ExcludeFile)
if err != nil {
cmd.global.Warnf("error reading exclude patterns: %v", err)
Warnf("error reading exclude patterns: %v", err)
return nil
}
@ -369,15 +330,15 @@ func (cmd CmdBackup) Execute(args []string) error {
line := scanner.Text()
if !strings.HasPrefix(line, "#") {
line = os.ExpandEnv(line)
cmd.Excludes = append(cmd.Excludes, line)
opts.Excludes = append(opts.Excludes, line)
}
}
}
selectFilter := func(item string, fi os.FileInfo) bool {
matched, err := filter.List(cmd.Excludes, item)
matched, err := filter.List(opts.Excludes, item)
if err != nil {
cmd.global.Warnf("error for exclude pattern: %v", err)
Warnf("error for exclude pattern: %v", err)
}
if matched {
@ -385,7 +346,7 @@ func (cmd CmdBackup) Execute(args []string) error {
return false
}
if !cmd.ExcludeOtherFS {
if !opts.ExcludeOtherFS {
return true
}
@ -404,27 +365,27 @@ func (cmd CmdBackup) Execute(args []string) error {
return true
}
stat, err := archiver.Scan(target, selectFilter, cmd.newScanProgress())
stat, err := archiver.Scan(target, selectFilter, newScanProgress(gopts))
if err != nil {
return err
}
arch := archiver.New(repo)
arch.Excludes = cmd.Excludes
arch.Excludes = opts.Excludes
arch.SelectFilter = selectFilter
arch.Error = func(dir string, fi os.FileInfo, err error) error {
// TODO: make ignoring errors configurable
cmd.global.Warnf("%s\rerror for %s: %v\n", ClearLine(), dir, err)
Warnf("%s\rerror for %s: %v\n", ClearLine(), dir, err)
return nil
}
_, id, err := arch.Snapshot(cmd.newArchiveProgress(stat), target, cmd.Tags, parentSnapshotID)
_, id, err := arch.Snapshot(newArchiveProgress(gopts, stat), target, opts.Tags, parentSnapshotID)
if err != nil {
return err
}
cmd.global.Verbosef("snapshot %s saved\n", id.Str())
Verbosef("snapshot %s saved\n", id.Str())
return nil
}

View File

@ -5,6 +5,8 @@ import (
"fmt"
"os"
"github.com/spf13/cobra"
"restic"
"restic/backend"
"restic/debug"
@ -12,30 +14,27 @@ import (
"restic/repository"
)
type CmdCat struct {
global *GlobalOptions
var cmdCat = &cobra.Command{
Use: "cat [flags] [pack|blob|tree|snapshot|key|masterkey|config|lock] ID",
Short: "print internal objects to stdout",
Long: `
The "cat" command is used to print internal objects to stdout.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runCat(globalOptions, args)
},
}
func init() {
_, err := parser.AddCommand("cat",
"dump something",
"The cat command dumps data structures or data from a repository",
&CmdCat{global: &globalOpts})
if err != nil {
panic(err)
}
cmdRoot.AddCommand(cmdCat)
}
func (cmd CmdCat) Usage() string {
return "[pack|blob|tree|snapshot|key|masterkey|config|lock] ID"
}
func (cmd CmdCat) Execute(args []string) error {
func runCat(gopts GlobalOptions, args []string) error {
if len(args) < 1 || (args[0] != "masterkey" && args[0] != "config" && len(args) != 2) {
return errors.Fatalf("type or ID not specified, Usage: %s", cmd.Usage())
return errors.Fatalf("type or ID not specified")
}
repo, err := cmd.global.OpenRepository()
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
@ -158,7 +157,7 @@ func (cmd CmdCat) Execute(args []string) error {
hash := restic.Hash(buf)
if !hash.Equal(id) {
fmt.Fprintf(cmd.global.stderr, "Warning: hash of data does not match ID, want\n %v\ngot:\n %v\n", id.String(), hash.String())
fmt.Fprintf(stderr, "Warning: hash of data does not match ID, want\n %v\ngot:\n %v\n", id.String(), hash.String())
}
_, err = os.Stdout.Write(buf)

View File

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

View File

@ -8,6 +8,8 @@ import (
"io"
"os"
"github.com/spf13/cobra"
"restic"
"restic/errors"
"restic/pack"
@ -16,24 +18,19 @@ import (
"restic/worker"
)
type CmdDump struct {
global *GlobalOptions
repo *repository.Repository
var cmdDump = &cobra.Command{
Use: "dump [indexes|snapshots|trees|all|packs]",
Short: "dump data structures",
Long: `
The "dump" command dumps data structures from a repository as JSON objects. It
is used for debugging purposes only.`,
RunE: func(cmd *cobra.Command, args []string) error {
return runDump(globalOptions, args)
},
}
func init() {
_, err := parser.AddCommand("dump",
"dump data structures",
"The dump command dumps data structures from a repository as JSON documents",
&CmdDump{global: &globalOpts})
if err != nil {
panic(err)
}
}
func (cmd CmdDump) Usage() string {
return "[indexes|snapshots|trees|all|packs]"
cmdRoot.AddCommand(cmdDump)
}
func prettyPrintJSON(wr io.Writer, item interface{}) error {
@ -148,14 +145,14 @@ func printPacks(repo *repository.Repository, wr io.Writer) error {
return nil
}
func (cmd CmdDump) DumpIndexes() error {
func dumpIndexes(repo restic.Repository) error {
done := make(chan struct{})
defer close(done)
for id := range cmd.repo.List(restic.IndexFile, done) {
for id := range repo.List(restic.IndexFile, done) {
fmt.Printf("index_id: %v\n", id)
idx, err := repository.LoadIndex(cmd.repo, id)
idx, err := repository.LoadIndex(repo, id)
if err != nil {
return err
}
@ -169,21 +166,22 @@ func (cmd CmdDump) DumpIndexes() error {
return nil
}
func (cmd CmdDump) Execute(args []string) error {
func runDump(gopts GlobalOptions, args []string) error {
if len(args) != 1 {
return errors.Fatalf("type not specified, Usage: %s", cmd.Usage())
return errors.Fatalf("type not specified")
}
repo, err := cmd.global.OpenRepository()
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
cmd.repo = repo
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
if !gopts.NoLock {
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
}
}
err = repo.LoadIndex()
@ -195,7 +193,7 @@ func (cmd CmdDump) Execute(args []string) error {
switch tpe {
case "indexes":
return cmd.DumpIndexes()
return dumpIndexes(repo)
case "snapshots":
return debugPrintSnapshots(repo, os.Stdout)
case "packs":
@ -208,7 +206,7 @@ func (cmd CmdDump) Execute(args []string) error {
}
fmt.Printf("\nindexes:\n")
err = cmd.DumpIndexes()
err = dumpIndexes(repo)
if err != nil {
return err
}

View File

@ -4,27 +4,53 @@ import (
"path/filepath"
"time"
"github.com/spf13/cobra"
"restic"
"restic/debug"
"restic/errors"
"restic/repository"
)
var cmdFind = &cobra.Command{
Use: "find [flags] PATTERN",
Short: "find a file or directory",
Long: `
The "find" command searches for files or directories in snapshots stored in the
repo. `,
RunE: func(cmd *cobra.Command, args []string) error {
return runFind(findOptions, globalOptions, args)
},
}
// FindOptions bundle all options for the find command.
type FindOptions struct {
Oldest string
Newest string
Snapshot string
}
var findOptions FindOptions
func init() {
cmdRoot.AddCommand(cmdFind)
f := cmdFind.Flags()
f.StringVarP(&findOptions.Oldest, "oldest", "o", "", "Oldest modification date/time")
f.StringVarP(&findOptions.Newest, "newest", "n", "", "Newest modification date/time")
f.StringVarP(&findOptions.Snapshot, "snapshot", "s", "", "Snapshot ID to search in")
}
type findPattern struct {
oldest, newest time.Time
pattern string
}
type findResult struct {
node *restic.Node
path string
}
type CmdFind struct {
Oldest string `short:"o" long:"oldest" description:"Oldest modification date/time"`
Newest string `short:"n" long:"newest" description:"Newest modification date/time"`
Snapshot string `short:"s" long:"snapshot" description:"Snapshot ID to search in"`
oldest, newest time.Time
pattern string
global *GlobalOptions
}
var timeFormats = []string{
"2006-01-02",
"2006-01-02 15:04",
@ -39,16 +65,6 @@ var timeFormats = []string{
"Mon Jan 2 15:04:05 -0700 MST 2006",
}
func init() {
_, err := parser.AddCommand("find",
"find a file/directory",
"The find command searches for files or directories in snapshots",
&CmdFind{global: &globalOpts})
if err != nil {
panic(err)
}
}
func parseTime(str string) (time.Time, error) {
for _, fmt := range timeFormats {
if t, err := time.ParseInLocation(fmt, str, time.Local); err == nil {
@ -59,7 +75,7 @@ func parseTime(str string) (time.Time, error) {
return time.Time{}, errors.Fatalf("unable to parse time: %q", str)
}
func (c CmdFind) findInTree(repo *repository.Repository, id restic.ID, path string) ([]findResult, error) {
func findInTree(repo *repository.Repository, pat findPattern, id restic.ID, path string) ([]findResult, error) {
debug.Log("restic.find", "checking tree %v\n", id)
tree, err := repo.LoadTree(id)
if err != nil {
@ -70,20 +86,20 @@ func (c CmdFind) findInTree(repo *repository.Repository, id restic.ID, path stri
for _, node := range tree.Nodes {
debug.Log("restic.find", " testing entry %q\n", node.Name)
m, err := filepath.Match(c.pattern, node.Name)
m, err := filepath.Match(pat.pattern, node.Name)
if err != nil {
return nil, err
}
if m {
debug.Log("restic.find", " pattern matches\n")
if !c.oldest.IsZero() && node.ModTime.Before(c.oldest) {
debug.Log("restic.find", " ModTime is older than %s\n", c.oldest)
if !pat.oldest.IsZero() && node.ModTime.Before(pat.oldest) {
debug.Log("restic.find", " ModTime is older than %s\n", pat.oldest)
continue
}
if !c.newest.IsZero() && node.ModTime.After(c.newest) {
debug.Log("restic.find", " ModTime is newer than %s\n", c.newest)
if !pat.newest.IsZero() && node.ModTime.After(pat.newest) {
debug.Log("restic.find", " ModTime is newer than %s\n", pat.newest)
continue
}
@ -93,7 +109,7 @@ func (c CmdFind) findInTree(repo *repository.Repository, id restic.ID, path stri
}
if node.Type == "dir" {
subdirResults, err := c.findInTree(repo, *node.Subtree, filepath.Join(path, node.Name))
subdirResults, err := findInTree(repo, pat, *node.Subtree, filepath.Join(path, node.Name))
if err != nil {
return nil, err
}
@ -105,15 +121,15 @@ func (c CmdFind) findInTree(repo *repository.Repository, id restic.ID, path stri
return results, nil
}
func (c CmdFind) findInSnapshot(repo *repository.Repository, id restic.ID) error {
debug.Log("restic.find", "searching in snapshot %s\n for entries within [%s %s]", id.Str(), c.oldest, c.newest)
func findInSnapshot(repo *repository.Repository, pat findPattern, id restic.ID) error {
debug.Log("restic.find", "searching in snapshot %s\n for entries within [%s %s]", id.Str(), pat.oldest, pat.newest)
sn, err := restic.LoadSnapshot(repo, id)
if err != nil {
return err
}
results, err := c.findInTree(repo, *sn.Tree, "")
results, err := findInTree(repo, pat, *sn.Tree, "")
if err != nil {
return err
}
@ -121,49 +137,50 @@ func (c CmdFind) findInSnapshot(repo *repository.Repository, id restic.ID) error
if len(results) == 0 {
return nil
}
c.global.Verbosef("found %d matching entries in snapshot %s\n", len(results), id)
Verbosef("found %d matching entries in snapshot %s\n", len(results), id)
for _, res := range results {
res.node.Name = filepath.Join(res.path, res.node.Name)
c.global.Printf(" %s\n", res.node)
Printf(" %s\n", res.node)
}
return nil
}
func (CmdFind) Usage() string {
return "[find-OPTIONS] PATTERN"
}
func (c CmdFind) Execute(args []string) error {
func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
if len(args) != 1 {
return errors.Fatalf("wrong number of arguments, Usage: %s", c.Usage())
return errors.Fatalf("wrong number of arguments")
}
var err error
var (
err error
pat findPattern
)
if c.Oldest != "" {
c.oldest, err = parseTime(c.Oldest)
if opts.Oldest != "" {
pat.oldest, err = parseTime(opts.Oldest)
if err != nil {
return err
}
}
if c.Newest != "" {
c.newest, err = parseTime(c.Newest)
if opts.Newest != "" {
pat.newest, err = parseTime(opts.Newest)
if err != nil {
return err
}
}
repo, err := c.global.OpenRepository()
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
if !gopts.NoLock {
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
}
}
err = repo.LoadIndex()
@ -171,21 +188,21 @@ func (c CmdFind) Execute(args []string) error {
return err
}
c.pattern = args[0]
pat.pattern = args[0]
if c.Snapshot != "" {
snapshotID, err := restic.FindSnapshot(repo, c.Snapshot)
if opts.Snapshot != "" {
snapshotID, err := restic.FindSnapshot(repo, opts.Snapshot)
if err != nil {
return errors.Fatalf("invalid id %q: %v", args[1], err)
}
return c.findInSnapshot(repo, snapshotID)
return findInSnapshot(repo, pat, snapshotID)
}
done := make(chan struct{})
defer close(done)
for snapshotID := range repo.List(restic.SnapshotFile, done) {
err := c.findInSnapshot(repo, snapshotID)
err := findInSnapshot(repo, pat, snapshotID)
if err != nil {
return err

View File

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

View File

@ -3,24 +3,37 @@ package main
import (
"restic/errors"
"restic/repository"
"github.com/spf13/cobra"
)
type CmdInit struct {
global *GlobalOptions
var cmdInit = &cobra.Command{
Use: "init",
Short: "initialize a new repository",
Long: `
The "init" command initializes a new repository.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runInit(globalOptions, args)
},
}
func (cmd CmdInit) Execute(args []string) error {
if cmd.global.Repo == "" {
func init() {
cmdRoot.AddCommand(cmdInit)
}
func runInit(gopts GlobalOptions, args []string) error {
if gopts.Repo == "" {
return errors.Fatal("Please specify repository location (-r)")
}
be, err := create(cmd.global.Repo)
be, err := create(gopts.Repo)
if err != nil {
cmd.global.Exitf(1, "creating backend at %s failed: %v\n", cmd.global.Repo, err)
return errors.Fatalf("create backend at %s failed: %v\n", gopts.Repo, err)
}
if cmd.global.password == "" {
cmd.global.password, err = cmd.global.ReadPasswordTwice(
if gopts.password == "" {
gopts.password, err = ReadPasswordTwice(gopts,
"enter password for new backend: ",
"enter password again: ")
if err != nil {
@ -30,26 +43,16 @@ func (cmd CmdInit) Execute(args []string) error {
s := repository.New(be)
err = s.Init(cmd.global.password)
err = s.Init(gopts.password)
if err != nil {
cmd.global.Exitf(1, "creating key in backend at %s failed: %v\n", cmd.global.Repo, err)
return errors.Fatalf("create key in backend at %s failed: %v\n", gopts.Repo, err)
}
cmd.global.Verbosef("created restic backend %v at %s\n", s.Config().ID[:10], cmd.global.Repo)
cmd.global.Verbosef("\n")
cmd.global.Verbosef("Please note that knowledge of your password is required to access\n")
cmd.global.Verbosef("the repository. Losing your password means that your data is\n")
cmd.global.Verbosef("irrecoverably lost.\n")
Verbosef("created restic backend %v at %s\n", s.Config().ID[:10], gopts.Repo)
Verbosef("\n")
Verbosef("Please note that knowledge of your password is required to access\n")
Verbosef("the repository. Losing your password means that your data is\n")
Verbosef("irrecoverably lost.\n")
return nil
}
func init() {
_, err := parser.AddCommand("init",
"create repository",
"The init command creates a new repository",
&CmdInit{global: &globalOpts})
if err != nil {
panic(err)
}
}

View File

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

View File

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

View File

@ -5,29 +5,34 @@ import (
"os"
"path/filepath"
"github.com/spf13/cobra"
"restic"
"restic/errors"
"restic/repository"
)
type CmdLs struct {
Long bool `short:"l" long:"long" description:"Use a long listing format showing size and mode"`
global *GlobalOptions
var cmdLs = &cobra.Command{
Use: "ls [flags] snapshot-ID",
Short: "list files in a snapshot",
Long: `
The "ls" command allows listing files and directories in a snapshot.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runLs(globalOptions, args)
},
}
var listLong bool
func init() {
_, err := parser.AddCommand("ls",
"list files",
"The ls command lists all files and directories in a snapshot",
&CmdLs{global: &globalOpts})
if err != nil {
panic(err)
}
cmdRoot.AddCommand(cmdLs)
cmdLs.Flags().BoolVarP(&listLong, "long", "l", false, "use a long listing format showing size and mode")
}
func (cmd CmdLs) printNode(prefix string, n *restic.Node) string {
if !cmd.Long {
func printNode(prefix string, n *restic.Node) string {
if !listLong {
return filepath.Join(prefix, n.Name)
}
@ -46,17 +51,17 @@ func (cmd CmdLs) printNode(prefix string, n *restic.Node) string {
}
}
func (cmd CmdLs) printTree(prefix string, repo *repository.Repository, id restic.ID) error {
func printTree(prefix string, repo *repository.Repository, id restic.ID) error {
tree, err := repo.LoadTree(id)
if err != nil {
return err
}
for _, entry := range tree.Nodes {
cmd.global.Printf(cmd.printNode(prefix, entry) + "\n")
Printf(printNode(prefix, entry) + "\n")
if entry.Type == "dir" && entry.Subtree != nil {
err = cmd.printTree(filepath.Join(prefix, entry.Name), repo, *entry.Subtree)
err = printTree(filepath.Join(prefix, entry.Name), repo, *entry.Subtree)
if err != nil {
return err
}
@ -66,16 +71,12 @@ func (cmd CmdLs) printTree(prefix string, repo *repository.Repository, id restic
return nil
}
func (cmd CmdLs) Usage() string {
return "snapshot-ID [DIR]"
}
func (cmd CmdLs) Execute(args []string) error {
func runLs(gopts GlobalOptions, args []string) error {
if len(args) < 1 || len(args) > 2 {
return errors.Fatalf("wrong number of arguments, Usage: %s", cmd.Usage())
return errors.Fatalf("no snapshot ID given")
}
repo, err := cmd.global.OpenRepository()
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
@ -95,7 +96,7 @@ func (cmd CmdLs) Execute(args []string) error {
return err
}
cmd.global.Verbosef("snapshot of %v at %s:\n", sn.Paths, sn.Time)
Verbosef("snapshot of %v at %s:\n", sn.Paths, sn.Time)
return cmd.printTree("", repo, *sn.Tree)
return printTree("", repo, *sn.Tree)
}

View File

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

View File

@ -10,26 +10,25 @@ import (
"restic/repository"
"time"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
)
// CmdPrune implements the 'prune' command.
type CmdPrune struct {
global *GlobalOptions
var cmdPrune = &cobra.Command{
Use: "prune [flags]",
Short: "remove unneeded data from the repository",
Long: `
The "prune" command checks the repository and removes data that is not
referenced and therefore not needed any more.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runPrune(globalOptions)
},
}
func init() {
_, err := parser.AddCommand("prune",
"removes content from a repository",
`
The prune command removes rendundant and unneeded data from the repository.
For removing snapshots, please see the 'forget' command, then afterwards run
'prune'.
`,
&CmdPrune{global: &globalOpts})
if err != nil {
panic(err)
}
cmdRoot.AddCommand(cmdPrune)
}
// newProgressMax returns a progress that counts blobs.
@ -64,9 +63,8 @@ func newProgressMax(show bool, max uint64, description string) *restic.Progress
return p
}
// Execute runs the 'prune' command.
func (cmd CmdPrune) Execute(args []string) error {
repo, err := cmd.global.OpenRepository()
func runPrune(gopts GlobalOptions) error {
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
@ -92,14 +90,14 @@ func (cmd CmdPrune) Execute(args []string) error {
bytes int64
}
cmd.global.Verbosef("counting files in repo\n")
Verbosef("counting files in repo\n")
for _ = range repo.List(restic.DataFile, done) {
stats.packs++
}
cmd.global.Verbosef("building new index for repo\n")
Verbosef("building new index for repo\n")
bar := newProgressMax(cmd.global.ShowProgress(), uint64(stats.packs), "packs")
bar := newProgressMax(!gopts.Quiet, uint64(stats.packs), "packs")
idx, err := index.New(repo, bar)
if err != nil {
return err
@ -108,7 +106,7 @@ func (cmd CmdPrune) Execute(args []string) error {
for _, pack := range idx.Packs {
stats.bytes += pack.Size
}
cmd.global.Verbosef("repository contains %v packs (%v blobs) with %v bytes\n",
Verbosef("repository contains %v packs (%v blobs) with %v bytes\n",
len(idx.Packs), len(idx.Blobs), formatBytes(uint64(stats.bytes)))
blobCount := make(map[restic.BlobHandle]int)
@ -129,9 +127,9 @@ func (cmd CmdPrune) Execute(args []string) error {
}
}
cmd.global.Verbosef("processed %d blobs: %d duplicate blobs, %v duplicate\n",
Verbosef("processed %d blobs: %d duplicate blobs, %v duplicate\n",
stats.blobs, duplicateBlobs, formatBytes(uint64(duplicateBytes)))
cmd.global.Verbosef("load all snapshots\n")
Verbosef("load all snapshots\n")
// find referenced blobs
snapshots, err := restic.LoadAllSnapshots(repo)
@ -141,12 +139,12 @@ func (cmd CmdPrune) Execute(args []string) error {
stats.snapshots = len(snapshots)
cmd.global.Verbosef("find data that is still in use for %d snapshots\n", stats.snapshots)
Verbosef("find data that is still in use for %d snapshots\n", stats.snapshots)
usedBlobs := restic.NewBlobSet()
seenBlobs := restic.NewBlobSet()
bar = newProgressMax(cmd.global.ShowProgress(), uint64(len(snapshots)), "snapshots")
bar = newProgressMax(!gopts.Quiet, uint64(len(snapshots)), "snapshots")
bar.Start()
for _, sn := range snapshots {
debug.Log("CmdPrune.Execute", "process snapshot %v", sn.ID().Str())
@ -161,7 +159,7 @@ func (cmd CmdPrune) Execute(args []string) error {
}
bar.Done()
cmd.global.Verbosef("found %d of %d data blobs still in use, removing %d blobs\n",
Verbosef("found %d of %d data blobs still in use, removing %d blobs\n",
len(usedBlobs), stats.blobs, stats.blobs-len(usedBlobs))
// find packs that need a rewrite
@ -207,7 +205,7 @@ func (cmd CmdPrune) Execute(args []string) error {
rewritePacks.Delete(packID)
}
cmd.global.Verbosef("will delete %d packs and rewrite %d packs, this frees %s\n",
Verbosef("will delete %d packs and rewrite %d packs, this frees %s\n",
len(removePacks), len(rewritePacks), formatBytes(uint64(removeBytes)))
err = repository.Repack(repo, rewritePacks, usedBlobs)
@ -218,17 +216,17 @@ func (cmd CmdPrune) Execute(args []string) error {
for packID := range removePacks {
err = repo.Backend().Remove(restic.DataFile, packID.String())
if err != nil {
cmd.global.Warnf("unable to remove file %v from the repository\n", packID.Str())
Warnf("unable to remove file %v from the repository\n", packID.Str())
}
}
cmd.global.Verbosef("creating new index\n")
Verbosef("creating new index\n")
stats.packs = 0
for _ = range repo.List(restic.DataFile, done) {
stats.packs++
}
bar = newProgressMax(cmd.global.ShowProgress(), uint64(stats.packs), "packs")
bar = newProgressMax(!gopts.Quiet, uint64(stats.packs), "packs")
idx, err = index.New(repo, bar)
if err != nil {
return err
@ -248,8 +246,8 @@ func (cmd CmdPrune) Execute(args []string) error {
if err != nil {
return err
}
cmd.global.Verbosef("saved new index as %v\n", id.Str())
Verbosef("saved new index as %v\n", id.Str())
cmd.global.Verbosef("done\n")
Verbosef("done\n")
return nil
}

View File

@ -1,29 +1,32 @@
package main
import "restic/repository"
import (
"restic/repository"
type CmdRebuildIndex struct {
global *GlobalOptions
"github.com/spf13/cobra"
)
repo *repository.Repository
var cmdRebuildIndex = &cobra.Command{
Use: "rebuild-index [flags]",
Short: "build a new index file",
Long: `
The "rebuild-index" command creates a new index by combining the index files
into a new one.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runRebuildIndex(globalOptions)
},
}
func init() {
_, err := parser.AddCommand("rebuild-index",
"rebuild the index",
"The rebuild-index command builds a new index",
&CmdRebuildIndex{global: &globalOpts})
if err != nil {
panic(err)
}
cmdRoot.AddCommand(cmdRebuildIndex)
}
func (cmd CmdRebuildIndex) Execute(args []string) error {
repo, err := cmd.global.OpenRepository()
func runRebuildIndex(gopts GlobalOptions) error {
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
cmd.repo = repo
lock, err := lockRepoExclusive(repo)
defer unlockRepo(lock)

View File

@ -5,55 +5,71 @@ import (
"restic/debug"
"restic/errors"
"restic/filter"
"github.com/spf13/cobra"
)
type CmdRestore struct {
Exclude []string `short:"e" long:"exclude" description:"Exclude a pattern (can be specified multiple times)"`
Include []string `short:"i" long:"include" description:"Include a pattern, exclude everything else (can be specified multiple times)"`
Target string `short:"t" long:"target" description:"Directory to restore to"`
Host string `short:"h" long:"host" description:"Source Filter (for id=latest)"`
Paths []string `short:"p" long:"path" description:"Path Filter (absolute path;for id=latest) (can be specified multiple times)"`
var cmdRestore = &cobra.Command{
Use: "restore [flags] snapshotID",
Short: "extract the data from a snapshot",
Long: `
The "restore" command extracts the data from a snapshot from the repository to
a directory.
global *GlobalOptions
The special snapshot "latest" can be used to restore the latest snapshot in the
repository.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runRestore(restoreOptions, globalOptions, args)
},
}
// RestoreOptions collects all options for the restore command.
type RestoreOptions struct {
Exclude []string
Include []string
Target string
Host string
Paths []string
}
var restoreOptions RestoreOptions
func init() {
_, err := parser.AddCommand("restore",
"restore a snapshot",
"The restore command restores a snapshot to a directory",
&CmdRestore{global: &globalOpts})
if err != nil {
panic(err)
}
cmdRoot.AddCommand(cmdRestore)
flags := cmdRestore.Flags()
flags.StringSliceVarP(&restoreOptions.Exclude, "exclude", "e", nil, "exclude a pattern (can be specified multiple times)")
flags.StringSliceVarP(&restoreOptions.Include, "include", "i", nil, "include a pattern, exclude everything else (can be specified multiple times)")
flags.StringVarP(&restoreOptions.Target, "target", "t", "", "directory to extract data to")
flags.StringVarP(&restoreOptions.Host, "host", "h", "", `only consider snapshots for this host when the snapshot ID is "latest"`)
flags.StringSliceVarP(&restoreOptions.Paths, "path", "p", nil, `only consider snapshots which include this (absolute) path for snapshot ID "latest"`)
}
func (cmd CmdRestore) Usage() string {
return "snapshot-ID"
}
func (cmd CmdRestore) Execute(args []string) error {
func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
if len(args) != 1 {
return errors.Fatalf("wrong number of arguments, Usage: %s", cmd.Usage())
return errors.Fatalf("no snapshot ID specified")
}
if cmd.Target == "" {
if opts.Target == "" {
return errors.Fatal("please specify a directory to restore to (--target)")
}
if len(cmd.Exclude) > 0 && len(cmd.Include) > 0 {
if len(opts.Exclude) > 0 && len(opts.Include) > 0 {
return errors.Fatal("exclude and include patterns are mutually exclusive")
}
snapshotIDString := args[0]
debug.Log("restore", "restore %v to %v", snapshotIDString, cmd.Target)
debug.Log("restore", "restore %v to %v", snapshotIDString, opts.Target)
repo, err := cmd.global.OpenRepository()
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
if !cmd.global.NoLock {
if !gopts.NoLock {
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
@ -69,57 +85,52 @@ func (cmd CmdRestore) Execute(args []string) error {
var id restic.ID
if snapshotIDString == "latest" {
id, err = restic.FindLatestSnapshot(repo, cmd.Paths, cmd.Host)
id, err = restic.FindLatestSnapshot(repo, opts.Paths, opts.Host)
if err != nil {
cmd.global.Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, cmd.Paths, cmd.Host)
Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, opts.Paths, opts.Host)
}
} else {
id, err = restic.FindSnapshot(repo, snapshotIDString)
if err != nil {
cmd.global.Exitf(1, "invalid id %q: %v", snapshotIDString, err)
Exitf(1, "invalid id %q: %v", snapshotIDString, err)
}
}
res, err := restic.NewRestorer(repo, id)
if err != nil {
cmd.global.Exitf(2, "creating restorer failed: %v\n", err)
Exitf(2, "creating restorer failed: %v\n", err)
}
res.Error = func(dir string, node *restic.Node, err error) error {
cmd.global.Warnf("error for %s: %+v\n", dir, err)
Warnf("error for %s: %+v\n", dir, err)
return nil
}
selectExcludeFilter := func(item string, dstpath string, node *restic.Node) bool {
matched, err := filter.List(cmd.Exclude, item)
matched, err := filter.List(opts.Exclude, item)
if err != nil {
cmd.global.Warnf("error for exclude pattern: %v", err)
Warnf("error for exclude pattern: %v", err)
}
return !matched
}
selectIncludeFilter := func(item string, dstpath string, node *restic.Node) bool {
matched, err := filter.List(cmd.Include, item)
matched, err := filter.List(opts.Include, item)
if err != nil {
cmd.global.Warnf("error for include pattern: %v", err)
Warnf("error for include pattern: %v", err)
}
return matched
}
if len(cmd.Exclude) > 0 {
if len(opts.Exclude) > 0 {
res.SelectFilter = selectExcludeFilter
} else if len(cmd.Include) > 0 {
} else if len(opts.Include) > 0 {
res.SelectFilter = selectIncludeFilter
}
cmd.global.Verbosef("restoring %s to %s\n", res.Snapshot(), cmd.Target)
Verbosef("restoring %s to %s\n", res.Snapshot(), opts.Target)
err = res.RestoreTo(cmd.Target)
if err != nil {
return err
}
return nil
return res.RestoreTo(opts.Target)
}

View File

@ -2,87 +2,60 @@ package main
import (
"fmt"
"io"
"os"
"restic/errors"
"sort"
"strings"
"github.com/spf13/cobra"
"restic"
)
type Table struct {
Header string
Rows [][]interface{}
RowFormat string
var cmdSnapshots = &cobra.Command{
Use: "snapshots",
Short: "list all snapshots",
Long: `
The "snapshots" command lists all snapshots stored in a repository.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runSnapshots(snapshotOptions, globalOptions, args)
},
}
func NewTable() Table {
return Table{
Rows: [][]interface{}{},
}
// SnapshotOptions bundle all options for the snapshots command.
type SnapshotOptions struct {
Host string
Paths []string
}
func (t Table) Write(w io.Writer) error {
_, err := fmt.Fprintln(w, t.Header)
if err != nil {
return err
var snapshotOptions SnapshotOptions
func init() {
cmdRoot.AddCommand(cmdSnapshots)
f := cmdSnapshots.Flags()
f.StringVar(&snapshotOptions.Host, "host", "", "only print snapshots for this host")
f.StringSliceVar(&snapshotOptions.Paths, "path", []string{}, "only print snapshots for this path (can be specified multiple times)")
}
func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) error {
if len(args) != 0 {
return errors.Fatalf("wrong number of arguments")
}
_, err = fmt.Fprintln(w, strings.Repeat("-", 70))
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
for _, row := range t.Rows {
_, err = fmt.Fprintf(w, t.RowFormat+"\n", row...)
if !gopts.NoLock {
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
}
}
return nil
}
const TimeFormat = "2006-01-02 15:04:05"
type CmdSnapshots struct {
Host string `short:"h" long:"host" description:"Host Filter"`
Paths []string `short:"p" long:"path" description:"Path Filter (absolute path) (can be specified multiple times)"`
global *GlobalOptions
}
func init() {
_, err := parser.AddCommand("snapshots",
"show snapshots",
"The snapshots command lists all snapshots stored in a repository",
&CmdSnapshots{global: &globalOpts})
if err != nil {
panic(err)
}
}
func (cmd CmdSnapshots) Usage() string {
return ""
}
func (cmd CmdSnapshots) Execute(args []string) error {
if len(args) != 0 {
return errors.Fatalf("wrong number of arguments, usage: %s", cmd.Usage())
}
repo, err := cmd.global.OpenRepository()
if err != nil {
return err
}
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
}
tab := NewTable()
tab.Header = fmt.Sprintf("%-8s %-19s %-10s %-10s %s", "ID", "Date", "Host", "Tags", "Directory")
tab.RowFormat = "%-8s %-19s %-10s %-10s %s"
@ -98,7 +71,7 @@ func (cmd CmdSnapshots) Execute(args []string) error {
continue
}
if restic.SamePaths(sn.Paths, cmd.Paths) && (cmd.Host == "" || cmd.Host == sn.Hostname) {
if restic.SamePaths(sn.Paths, opts.Paths) && (opts.Host == "" || opts.Host == sn.Hostname) {
pos := sort.Search(len(list), func(i int) bool {
return list[i].Time.After(sn.Time)
})

View File

@ -1,35 +1,43 @@
package main
import "restic"
import (
"restic"
type CmdUnlock struct {
RemoveAll bool `long:"remove-all" description:"Remove all locks, even stale ones"`
"github.com/spf13/cobra"
)
global *GlobalOptions
var unlockCmd = &cobra.Command{
Use: "unlock",
Short: "remove locks other processes created",
Long: `
The "unlock" command removes stale locks that have been created by other restic processes.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runUnlock(unlockOptions, globalOptions)
},
}
// UnlockOptions collects all options for the unlock command.
type UnlockOptions struct {
RemoveAll bool
}
var unlockOptions UnlockOptions
func init() {
_, err := parser.AddCommand("unlock",
"remove locks",
"The unlock command checks for stale locks and removes them",
&CmdUnlock{global: &globalOpts})
if err != nil {
panic(err)
}
cmdRoot.AddCommand(unlockCmd)
unlockCmd.Flags().BoolVar(&unlockOptions.RemoveAll, "remove-all", false, "Remove all locks, even non-stale ones")
}
func (cmd CmdUnlock) Usage() string {
return "[unlock-options]"
}
func (cmd CmdUnlock) Execute(args []string) error {
repo, err := cmd.global.OpenRepository()
func runUnlock(opts UnlockOptions, gopts GlobalOptions) error {
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
fn := restic.RemoveStaleLocks
if cmd.RemoveAll {
if opts.RemoveAll {
fn = restic.RemoveAllLocks
}
@ -38,6 +46,6 @@ func (cmd CmdUnlock) Execute(args []string) error {
return err
}
cmd.global.Verbosef("successfully removed locks\n")
Verbosef("successfully removed locks\n")
return nil
}

View File

@ -3,23 +3,23 @@ package main
import (
"fmt"
"runtime"
"github.com/spf13/cobra"
)
type CmdVersion struct{}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print version information",
Long: `
The "version" command prints detailed information about the build environment
and the version of this software.
`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("restic %s\ncompiled at %s with %v on %v/%v\n",
version, compiledAt, runtime.Version(), runtime.GOOS, runtime.GOARCH)
},
}
func init() {
_, err := parser.AddCommand("version",
"display version",
"The version command displays detailed information about the version",
&CmdVersion{})
if err != nil {
panic(err)
}
}
func (cmd CmdVersion) Execute(args []string) error {
fmt.Printf("restic %s\ncompiled at %s with %v on %v/%v\n",
version, compiledAt, runtime.Version(), runtime.GOOS, runtime.GOARCH)
return nil
cmdRoot.AddCommand(versionCmd)
}

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"
"syscall"
"github.com/spf13/cobra"
"restic/backend/local"
"restic/backend/rest"
"restic/backend/s3"
@ -20,28 +22,48 @@ import (
"restic/errors"
"github.com/jessevdk/go-flags"
"golang.org/x/crypto/ssh/terminal"
)
var version = "compiled manually"
var compiledAt = "unknown time"
// GlobalOptions holds all those options that can be set for every command.
func parseEnvironment(cmd *cobra.Command, args []string) {
repo := os.Getenv("RESTIC_REPOSITORY")
if repo != "" {
globalOptions.Repo = repo
}
pw := os.Getenv("RESTIC_PASSWORD")
if pw != "" {
globalOptions.password = pw
}
}
// GlobalOptions hold all global options for restic.
type GlobalOptions struct {
Repo string `short:"r" long:"repo" description:"Repository directory to backup to/restore from"`
PasswordFile string `short:"p" long:"password-file" description:"Read the repository password from a file"`
CacheDir string ` long:"cache-dir" description:"Directory to use as a local cache"`
Quiet bool `short:"q" long:"quiet" description:"Do not output comprehensive progress report"`
NoLock bool ` long:"no-lock" description:"Do not lock the repo, this allows some operations on read-only repos."`
Options []string `short:"o" long:"option" description:"Specify options in the form 'foo.key=value'"`
Repo string
PasswordFile string
Quiet bool
NoLock bool
password string
stdout io.Writer
stderr io.Writer
}
var globalOptions = GlobalOptions{
stdout: os.Stdout,
stderr: os.Stderr,
}
func init() {
f := cmdRoot.PersistentFlags()
f.StringVarP(&globalOptions.Repo, "repo", "r", "", "repository to backup to or restore from (default: $RESTIC_REPOSITORY)")
f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", "", "read the repository password from a file")
f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not outputcomprehensive progress report")
f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos")
restoreTerminal()
}
@ -91,9 +113,6 @@ func restoreTerminal() {
})
}
var globalOpts = GlobalOptions{stdout: os.Stdout, stderr: os.Stderr}
var parser = flags.NewParser(&globalOpts, flags.HelpFlag|flags.PassDoubleDash)
// ClearLine creates a platform dependent string to clear the current
// line, so it can be overwritten. ANSI sequences are not supported on
// current windows cmd shell.
@ -109,8 +128,8 @@ func ClearLine() string {
}
// Printf writes the message to the configured stdout stream.
func (o GlobalOptions) Printf(format string, args ...interface{}) {
_, err := fmt.Fprintf(o.stdout, format, args...)
func Printf(format string, args ...interface{}) {
_, err := fmt.Fprintf(globalOptions.stdout, format, args...)
if err != nil {
fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err)
os.Exit(100)
@ -118,22 +137,12 @@ func (o GlobalOptions) Printf(format string, args ...interface{}) {
}
// Verbosef calls Printf to write the message when the verbose flag is set.
func (o GlobalOptions) Verbosef(format string, args ...interface{}) {
if o.Quiet {
func Verbosef(format string, args ...interface{}) {
if globalOptions.Quiet {
return
}
o.Printf(format, args...)
}
// ShowProgress returns true iff the progress status should be written, i.e.
// the quiet flag is not set.
func (o GlobalOptions) ShowProgress() bool {
if o.Quiet {
return false
}
return true
Printf(format, args...)
}
// PrintProgress wraps fmt.Printf to handle the difference in writing progress
@ -162,8 +171,8 @@ func PrintProgress(format string, args ...interface{}) {
}
// Warnf writes the message to the configured stderr stream.
func (o GlobalOptions) Warnf(format string, args ...interface{}) {
_, err := fmt.Fprintf(o.stderr, format, args...)
func Warnf(format string, args ...interface{}) {
_, err := fmt.Fprintf(globalOptions.stderr, format, args...)
if err != nil {
fmt.Fprintf(os.Stderr, "unable to write to stderr: %v\n", err)
os.Exit(100)
@ -171,12 +180,12 @@ func (o GlobalOptions) Warnf(format string, args ...interface{}) {
}
// Exitf uses Warnf to write the message and then calls os.Exit(exitcode).
func (o GlobalOptions) Exitf(exitcode int, format string, args ...interface{}) {
func Exitf(exitcode int, format string, args ...interface{}) {
if format[len(format)-1] != '\n' {
format += "\n"
}
o.Warnf(format, args...)
Warnf(format, args...)
os.Exit(exitcode)
}
@ -210,9 +219,9 @@ func readPasswordTerminal(in *os.File, out io.Writer, prompt string) (password s
// ReadPassword reads the password from a password file, the environment
// variable RESTIC_PASSWORD or prompts the user.
func (o GlobalOptions) ReadPassword(prompt string) (string, error) {
if o.PasswordFile != "" {
s, err := ioutil.ReadFile(o.PasswordFile)
func ReadPassword(opts GlobalOptions, prompt string) (string, error) {
if opts.PasswordFile != "" {
s, err := ioutil.ReadFile(opts.PasswordFile)
return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile")
}
@ -244,12 +253,12 @@ func (o GlobalOptions) ReadPassword(prompt string) (string, error) {
// ReadPasswordTwice calls ReadPassword two times and returns an error when the
// passwords don't match.
func (o GlobalOptions) ReadPasswordTwice(prompt1, prompt2 string) (string, error) {
pw1, err := o.ReadPassword(prompt1)
func ReadPasswordTwice(gopts GlobalOptions, prompt1, prompt2 string) (string, error) {
pw1, err := ReadPassword(gopts, prompt1)
if err != nil {
return "", err
}
pw2, err := o.ReadPassword(prompt2)
pw2, err := ReadPassword(gopts, prompt2)
if err != nil {
return "", err
}
@ -264,26 +273,26 @@ func (o GlobalOptions) ReadPasswordTwice(prompt1, prompt2 string) (string, error
const maxKeys = 20
// OpenRepository reads the password and opens the repository.
func (o GlobalOptions) OpenRepository() (*repository.Repository, error) {
if o.Repo == "" {
func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
if opts.Repo == "" {
return nil, errors.Fatal("Please specify repository location (-r)")
}
be, err := open(o.Repo)
be, err := open(opts.Repo)
if err != nil {
return nil, err
}
s := repository.New(be)
if o.password == "" {
o.password, err = o.ReadPassword("enter password for repository: ")
if opts.password == "" {
opts.password, err = ReadPassword(opts, "enter password for repository: ")
if err != nil {
return nil, err
}
}
err = s.SearchKey(o.password, maxKeys)
err = s.SearchKey(opts.password, maxKeys)
if err != nil {
return nil, errors.Fatalf("unable to open repo: %v", err)
}

View File

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

View File

@ -166,18 +166,6 @@ type testEnvironment struct {
base, cache, repo, testdata string
}
func configureRestic(t testing.TB, cache, repo string) GlobalOptions {
return GlobalOptions{
CacheDir: cache,
Repo: repo,
Quiet: true,
password: TestPassword,
stdout: os.Stdout,
stderr: os.Stderr,
}
}
// withTestEnvironment creates a test environment and calls f with it. After f has
// returned, the temporary directory is removed.
func withTestEnvironment(t testing.TB, f func(*testEnvironment, GlobalOptions)) {
@ -201,7 +189,18 @@ func withTestEnvironment(t testing.TB, f func(*testEnvironment, GlobalOptions))
OK(t, os.MkdirAll(env.cache, 0700))
OK(t, os.MkdirAll(env.repo, 0700))
f(&env, configureRestic(t, env.cache, env.repo))
gopts := GlobalOptions{
Repo: env.repo,
Quiet: true,
password: TestPassword,
stdout: os.Stdout,
stderr: os.Stderr,
}
// always overwrite global options
globalOptions = gopts
f(&env, gopts)
if !TestCleanupTempDirs {
t.Logf("leaving temporary directory %v used for test", tempdir)

View File

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

View File

@ -7,11 +7,24 @@ import (
"restic/debug"
"runtime"
"restic/errors"
"github.com/spf13/cobra"
"github.com/jessevdk/go-flags"
"restic/errors"
)
// cmdRoot is the base command when no other command has been specified.
var cmdRoot = &cobra.Command{
Use: "restic",
Short: "backup and restore files",
Long: `
restic is a backup program which allows saving multiple revisions of files and
directories in an encrypted repository stored on different backends.
`,
SilenceErrors: true,
SilenceUsage: true,
PersistentPreRun: parseEnvironment,
}
func init() {
// set GOMAXPROCS to number of CPUs
if runtime.Version() < "go1.5" {
@ -21,23 +34,11 @@ func init() {
runtime.GOMAXPROCS(runtime.NumCPU())
}
}
}
func main() {
// defer profile.Start(profile.MemProfileRate(100000), profile.ProfilePath(".")).Stop()
// defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop()
globalOpts.Repo = os.Getenv("RESTIC_REPOSITORY")
debug.Log("restic", "main %#v", os.Args)
_, err := parser.Parse()
if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp {
parser.WriteHelp(os.Stdout)
os.Exit(0)
}
debug.Log("main", "command returned error: %#v", err)
err := cmdRoot.Execute()
switch {
case restic.IsAlreadyLocked(errors.Cause(err)):

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
func SamePaths(expected, actual []string) bool {
if expected == nil || actual == nil {
if len(expected) == 0 || len(actual) == 0 {
return true
}

View File

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