diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index d8f2a3509..86bde9dbb 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -16,13 +16,15 @@ import ( type CmdBackup struct { Parent string `short:"p" long:"parent" description:"use this parent snapshot (default: last snapshot in repo that has the same target)"` Force bool `short:"f" long:"force" description:"Force re-reading the target. Overrides the \"parent\" flag"` + + global *GlobalOptions } func init() { _, err := parser.AddCommand("backup", "save file/directory", "The backup command creates a snapshot of a file or directory", - &CmdBackup{}) + &CmdBackup{global: &globalOpts}) if err != nil { panic(err) } @@ -97,8 +99,8 @@ func (cmd CmdBackup) Usage() string { return "DIR/FILE [DIR/FILE] [...]" } -func newScanProgress() *restic.Progress { - if disableProgress() { +func (cmd CmdBackup) newScanProgress() *restic.Progress { + if !cmd.global.ShowProgress() { return nil } @@ -113,8 +115,8 @@ func newScanProgress() *restic.Progress { return p } -func newArchiveProgress(todo restic.Stat) *restic.Progress { - if disableProgress() { +func (cmd CmdBackup) newArchiveProgress(todo restic.Stat) *restic.Progress { + if !cmd.global.ShowProgress() { return nil } @@ -213,7 +215,7 @@ func (cmd CmdBackup) Execute(args []string) error { target = append(target, d) } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } @@ -232,7 +234,7 @@ func (cmd CmdBackup) Execute(args []string) error { return fmt.Errorf("invalid id %q: %v", cmd.Parent, err) } - verbosePrintf("found parent snapshot %v\n", parentSnapshotID.Str()) + cmd.global.Printf("found parent snapshot %v\n", parentSnapshotID.Str()) } // Find last snapshot to set it as parent, if not already set @@ -243,13 +245,13 @@ func (cmd CmdBackup) Execute(args []string) error { } if parentSnapshotID != nil { - verbosePrintf("using parent snapshot %v\n", parentSnapshotID) + cmd.global.Printf("using parent snapshot %v\n", parentSnapshotID) } } - verbosePrintf("scan %v\n", target) + cmd.global.Printf("scan %v\n", target) - stat, err := restic.Scan(target, newScanProgress()) + stat, err := restic.Scan(target, cmd.newScanProgress()) // TODO: add filter // arch.Filter = func(dir string, fi os.FileInfo) bool { @@ -264,12 +266,12 @@ func (cmd CmdBackup) Execute(args []string) error { return nil } - _, id, err := arch.Snapshot(newArchiveProgress(stat), target, parentSnapshotID) + _, id, err := arch.Snapshot(cmd.newArchiveProgress(stat), target, parentSnapshotID) if err != nil { return err } - verbosePrintf("snapshot %s saved\n", id.Str()) + cmd.global.Printf("snapshot %s saved\n", id.Str()) return nil } diff --git a/cmd/restic/cmd_cache.go b/cmd/restic/cmd_cache.go index dfbe52227..26a6b7970 100644 --- a/cmd/restic/cmd_cache.go +++ b/cmd/restic/cmd_cache.go @@ -6,13 +6,15 @@ import ( "github.com/restic/restic" ) -type CmdCache struct{} +type CmdCache struct { + global *GlobalOptions +} func init() { _, err := parser.AddCommand("cache", "manage cache", "The cache command creates and manages the local cache", - &CmdCache{}) + &CmdCache{global: &globalOpts}) if err != nil { panic(err) } @@ -27,12 +29,12 @@ func (cmd CmdCache) Execute(args []string) error { // return fmt.Errorf("wrong number of parameters, Usage: %s", cmd.Usage()) // } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } - cache, err := restic.NewCache(s, mainOpts.CacheDir) + cache, err := restic.NewCache(s, cmd.global.CacheDir) if err != nil { return err } diff --git a/cmd/restic/cmd_cat.go b/cmd/restic/cmd_cat.go index 65e60c51f..65345cfe7 100644 --- a/cmd/restic/cmd_cat.go +++ b/cmd/restic/cmd_cat.go @@ -14,13 +14,15 @@ import ( "github.com/restic/restic/repository" ) -type CmdCat struct{} +type CmdCat struct { + global *GlobalOptions +} func init() { _, err := parser.AddCommand("cat", "dump something", "The cat command dumps data structures or data from a repository", - &CmdCat{}) + &CmdCat{global: &globalOpts}) if err != nil { panic(err) } @@ -35,7 +37,7 @@ func (cmd CmdCat) Execute(args []string) error { return fmt.Errorf("type or ID not specified, Usage: %s", cmd.Usage()) } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } diff --git a/cmd/restic/cmd_dump.go b/cmd/restic/cmd_dump.go index 7e7a54293..bf5f8db86 100644 --- a/cmd/restic/cmd_dump.go +++ b/cmd/restic/cmd_dump.go @@ -15,13 +15,15 @@ import ( "github.com/restic/restic/repository" ) -type CmdDump struct{} +type CmdDump struct { + global *MainOptions +} func init() { _, err := parser.AddCommand("dump", "dump data structures", "The dump command dumps data structures from a repository as JSON documents", - &CmdDump{}) + &CmdDump{global: &mainOpts}) if err != nil { panic(err) } @@ -102,7 +104,7 @@ func (cmd CmdDump) Execute(args []string) error { return fmt.Errorf("type not specified, Usage: %s", cmd.Usage()) } - repo, err := OpenRepo() + repo, err := cmd.global.OpenRepository() if err != nil { return err } diff --git a/cmd/restic/cmd_find.go b/cmd/restic/cmd_find.go index 932d596cf..c1a56ebc6 100644 --- a/cmd/restic/cmd_find.go +++ b/cmd/restic/cmd_find.go @@ -23,6 +23,7 @@ type CmdFind struct { oldest, newest time.Time pattern string + global *GlobalOptions } var timeFormats = []string{ @@ -43,7 +44,7 @@ func init() { _, err := parser.AddCommand("find", "find a file/directory", "The find command searches for files or directories in snapshots", - &CmdFind{}) + &CmdFind{global: &globalOpts}) if err != nil { panic(err) } @@ -156,7 +157,7 @@ func (c CmdFind) Execute(args []string) error { } } - s, err := OpenRepo() + s, err := c.global.OpenRepository() if err != nil { return err } diff --git a/cmd/restic/cmd_fsck.go b/cmd/restic/cmd_fsck.go index 1f84a3b6e..17a0608ce 100644 --- a/cmd/restic/cmd_fsck.go +++ b/cmd/restic/cmd_fsck.go @@ -19,6 +19,8 @@ type CmdFsck struct { Orphaned bool `short:"o" long:"orphaned" description:"Check for orphaned blobs"` RemoveOrphaned bool `short:"r" long:"remove-orphaned" description:"Remove orphaned blobs (implies -o)"` + global *GlobalOptions + // lists checking for orphaned blobs o_data *backend.IDSet o_trees *backend.IDSet @@ -28,13 +30,13 @@ func init() { _, err := parser.AddCommand("fsck", "check the repository", "The fsck command check the integrity and consistency of the repository", - &CmdFsck{}) + &CmdFsck{global: &globalOpts}) if err != nil { panic(err) } } -func fsckFile(opts CmdFsck, repo *repository.Repository, IDs []backend.ID) (uint64, error) { +func fsckFile(global CmdFsck, repo *repository.Repository, IDs []backend.ID) (uint64, error) { debug.Log("restic.fsckFile", "checking file %v", IDs) var bytes uint64 @@ -50,7 +52,7 @@ func fsckFile(opts CmdFsck, repo *repository.Repository, IDs []backend.ID) (uint bytes += uint64(length - crypto.Extension) debug.Log("restic.fsck", " blob found in pack %v\n", packID) - if opts.CheckData { + if global.CheckData { // load content _, err := repo.LoadBlob(pack.Data, id) if err != nil { @@ -69,16 +71,16 @@ func fsckFile(opts CmdFsck, repo *repository.Repository, IDs []backend.ID) (uint } // if orphan check is active, record storage id - if opts.o_data != nil { + if global.o_data != nil { debug.Log("restic.fsck", " recording blob %v as used\n", id) - opts.o_data.Insert(id) + global.o_data.Insert(id) } } return bytes, nil } -func fsckTree(opts CmdFsck, repo *repository.Repository, id backend.ID) error { +func fsckTree(global CmdFsck, repo *repository.Repository, id backend.ID) error { debug.Log("restic.fsckTree", "checking tree %v", id.Str()) tree, err := restic.LoadTree(repo, id) @@ -87,9 +89,9 @@ func fsckTree(opts CmdFsck, repo *repository.Repository, id backend.ID) error { } // if orphan check is active, record storage id - if opts.o_trees != nil { + if global.o_trees != nil { // add ID to list - opts.o_trees.Insert(id) + global.o_trees.Insert(id) } var firstErr error @@ -123,7 +125,7 @@ func fsckTree(opts CmdFsck, repo *repository.Repository, id backend.ID) error { } debug.Log("restic.fsckTree", "check file %v (%v)", node.Name, id.Str()) - bytes, err := fsckFile(opts, repo, node.Content) + bytes, err := fsckFile(global, repo, node.Content) if err != nil { return err } @@ -140,7 +142,7 @@ func fsckTree(opts CmdFsck, repo *repository.Repository, id backend.ID) error { // record id seenIDs.Insert(node.Subtree) - err = fsckTree(opts, repo, node.Subtree) + err = fsckTree(global, repo, node.Subtree) if err != nil { firstErr = err fmt.Fprintf(os.Stderr, "%v\n", err) @@ -158,7 +160,7 @@ func fsckTree(opts CmdFsck, repo *repository.Repository, id backend.ID) error { return firstErr } -func fsckSnapshot(opts CmdFsck, repo *repository.Repository, id backend.ID) error { +func fsckSnapshot(global CmdFsck, repo *repository.Repository, id backend.ID) error { debug.Log("restic.fsck", "checking snapshot %v\n", id) sn, err := restic.LoadSnapshot(repo, id) @@ -166,7 +168,7 @@ func fsckSnapshot(opts CmdFsck, repo *repository.Repository, id backend.ID) erro return fmt.Errorf("loading snapshot %v failed: %v", id, err) } - err = fsckTree(opts, repo, sn.Tree) + err = fsckTree(global, repo, sn.Tree) if err != nil { debug.Log("restic.fsck", " checking tree %v for snapshot %v\n", sn.Tree, id) fmt.Fprintf(os.Stderr, "snapshot %v:\n error for tree %v:\n %v\n", id, sn.Tree, err) @@ -188,7 +190,7 @@ func (cmd CmdFsck) Execute(args []string) error { cmd.Orphaned = true } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } diff --git a/cmd/restic/cmd_init.go b/cmd/restic/cmd_init.go new file mode 100644 index 000000000..1e75ee5f9 --- /dev/null +++ b/cmd/restic/cmd_init.go @@ -0,0 +1,52 @@ +package main + +import ( + "errors" + + "github.com/restic/restic/repository" +) + +type CmdInit struct { + global *GlobalOptions +} + +func (cmd CmdInit) Execute(args []string) error { + if cmd.global.Repo == "" { + return errors.New("Please specify repository location (-r)") + } + + if cmd.global.password == "" { + cmd.global.password = cmd.global.ReadPasswordTwice( + "enter password for new backend: ", + "enter password again: ") + } + + be, err := create(cmd.global.Repo) + if err != nil { + cmd.global.Exitf(1, "creating backend at %s failed: %v\n", cmd.global.Repo, err) + } + + s := repository.New(be) + err = s.Init(cmd.global.password) + if err != nil { + cmd.global.Exitf(1, "creating key in backend at %s failed: %v\n", cmd.global.Repo, err) + } + + cmd.global.Printf("created restic backend %v at %s\n", s.Config.ID[:10], cmd.global.Repo) + cmd.global.Printf("\n") + cmd.global.Printf("Please note that knowledge of your password is required to access\n") + cmd.global.Printf("the repository. Losing your password means that your data is\n") + cmd.global.Printf("irrecoverably lost.\n") + + return nil +} + +func init() { + _, err := parser.AddCommand("init", + "create repository", + "The init command creates a new repository", + &CmdInit{global: &globalOpts}) + if err != nil { + panic(err) + } +} diff --git a/cmd/restic/cmd_key.go b/cmd/restic/cmd_key.go index 86e390eef..a110357aa 100644 --- a/cmd/restic/cmd_key.go +++ b/cmd/restic/cmd_key.go @@ -9,19 +9,21 @@ import ( "github.com/restic/restic/repository" ) -type CmdKey struct{} +type CmdKey struct { + global *GlobalOptions +} func init() { _, err := parser.AddCommand("key", "manage keys", "The key command manages keys (passwords) of a repository", - &CmdKey{}) + &CmdKey{global: &globalOpts}) if err != nil { panic(err) } } -func listKeys(s *repository.Repository) error { +func (cmd CmdKey) listKeys(s *repository.Repository) error { tab := NewTable() tab.Header = fmt.Sprintf(" %-10s %-10s %-10s %s", "ID", "User", "Host", "Created") tab.RowFormat = "%s%-10s %-10s %-10s %s" @@ -56,23 +58,20 @@ func listKeys(s *repository.Repository) error { return nil } -func getNewPassword() (string, error) { +func (cmd CmdKey) getNewPassword() (string, error) { newPassword := os.Getenv("RESTIC_NEWPASSWORD") if newPassword == "" { - newPassword = readPassword("enter password for new key: ") - newPassword2 := readPassword("enter password again: ") - - if newPassword != newPassword2 { - return "", errors.New("passwords do not match") - } + newPassword = cmd.global.ReadPasswordTwice( + "enter password for new key: ", + "enter password again: ") } return newPassword, nil } -func addKey(repo *repository.Repository) error { - newPassword, err := getNewPassword() +func (cmd CmdKey) addKey(repo *repository.Repository) error { + newPassword, err := cmd.getNewPassword() if err != nil { return err } @@ -87,7 +86,7 @@ func addKey(repo *repository.Repository) error { return nil } -func deleteKey(repo *repository.Repository, name string) error { +func (cmd CmdKey) deleteKey(repo *repository.Repository, name string) error { if name == repo.KeyName() { return errors.New("refusing to remove key currently used to access repository") } @@ -101,8 +100,8 @@ func deleteKey(repo *repository.Repository, name string) error { return nil } -func changePassword(repo *repository.Repository) error { - newPassword, err := getNewPassword() +func (cmd CmdKey) changePassword(repo *repository.Repository) error { + newPassword, err := cmd.getNewPassword() if err != nil { return err } @@ -131,25 +130,25 @@ func (cmd CmdKey) Execute(args []string) error { return fmt.Errorf("wrong number of arguments, Usage: %s", cmd.Usage()) } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } switch args[0] { case "list": - return listKeys(s) + return cmd.listKeys(s) case "add": - return addKey(s) + return cmd.addKey(s) case "rm": id, err := backend.Find(s.Backend(), backend.Key, args[1]) if err != nil { return err } - return deleteKey(s, id) + return cmd.deleteKey(s, id) case "passwd": - return changePassword(s) + return cmd.changePassword(s) } return nil diff --git a/cmd/restic/cmd_list.go b/cmd/restic/cmd_list.go index 641d72c2e..4d5cf0ab1 100644 --- a/cmd/restic/cmd_list.go +++ b/cmd/restic/cmd_list.go @@ -10,14 +10,15 @@ import ( ) type CmdList struct { - w io.Writer + w io.Writer + global *GlobalOptions } func init() { _, err := parser.AddCommand("list", "lists data", "The list command lists structures or data of a repository", - &CmdList{}) + &CmdList{global: &globalOpts}) if err != nil { panic(err) } @@ -36,7 +37,7 @@ func (cmd CmdList) Execute(args []string) error { return fmt.Errorf("type not specified, Usage: %s", cmd.Usage()) } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index 1f75b515c..59144befc 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -10,13 +10,15 @@ import ( "github.com/restic/restic/repository" ) -type CmdLs struct{} +type CmdLs struct { + global *GlobalOptions +} func init() { _, err := parser.AddCommand("ls", "list files", "The ls command lists all files and directories in a snapshot", - &CmdLs{}) + &CmdLs{global: &globalOpts}) if err != nil { panic(err) } @@ -67,7 +69,7 @@ func (cmd CmdLs) Execute(args []string) error { return fmt.Errorf("wrong number of arguments, Usage: %s", cmd.Usage()) } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index cd91228e6..183798932 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -8,13 +8,15 @@ import ( "github.com/restic/restic" ) -type CmdRestore struct{} +type CmdRestore struct { + global *GlobalOptions +} func init() { _, err := parser.AddCommand("restore", "restore a snapshot", "The restore command restores a snapshot to a directory", - &CmdRestore{}) + &CmdRestore{global: &globalOpts}) if err != nil { panic(err) } @@ -29,7 +31,7 @@ func (cmd CmdRestore) Execute(args []string) error { return fmt.Errorf("wrong number of arguments, Usage: %s", cmd.Usage()) } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } @@ -41,7 +43,7 @@ func (cmd CmdRestore) Execute(args []string) error { id, err := restic.FindSnapshot(s, args[0]) if err != nil { - errx(1, "invalid id %q: %v", args[0], err) + cmd.global.Exitf(1, "invalid id %q: %v", args[0], err) } target := args[1] @@ -81,7 +83,7 @@ func (cmd CmdRestore) Execute(args []string) error { } } - verbosePrintf("restoring %s to %s\n", res.Snapshot(), target) + cmd.global.Printf("restoring %s to %s\n", res.Snapshot(), target) err = res.RestoreTo(target) if err != nil { diff --git a/cmd/restic/cmd_snapshots.go b/cmd/restic/cmd_snapshots.go index 3703884b2..f1f8ba31e 100644 --- a/cmd/restic/cmd_snapshots.go +++ b/cmd/restic/cmd_snapshots.go @@ -71,13 +71,15 @@ func reltime(t time.Time) string { } } -type CmdSnapshots struct{} +type CmdSnapshots struct { + global *GlobalOptions +} func init() { _, err := parser.AddCommand("snapshots", "show snapshots", "The snapshots command lists all snapshots stored in a repository", - &CmdSnapshots{}) + &CmdSnapshots{global: &globalOpts}) if err != nil { panic(err) } @@ -92,7 +94,7 @@ func (cmd CmdSnapshots) Execute(args []string) error { return fmt.Errorf("wrong number of arguments, usage: %s", cmd.Usage()) } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } diff --git a/cmd/restic/global.go b/cmd/restic/global.go new file mode 100644 index 000000000..2f7ffe27e --- /dev/null +++ b/cmd/restic/global.go @@ -0,0 +1,162 @@ +package main + +import ( + "errors" + "fmt" + "io" + "net/url" + "os" + + "github.com/jessevdk/go-flags" + "github.com/restic/restic/backend" + "github.com/restic/restic/backend/local" + "github.com/restic/restic/backend/sftp" + "github.com/restic/restic/repository" + "golang.org/x/crypto/ssh/terminal" +) + +var version = "compiled manually" + +type GlobalOptions struct { + Repo string `short:"r" long:"repo" description:"Repository directory to backup to/restore from"` + CacheDir string ` long:"cache-dir" description:"Directory to use as a local cache"` + Quiet bool `short:"q" long:"quiet" default:"false" description:"Do not output comprehensive progress report"` + + password string + stdout io.Writer +} + +var globalOpts = GlobalOptions{stdout: os.Stdout} +var parser = flags.NewParser(&globalOpts, flags.Default) + +func (o GlobalOptions) Printf(format string, args ...interface{}) { + if o.Quiet { + return + } + + _, err := fmt.Fprintf(o.stdout, format, args...) + if err != nil { + fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err) + os.Exit(100) + } +} + +func (o GlobalOptions) ShowProgress() bool { + if o.Quiet { + return false + } + + if !terminal.IsTerminal(int(os.Stdout.Fd())) { + return false + } + + return true +} + +func (o GlobalOptions) Warnf(format string, args ...interface{}) { + _, err := fmt.Fprintf(os.Stderr, format, args...) + if err != nil { + fmt.Fprintf(os.Stderr, "unable to write to stderr: %v\n", err) + os.Exit(100) + } +} + +func (o GlobalOptions) Exitf(exitcode int, format string, args ...interface{}) { + if format[len(format)-1] != '\n' { + format += "\n" + } + + o.Warnf(format, args...) + os.Exit(exitcode) +} + +func (o GlobalOptions) ReadPassword(prompt string) string { + fmt.Fprint(os.Stderr, prompt) + pw, err := terminal.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + o.Exitf(2, "unable to read password: %v", err) + } + fmt.Fprintln(os.Stderr) + + return string(pw) +} + +func (o GlobalOptions) ReadPasswordTwice(prompt1, prompt2 string) string { + pw1 := o.ReadPassword(prompt1) + pw2 := o.ReadPassword(prompt2) + if pw1 != pw2 { + o.Exitf(1, "passwords do not match") + } + + return pw1 +} + +func (o GlobalOptions) OpenRepository() (*repository.Repository, error) { + if o.Repo == "" { + return nil, errors.New("Please specify repository location (-r)") + } + + be, err := open(o.Repo) + if err != nil { + return nil, err + } + + s := repository.New(be) + + if o.password == "" { + o.password = o.ReadPassword("enter password for repository: ") + } + + err = s.SearchKey(o.password) + if err != nil { + return nil, fmt.Errorf("unable to open repo: %v", err) + } + + return s, nil +} + +// Open the backend specified by URI. +// Valid formats are: +// * /foo/bar -> local repository at /foo/bar +// * sftp://user@host/foo/bar -> remote sftp repository on host for user at path foo/bar +// * sftp://host//tmp/backup -> remote sftp repository on host at path /tmp/backup +func open(u string) (backend.Backend, error) { + url, err := url.Parse(u) + if err != nil { + return nil, err + } + + if url.Scheme == "" { + return local.Open(url.Path) + } + + args := []string{url.Host} + if url.User != nil && url.User.Username() != "" { + args = append(args, "-l") + args = append(args, url.User.Username()) + } + args = append(args, "-s") + args = append(args, "sftp") + return sftp.Open(url.Path[1:], "ssh", args...) +} + +// Create the backend specified by URI. +func create(u string) (backend.Backend, error) { + url, err := url.Parse(u) + if err != nil { + return nil, err + } + + if url.Scheme == "" { + return local.Create(url.Path) + } + + args := []string{url.Host} + if url.User != nil && url.User.Username() != "" { + args = append(args, "-l") + args = append(args, url.User.Username()) + } + args = append(args, "-s") + args = append(args, "sftp") + return sftp.Create(url.Path[1:], "ssh", args...) +} diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go index c93f2ed77..ee025f955 100644 --- a/cmd/restic/integration_helpers_test.go +++ b/cmd/restic/integration_helpers_test.go @@ -177,11 +177,11 @@ type testEnvironment struct { } func configureRestic(t testing.TB, cache, repo string) { - mainOpts.CacheDir = cache - mainOpts.Repo = repo - mainOpts.Quiet = true + globalOpts.CacheDir = cache + globalOpts.Repo = repo + globalOpts.Quiet = true - mainOpts.password = TestPassword + globalOpts.password = TestPassword } func cleanupTempdir(t testing.TB, tempdir string) { diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index 143824bc5..d61432b6a 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -49,7 +49,7 @@ func cmdInit(t testing.TB) { cmd := &CmdInit{} OK(t, cmd.Execute(nil)) - t.Logf("repository initialized at %v", mainOpts.Repo) + t.Logf("repository initialized at %v", globalOpts.Repo) } func cmdBackup(t testing.TB, target []string, parentID backend.ID) { diff --git a/cmd/restic/main.go b/cmd/restic/main.go index 812b57d67..8669ae8d6 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -1,209 +1,23 @@ package main import ( - "errors" - "fmt" - "net/url" "os" "runtime" - "golang.org/x/crypto/ssh/terminal" - "github.com/jessevdk/go-flags" - "github.com/restic/restic/backend" - "github.com/restic/restic/backend/local" - "github.com/restic/restic/backend/sftp" "github.com/restic/restic/debug" - "github.com/restic/restic/repository" ) -var version = "compiled manually" - -var mainOpts struct { - Repo string `short:"r" long:"repo" description:"Repository directory to backup to/restore from"` - CacheDir string ` long:"cache-dir" description:"Directory to use as a local cache"` - Quiet bool `short:"q" long:"quiet" default:"false" description:"Do not output comprehensive progress report"` - - password string -} - -var parser = flags.NewParser(&mainOpts, flags.Default) - -func errx(code int, format string, data ...interface{}) { - if len(format) > 0 && format[len(format)-1] != '\n' { - format += "\n" - } - fmt.Fprintf(os.Stderr, format, data...) - os.Exit(code) -} - -func readPassword(prompt string) string { - fmt.Fprint(os.Stderr, prompt) - pw, err := terminal.ReadPassword(int(os.Stdin.Fd())) - if err != nil { - errx(2, "unable to read password: %v", err) - } - fmt.Fprintln(os.Stderr) - - return string(pw) -} - -func disableProgress() bool { - if mainOpts.Quiet { - return true - } - - if !terminal.IsTerminal(int(os.Stdout.Fd())) { - return true - } - - return false -} - -func silenceRequested() bool { - if mainOpts.Quiet { - return true - } - - return false -} - -func verbosePrintf(format string, args ...interface{}) { - if silenceRequested() { - return - } - - fmt.Printf(format, args...) -} - -type CmdInit struct{} - -func (cmd CmdInit) Execute(args []string) error { - if mainOpts.Repo == "" { - return errors.New("Please specify repository location (-r)") - } - - if mainOpts.password == "" { - pw := readPassword("enter password for new backend: ") - pw2 := readPassword("enter password again: ") - - if pw != pw2 { - errx(1, "passwords do not match") - } - - mainOpts.password = pw - } - - be, err := create(mainOpts.Repo) - if err != nil { - fmt.Fprintf(os.Stderr, "creating backend at %s failed: %v\n", mainOpts.Repo, err) - os.Exit(1) - } - - s := repository.New(be) - err = s.Init(mainOpts.password) - if err != nil { - fmt.Fprintf(os.Stderr, "creating key in backend at %s failed: %v\n", mainOpts.Repo, err) - os.Exit(1) - } - - verbosePrintf("created restic backend %v at %s\n", s.Config.ID[:10], mainOpts.Repo) - verbosePrintf("\n") - verbosePrintf("Please note that knowledge of your password is required to access\n") - verbosePrintf("the repository. Losing your password means that your data is\n") - verbosePrintf("irrecoverably lost.\n") - - return nil -} - -// Open the backend specified by URI. -// Valid formats are: -// * /foo/bar -> local repository at /foo/bar -// * sftp://user@host/foo/bar -> remote sftp repository on host for user at path foo/bar -// * sftp://host//tmp/backup -> remote sftp repository on host at path /tmp/backup -func open(u string) (backend.Backend, error) { - url, err := url.Parse(u) - if err != nil { - return nil, err - } - - if url.Scheme == "" { - return local.Open(url.Path) - } - - args := []string{url.Host} - if url.User != nil && url.User.Username() != "" { - args = append(args, "-l") - args = append(args, url.User.Username()) - } - args = append(args, "-s") - args = append(args, "sftp") - return sftp.Open(url.Path[1:], "ssh", args...) -} - -// Create the backend specified by URI. -func create(u string) (backend.Backend, error) { - url, err := url.Parse(u) - if err != nil { - return nil, err - } - - if url.Scheme == "" { - return local.Create(url.Path) - } - - args := []string{url.Host} - if url.User != nil && url.User.Username() != "" { - args = append(args, "-l") - args = append(args, url.User.Username()) - } - args = append(args, "-s") - args = append(args, "sftp") - return sftp.Create(url.Path[1:], "ssh", args...) -} - -func OpenRepo() (*repository.Repository, error) { - if mainOpts.Repo == "" { - return nil, errors.New("Please specify repository location (-r)") - } - - be, err := open(mainOpts.Repo) - if err != nil { - return nil, err - } - - s := repository.New(be) - - if mainOpts.password == "" { - mainOpts.password = readPassword("enter password for repository: ") - } - - err = s.SearchKey(mainOpts.password) - if err != nil { - return nil, fmt.Errorf("unable to open repo: %v", err) - } - - return s, nil -} - func init() { // set GOMAXPROCS to number of CPUs runtime.GOMAXPROCS(runtime.NumCPU()) - - _, err := parser.AddCommand("init", - "create repository", - "The init command creates a new repository", - &CmdInit{}) - if err != nil { - panic(err) - } } func main() { // defer profile.Start(profile.MemProfileRate(100000), profile.ProfilePath(".")).Stop() // defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop() - mainOpts.Repo = os.Getenv("RESTIC_REPOSITORY") - mainOpts.password = os.Getenv("RESTIC_PASSWORD") + globalOpts.Repo = os.Getenv("RESTIC_REPOSITORY") + globalOpts.password = os.Getenv("RESTIC_PASSWORD") debug.Log("restic", "main %#v", os.Args)