summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorFélix Sipma <felix+debian@gueux.org>2019-01-09 12:28:30 +0100
committerFélix Sipma <felix+debian@gueux.org>2019-01-09 12:28:30 +0100
commit3ca26846d6c7013885026f21841c280c4964010d (patch)
tree68930aea204c99c8a04b1a5a70c1a17eaf40687c /cmd
parent9d6267b03f1f9d84b6c7e2f7e991b26cef89bc0c (diff)
New upstream version 0.9.4+ds
Diffstat (limited to 'cmd')
-rw-r--r--cmd/restic/cmd_backup.go65
-rw-r--r--cmd/restic/cmd_dump.go7
-rw-r--r--cmd/restic/cmd_find.go2
-rw-r--r--cmd/restic/cmd_forget.go2
-rw-r--r--cmd/restic/cmd_key.go2
-rw-r--r--cmd/restic/cmd_ls.go27
-rw-r--r--cmd/restic/cmd_mount.go21
-rw-r--r--cmd/restic/cmd_recover.go148
-rw-r--r--cmd/restic/cmd_restore.go4
-rw-r--r--cmd/restic/cmd_self_update.go32
-rw-r--r--cmd/restic/cmd_snapshots.go4
-rw-r--r--cmd/restic/cmd_stats.go4
-rw-r--r--cmd/restic/format.go4
-rw-r--r--cmd/restic/global.go57
-rw-r--r--cmd/restic/main.go2
15 files changed, 290 insertions, 91 deletions
diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go
index 4dbae8dd5..fc24868a5 100644
--- a/cmd/restic/cmd_backup.go
+++ b/cmd/restic/cmd_backup.go
@@ -45,8 +45,12 @@ given as the arguments.
},
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
- if backupOptions.Stdin && backupOptions.FilesFrom == "-" {
- return errors.Fatal("cannot use both `--stdin` and `--files-from -`")
+ if backupOptions.Stdin {
+ for _, filename := range backupOptions.FilesFrom {
+ if filename == "-" {
+ return errors.Fatal("cannot use both `--stdin` and `--files-from -`")
+ }
+ }
}
var t tomb.Tomb
@@ -75,7 +79,7 @@ type BackupOptions struct {
StdinFilename string
Tags []string
Host string
- FilesFrom string
+ FilesFrom []string
TimeStamp string
WithAtime bool
}
@@ -97,11 +101,11 @@ func init() {
f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "file name to use when reading from stdin")
f.StringArrayVar(&backupOptions.Tags, "tag", nil, "add a `tag` for the new snapshot (can be specified multiple times)")
- f.StringVar(&backupOptions.Host, "host", "H", "set the `hostname` for the snapshot manually. To prevent an expensive rescan use the \"parent\" flag")
+ f.StringVarP(&backupOptions.Host, "host", "H", "", "set the `hostname` for the snapshot manually. To prevent an expensive rescan use the \"parent\" flag")
f.StringVar(&backupOptions.Host, "hostname", "", "set the `hostname` for the snapshot manually")
f.MarkDeprecated("hostname", "use --host")
- f.StringVar(&backupOptions.FilesFrom, "files-from", "", "read the files to backup from file (can be combined with file args)")
+ f.StringArrayVar(&backupOptions.FilesFrom, "files-from", nil, "read the files to backup from file (can be combined with file args/can be specified multiple times)")
f.StringVar(&backupOptions.TimeStamp, "time", "", "time of the backup (ex. '2012-11-01 22:08:41') (default: now)")
f.BoolVar(&backupOptions.WithAtime, "with-atime", false, "store the atime for all files and directories")
}
@@ -175,12 +179,16 @@ func readLinesFromFile(filename string) ([]string, error) {
// Check returns an error when an invalid combination of options was set.
func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error {
- if opts.FilesFrom == "-" && gopts.password == "" {
- return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD")
+ if gopts.password == "" {
+ for _, filename := range opts.FilesFrom {
+ if filename == "-" {
+ return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD")
+ }
+ }
}
if opts.Stdin {
- if opts.FilesFrom != "" {
+ if len(opts.FilesFrom) > 0 {
return errors.Fatal("--stdin and --files-from cannot be used together")
}
@@ -302,20 +310,25 @@ func collectTargets(opts BackupOptions, args []string) (targets []string, err er
return nil, nil
}
- fromfile, err := readLinesFromFile(opts.FilesFrom)
- if err != nil {
- return nil, err
- }
-
- // expand wildcards
var lines []string
- for _, line := range fromfile {
- var expanded []string
- expanded, err := filepath.Glob(line)
+ for _, file := range opts.FilesFrom {
+ fromfile, err := readLinesFromFile(file)
if err != nil {
- return nil, errors.WithMessage(err, fmt.Sprintf("pattern: %s", line))
+ return nil, err
+ }
+
+ // expand wildcards
+ for _, line := range fromfile {
+ var expanded []string
+ expanded, err := filepath.Glob(line)
+ if err != nil {
+ return nil, errors.WithMessage(err, fmt.Sprintf("pattern: %s", line))
+ }
+ if len(expanded) == 0 {
+ Warnf("pattern %q does not match any files, skipping\n", line)
+ }
+ lines = append(lines, expanded...)
}
- lines = append(lines, expanded...)
}
// merge files from files-from into normal args so we can reuse the normal
@@ -374,7 +387,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
timeStamp := time.Now()
if opts.TimeStamp != "" {
- timeStamp, err = time.Parse(TimeFormat, opts.TimeStamp)
+ timeStamp, err = time.ParseInLocation(TimeFormat, opts.TimeStamp, time.Local)
if err != nil {
return errors.Fatalf("error in time option: %v\n", err)
}
@@ -382,6 +395,12 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
var t tomb.Tomb
+ term.Print("open repository\n")
+ repo, err := OpenRepository(gopts)
+ if err != nil {
+ return err
+ }
+
p := ui.NewBackup(term, gopts.verbosity)
// use the terminal for stdout/stderr
@@ -403,12 +422,6 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
t.Go(func() error { return p.Run(t.Context(gopts.ctx)) })
- p.V("open repository")
- repo, err := OpenRepository(gopts)
- if err != nil {
- return err
- }
-
p.V("lock repository")
lock, err := lockRepo(repo)
defer unlockRepo(lock)
diff --git a/cmd/restic/cmd_dump.go b/cmd/restic/cmd_dump.go
index 39c861bff..a2e4fbe4a 100644
--- a/cmd/restic/cmd_dump.go
+++ b/cmd/restic/cmd_dump.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
+ "path"
"path/filepath"
"github.com/restic/restic/internal/debug"
@@ -47,12 +48,12 @@ func init() {
flags.StringArrayVar(&dumpOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"")
}
-func splitPath(path string) []string {
- d, f := filepath.Split(path)
+func splitPath(p string) []string {
+ d, f := path.Split(p)
if d == "" || d == "/" {
return []string{f}
}
- s := splitPath(filepath.Clean(d))
+ s := splitPath(path.Clean(d))
return append(s, f)
}
diff --git a/cmd/restic/cmd_find.go b/cmd/restic/cmd_find.go
index 5025e9d21..48b952f5d 100644
--- a/cmd/restic/cmd_find.go
+++ b/cmd/restic/cmd_find.go
@@ -213,7 +213,7 @@ func (s *statefulOutput) PrintObjectNormal(kind, id, nodepath, treeID string, sn
} else {
Printf(" ... path %s\n", nodepath)
}
- Printf(" ... in snapshot %s (%s)\n", sn.ID().Str(), sn.Time.Format(TimeFormat))
+ Printf(" ... in snapshot %s (%s)\n", sn.ID().Str(), sn.Time.Local().Format(TimeFormat))
}
func (s *statefulOutput) PrintObject(kind, id, nodepath, treeID string, sn *restic.Snapshot) {
diff --git a/cmd/restic/cmd_forget.go b/cmd/restic/cmd_forget.go
index 3b9862171..bafd540bd 100644
--- a/cmd/restic/cmd_forget.go
+++ b/cmd/restic/cmd_forget.go
@@ -59,7 +59,7 @@ func init() {
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.VarP(&forgetOptions.Within, "keep-within", "", "keep snapshots that are older than `duration` (eg. 1y5m7d) relative to the latest snapshot")
+ f.VarP(&forgetOptions.Within, "keep-within", "", "keep snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
f.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)")
f.StringVar(&forgetOptions.Host, "host", "", "only consider snapshots with the given `host`")
diff --git a/cmd/restic/cmd_key.go b/cmd/restic/cmd_key.go
index 900b29d50..38ad77790 100644
--- a/cmd/restic/cmd_key.go
+++ b/cmd/restic/cmd_key.go
@@ -59,7 +59,7 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
ID: id.Str(),
UserName: k.Username,
HostName: k.Hostname,
- Created: k.Created.Format(TimeFormat),
+ Created: k.Created.Local().Format(TimeFormat),
}
keys = append(keys, key)
diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go
index 64c7ea678..db26467cd 100644
--- a/cmd/restic/cmd_ls.go
+++ b/cmd/restic/cmd_ls.go
@@ -64,10 +64,8 @@ func init() {
type lsSnapshot struct {
*restic.Snapshot
-
ID *restic.ID `json:"id"`
ShortID string `json:"short_id"`
- Nodes []lsNode `json:"nodes"`
StructType string `json:"struct_type"` // "snapshot"
}
@@ -150,24 +148,22 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
var (
printSnapshot func(sn *restic.Snapshot)
printNode func(path string, node *restic.Node)
- printFinish func() error
)
if gopts.JSON {
- var lssnapshots []lsSnapshot
+ enc := json.NewEncoder(gopts.stdout)
printSnapshot = func(sn *restic.Snapshot) {
- lss := lsSnapshot{
+ enc.Encode(lsSnapshot{
Snapshot: sn,
ID: sn.ID(),
ShortID: sn.ID().Str(),
StructType: "snapshot",
- }
- lssnapshots = append(lssnapshots, lss)
+ })
}
printNode = func(path string, node *restic.Node) {
- lsn := lsNode{
+ enc.Encode(lsNode{
Name: node.Name,
Type: node.Type,
Path: path,
@@ -179,25 +175,15 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
AccessTime: node.AccessTime,
ChangeTime: node.ChangeTime,
StructType: "node",
- }
- s := &lssnapshots[len(lssnapshots)-1]
- s.Nodes = append(s.Nodes, lsn)
- }
-
- printFinish = func() error {
- return json.NewEncoder(gopts.stdout).Encode(lssnapshots)
+ })
}
} else {
- // default output methods
printSnapshot = func(sn *restic.Snapshot) {
Verbosef("snapshot %s of %v filtered by %v at %s):\n", sn.ID().Str(), sn.Paths, dirs, sn.Time)
}
printNode = func(path string, node *restic.Node) {
Printf("%s\n", formatNode(path, node, lsOptions.ListLong))
}
- printFinish = func() error {
- return nil
- }
}
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args[:1]) {
@@ -240,5 +226,6 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
return err
}
}
- return printFinish()
+
+ return nil
}
diff --git a/cmd/restic/cmd_mount.go b/cmd/restic/cmd_mount.go
index 3e6382192..39ff1a144 100644
--- a/cmd/restic/cmd_mount.go
+++ b/cmd/restic/cmd_mount.go
@@ -53,13 +53,14 @@ For details please see the documentation for time.Format() at:
// MountOptions collects all options for the mount command.
type MountOptions struct {
- OwnerRoot bool
- AllowRoot bool
- AllowOther bool
- Host string
- Tags restic.TagLists
- Paths []string
- SnapshotTemplate string
+ OwnerRoot bool
+ AllowRoot bool
+ AllowOther bool
+ NoDefaultPermissions bool
+ Host string
+ Tags restic.TagLists
+ Paths []string
+ SnapshotTemplate string
}
var mountOptions MountOptions
@@ -71,6 +72,7 @@ func init() {
mountFlags.BoolVar(&mountOptions.OwnerRoot, "owner-root", false, "use 'root' as the owner of files and dirs")
mountFlags.BoolVar(&mountOptions.AllowRoot, "allow-root", false, "allow root user to access the data in the mounted directory")
mountFlags.BoolVar(&mountOptions.AllowOther, "allow-other", false, "allow other users to access the data in the mounted directory")
+ mountFlags.BoolVar(&mountOptions.NoDefaultPermissions, "no-default-permissions", false, "for 'allow-other', ignore Unix permissions and allow users to read all snapshot files")
mountFlags.StringVarP(&mountOptions.Host, "host", "H", "", `only consider snapshots for this host`)
mountFlags.Var(&mountOptions.Tags, "tag", "only consider snapshots which include this `taglist`")
@@ -118,6 +120,11 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
if opts.AllowOther {
mountOptions = append(mountOptions, systemFuse.AllowOther())
+
+ // let the kernel check permissions unless it is explicitly disabled
+ if !opts.NoDefaultPermissions {
+ mountOptions = append(mountOptions, systemFuse.DefaultPermissions())
+ }
}
c, err := systemFuse.Mount(mountpoint, mountOptions...)
diff --git a/cmd/restic/cmd_recover.go b/cmd/restic/cmd_recover.go
new file mode 100644
index 000000000..677442be7
--- /dev/null
+++ b/cmd/restic/cmd_recover.go
@@ -0,0 +1,148 @@
+package main
+
+import (
+ "os"
+ "time"
+
+ "github.com/restic/restic/internal/errors"
+ "github.com/restic/restic/internal/restic"
+ "github.com/spf13/cobra"
+)
+
+var cmdRecover = &cobra.Command{
+ Use: "recover [flags]",
+ Short: "Recover data from the repository",
+ Long: `
+The "recover" command build a new snapshot from all directories it can find in
+the raw data of the repository. It can be used if, for example, a snapshot has
+been removed by accident with "forget".
+`,
+ DisableAutoGenTag: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return runRecover(globalOptions)
+ },
+}
+
+func init() {
+ cmdRoot.AddCommand(cmdRecover)
+}
+
+func runRecover(gopts GlobalOptions) error {
+ hostname, err := os.Hostname()
+ if err != nil {
+ return err
+ }
+
+ repo, err := OpenRepository(gopts)
+ if err != nil {
+ return err
+ }
+
+ lock, err := lockRepo(repo)
+ defer unlockRepo(lock)
+ if err != nil {
+ return err
+ }
+
+ Verbosef("load index files\n")
+ if err = repo.LoadIndex(gopts.ctx); err != nil {
+ return err
+ }
+
+ // trees maps a tree ID to whether or not it is referenced by a different
+ // tree. If it is not referenced, we have a root tree.
+ trees := make(map[restic.ID]bool)
+
+ for blob := range repo.Index().Each(gopts.ctx) {
+ if blob.Blob.Type != restic.TreeBlob {
+ continue
+ }
+ trees[blob.Blob.ID] = false
+ }
+
+ cur := 0
+ max := len(trees)
+ Verbosef("load %d trees\n\n", len(trees))
+
+ for id := range trees {
+ cur++
+ Verbosef("\rtree (%v/%v)", cur, max)
+
+ if !trees[id] {
+ trees[id] = false
+ }
+
+ tree, err := repo.LoadTree(gopts.ctx, id)
+ if err != nil {
+ Warnf("unable to load tree %v: %v\n", id.Str(), err)
+ continue
+ }
+
+ for _, node := range tree.Nodes {
+ if node.Type != "dir" || node.Subtree == nil {
+ continue
+ }
+
+ subtree := *node.Subtree
+ trees[subtree] = true
+ }
+ }
+ Verbosef("\ndone\n")
+
+ roots := restic.NewIDSet()
+ for id, seen := range trees {
+ if seen {
+ continue
+ }
+
+ roots.Insert(id)
+ }
+
+ Verbosef("found %d roots\n", len(roots))
+
+ tree := restic.NewTree()
+ for id := range roots {
+ var subtreeID = id
+ node := restic.Node{
+ Type: "dir",
+ Name: id.Str(),
+ Mode: 0755,
+ Subtree: &subtreeID,
+ AccessTime: time.Now(),
+ ModTime: time.Now(),
+ ChangeTime: time.Now(),
+ }
+ tree.Insert(&node)
+ }
+
+ treeID, err := repo.SaveTree(gopts.ctx, tree)
+ if err != nil {
+ return errors.Fatalf("unable to save new tree to the repo: %v", err)
+ }
+
+ err = repo.Flush(gopts.ctx)
+ if err != nil {
+ return errors.Fatalf("unable to save blobs to the repo: %v", err)
+ }
+
+ err = repo.SaveIndex(gopts.ctx)
+ if err != nil {
+ return errors.Fatalf("unable to save new index to the repo: %v", err)
+ }
+
+ sn, err := restic.NewSnapshot([]string{"/recover"}, []string{}, hostname, time.Now())
+ if err != nil {
+ return errors.Fatalf("unable to save snapshot: %v", err)
+ }
+
+ sn.Tree = &treeID
+
+ id, err := repo.SaveJSONUnpacked(gopts.ctx, restic.SnapshotFile, sn)
+ if err != nil {
+ return errors.Fatalf("unable to save snapshot: %v", err)
+ }
+
+ Printf("saved new snapshot %v\n", id.Str())
+
+ return nil
+}
diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go
index 4bf59c06f..477192eab 100644
--- a/cmd/restic/cmd_restore.go
+++ b/cmd/restic/cmd_restore.go
@@ -113,8 +113,8 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
}
totalErrors := 0
- res.Error = func(dir string, node *restic.Node, err error) error {
- Warnf("ignoring error for %s: %s\n", dir, err)
+ res.Error = func(location string, err error) error {
+ Warnf("ignoring error for %s: %s\n", location, err)
totalErrors++
return nil
}
diff --git a/cmd/restic/cmd_self_update.go b/cmd/restic/cmd_self_update.go
index 00d91b294..5aed085de 100644
--- a/cmd/restic/cmd_self_update.go
+++ b/cmd/restic/cmd_self_update.go
@@ -1,4 +1,4 @@
-// +build selfupdate
+// xbuild selfupdate
package main
@@ -14,7 +14,7 @@ var cmdSelfUpdate = &cobra.Command{
Use: "self-update [flags]",
Short: "Update the restic binary",
Long: `
-The command "update-restic" downloads the latest stable release of restic from
+The command "self-update" downloads the latest stable release of restic from
GitHub and replaces the currently running binary. After download, the
authenticity of the binary is verified using the GPG signature on the release
files.
@@ -36,16 +36,38 @@ func init() {
cmdRoot.AddCommand(cmdSelfUpdate)
flags := cmdSelfUpdate.Flags()
- flags.StringVar(&selfUpdateOptions.Output, "output", os.Args[0], "Save the downloaded file as `filename`")
+ flags.StringVar(&selfUpdateOptions.Output, "output", "", "Save the downloaded file as `filename` (default: running binary itself)")
}
func runSelfUpdate(opts SelfUpdateOptions, gopts GlobalOptions, args []string) error {
- v, err := selfupdate.DownloadLatestStableRelease(gopts.ctx, opts.Output, Verbosef)
+ if opts.Output == "" {
+ file, err := os.Executable()
+ if err != nil {
+ return errors.Wrap(err, "unable to find executable")
+ }
+
+ opts.Output = file
+ }
+
+ fi, err := os.Lstat(opts.Output)
+ if err != nil {
+ return err
+ }
+
+ if !fi.Mode().IsRegular() {
+ return errors.Errorf("output file %v is not a normal file, use --output to specify a different file", opts.Output)
+ }
+
+ Printf("writing restic to %v\n", opts.Output)
+
+ v, err := selfupdate.DownloadLatestStableRelease(gopts.ctx, opts.Output, version, Verbosef)
if err != nil {
return errors.Fatalf("unable to update restic: %v", err)
}
- Printf("successfully updated restic to version %v\n", v)
+ if v != version {
+ Printf("successfully updated restic to version %v\n", v)
+ }
return nil
}
diff --git a/cmd/restic/cmd_snapshots.go b/cmd/restic/cmd_snapshots.go
index e456514c3..d9623b942 100644
--- a/cmd/restic/cmd_snapshots.go
+++ b/cmd/restic/cmd_snapshots.go
@@ -184,7 +184,7 @@ func PrintSnapshots(stdout io.Writer, list restic.Snapshots, reasons []restic.Ke
for _, sn := range list {
data := snapshot{
ID: sn.ID().Str(),
- Timestamp: sn.Time.Format(TimeFormat),
+ Timestamp: sn.Time.Local().Format(TimeFormat),
Hostname: sn.Hostname,
Tags: sn.Tags,
Paths: sn.Paths,
@@ -195,7 +195,7 @@ func PrintSnapshots(stdout io.Writer, list restic.Snapshots, reasons []restic.Ke
data.Reasons = keepReasons[*id].Matches
}
- if len(sn.Paths) > 1 {
+ if len(sn.Paths) > 1 && !compact {
multiline = true
}
diff --git a/cmd/restic/cmd_stats.go b/cmd/restic/cmd_stats.go
index 4ad57a613..590ef5f14 100644
--- a/cmd/restic/cmd_stats.go
+++ b/cmd/restic/cmd_stats.go
@@ -48,7 +48,7 @@ func init() {
cmdRoot.AddCommand(cmdStats)
f := cmdStats.Flags()
f.StringVar(&countMode, "mode", countModeRestoreSize, "counting mode: restore-size (default), files-by-contents, blobs-per-file, or raw-data")
- f.StringVar(&snapshotByHost, "host", "H", "filter latest snapshot by this hostname")
+ f.StringVarP(&snapshotByHost, "host", "H", "", "filter latest snapshot by this hostname")
}
func runStats(gopts GlobalOptions, args []string) error {
@@ -296,7 +296,7 @@ type statsContainer struct {
// blobs that have been seen as a part of the file
fileBlobs map[string]restic.IDSet
- // blobs and blobsSeen are used to count indiviudal
+ // blobs and blobsSeen are used to count individual
// unique blobs, independent of references to files
blobs, blobsSeen restic.BlobSet
}
diff --git a/cmd/restic/format.go b/cmd/restic/format.go
index 1de0335c9..d623371b8 100644
--- a/cmd/restic/format.go
+++ b/cmd/restic/format.go
@@ -21,7 +21,7 @@ func formatBytes(c uint64) string {
case c > 1<<10:
return fmt.Sprintf("%.3f KiB", b/(1<<10))
default:
- return fmt.Sprintf("%dB", c)
+ return fmt.Sprintf("%d B", c)
}
}
@@ -90,6 +90,6 @@ func formatNode(path string, n *restic.Node, long bool) string {
return fmt.Sprintf("%s %5d %5d %6d %s %s%s",
mode|n.Mode, n.UID, n.GID, n.Size,
- n.ModTime.Format(TimeFormat), path,
+ n.ModTime.Local().Format(TimeFormat), path,
target)
}
diff --git a/cmd/restic/global.go b/cmd/restic/global.go
index 6553a39b0..acff7ada6 100644
--- a/cmd/restic/global.go
+++ b/cmd/restic/global.go
@@ -34,26 +34,29 @@ import (
"github.com/restic/restic/internal/errors"
"golang.org/x/crypto/ssh/terminal"
+ "os/exec"
)
-var version = "0.9.3"
+var version = "0.9.4"
// TimeFormat is the format used for all timestamps printed by restic.
const TimeFormat = "2006-01-02 15:04:05"
// GlobalOptions hold all global options for restic.
type GlobalOptions struct {
- Repo string
- PasswordFile string
- Quiet bool
- Verbose int
- NoLock bool
- JSON bool
- CacheDir string
- NoCache bool
- CACerts []string
- TLSClientCert string
- CleanupCache bool
+ Repo string
+ PasswordFile string
+ PasswordCommand string
+ KeyHint string
+ Quiet bool
+ Verbose int
+ NoLock bool
+ JSON bool
+ CacheDir string
+ NoCache bool
+ CACerts []string
+ TLSClientCert string
+ CleanupCache bool
LimitUploadKb int
LimitDownloadKb int
@@ -91,6 +94,8 @@ func init() {
f := cmdRoot.PersistentFlags()
f.StringVarP(&globalOptions.Repo, "repo", "r", os.Getenv("RESTIC_REPOSITORY"), "repository to backup to or restore from (default: $RESTIC_REPOSITORY)")
f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", os.Getenv("RESTIC_PASSWORD_FILE"), "read the repository password from a file (default: $RESTIC_PASSWORD_FILE)")
+ f.StringVarP(&globalOptions.KeyHint, "key-hint", "", os.Getenv("RESTIC_KEY_HINT"), "key ID of key to try decrypting first (default: $RESTIC_KEY_HINT)")
+ f.StringVarP(&globalOptions.PasswordCommand, "password-command", "", os.Getenv("RESTIC_PASSWORD_COMMAND"), "specify a shell command to obtain a password (default: $RESTIC_PASSWORD_COMMAND)")
f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not output comprehensive progress report")
f.CountVarP(&globalOptions.Verbose, "verbose", "v", "be verbose (specify --verbose multiple times or level `n`)")
f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos")
@@ -179,7 +184,6 @@ func Printf(format string, args ...interface{}) {
_, err := fmt.Fprintf(globalOptions.stdout, format, args...)
if err != nil {
fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err)
- Exit(100)
}
}
@@ -220,7 +224,6 @@ func Warnf(format string, args ...interface{}) {
_, err := fmt.Fprintf(globalOptions.stderr, format, args...)
if err != nil {
fmt.Fprintf(os.Stderr, "unable to write to stderr: %v\n", err)
- Exit(100)
}
}
@@ -236,7 +239,23 @@ func Exitf(exitcode int, format string, args ...interface{}) {
}
// resolvePassword determines the password to be used for opening the repository.
-func resolvePassword(opts GlobalOptions, env string) (string, error) {
+func resolvePassword(opts GlobalOptions) (string, error) {
+ if opts.PasswordFile != "" && opts.PasswordCommand != "" {
+ return "", errors.Fatalf("Password file and command are mutually exclusive options")
+ }
+ if opts.PasswordCommand != "" {
+ args, err := backend.SplitShellStrings(opts.PasswordCommand)
+ if err != nil {
+ return "", err
+ }
+ cmd := exec.Command(args[0], args[1:]...)
+ cmd.Stderr = os.Stderr
+ output, err := cmd.Output()
+ if err != nil {
+ return "", err
+ }
+ return (strings.TrimSpace(string(output))), nil
+ }
if opts.PasswordFile != "" {
s, err := textfile.Read(opts.PasswordFile)
if os.IsNotExist(errors.Cause(err)) {
@@ -245,7 +264,7 @@ func resolvePassword(opts GlobalOptions, env string) (string, error) {
return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile")
}
- if pwd := os.Getenv(env); pwd != "" {
+ if pwd := os.Getenv("RESTIC_PASSWORD"); pwd != "" {
return pwd, nil
}
@@ -353,7 +372,7 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
return nil, err
}
- err = s.SearchKey(opts.ctx, opts.password, maxKeys)
+ err = s.SearchKey(opts.ctx, opts.password, maxKeys, opts.KeyHint)
if err != nil {
return nil, err
}
@@ -363,7 +382,9 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
if len(id) > 8 {
id = id[:8]
}
- Verbosef("repository %v opened successfully, password is correct\n", id)
+ if !opts.JSON {
+ Verbosef("repository %v opened successfully, password is correct\n", id)
+ }
}
if opts.NoCache {
diff --git a/cmd/restic/main.go b/cmd/restic/main.go
index 01a902b1d..e61547c5f 100644
--- a/cmd/restic/main.go
+++ b/cmd/restic/main.go
@@ -54,7 +54,7 @@ directories in an encrypted repository stored on different backends.
if c.Name() == "version" {
return nil
}
- pwd, err := resolvePassword(globalOptions, "RESTIC_PASSWORD")
+ pwd, err := resolvePassword(globalOptions)
if err != nil {
fmt.Fprintf(os.Stderr, "Resolving password failed: %v\n", err)
Exit(1)