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