mirror of
https://github.com/octoleo/restic.git
synced 2024-11-26 23:06:32 +00:00
Use cobra for all commands
This commit is contained in:
parent
3806623c23
commit
565d72ef36
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,22 +166,23 @@ 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
|
||||
|
||||
if !gopts.NoLock {
|
||||
lock, err := lockRepo(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = repo.LoadIndex()
|
||||
if err != nil {
|
||||
@ -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
|
||||
}
|
||||
|
@ -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,71 +137,72 @@ 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
|
||||
}
|
||||
|
||||
if !gopts.NoLock {
|
||||
lock, err := lockRepo(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = repo.LoadIndex()
|
||||
if err != nil {
|
||||
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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -2,86 +2,59 @@ 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
|
||||
}
|
||||
_, 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"
|
||||
|
||||
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
|
||||
}
|
||||
var snapshotOptions SnapshotOptions
|
||||
|
||||
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)
|
||||
}
|
||||
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 (cmd CmdSnapshots) Usage() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (cmd CmdSnapshots) Execute(args []string) error {
|
||||
func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) error {
|
||||
if len(args) != 0 {
|
||||
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
|
||||
}
|
||||
|
||||
if !gopts.NoLock {
|
||||
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")
|
||||
@ -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)
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -3,23 +3,23 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type CmdVersion struct{}
|
||||
|
||||
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 {
|
||||
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)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmdRoot.AddCommand(versionCmd)
|
||||
}
|
||||
|
60
src/cmds/restic/format.go
Normal file
60
src/cmds/restic/format.go
Normal 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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
// +build ignore
|
||||
// +build !openbsd
|
||||
// +build !windows
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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])
|
||||
})
|
||||
}
|
||||
|
@ -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
42
src/cmds/restic/table.go
Normal 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"
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -82,7 +82,7 @@ func (e ExpirePolicy) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
empty := ExpirePolicy{}
|
||||
empty := ExpirePolicy{Tags: e.Tags}
|
||||
return reflect.DeepEqual(e, empty)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user