diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index b165f2805..ad1986b7f 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "path/filepath" "github.com/restic/restic" "github.com/restic/restic/backend" @@ -21,11 +22,11 @@ func init() { } func (cmd CmdRestore) Usage() string { - return "snapshot-ID TARGETDIR" + return "snapshot-ID TARGETDIR [PATTERN]" } func (cmd CmdRestore) Execute(args []string) error { - if len(args) != 2 { + if len(args) < 2 || len(args) > 3 { return fmt.Errorf("wrong number of arguments, Usage: %s", cmd.Usage()) } @@ -64,6 +65,18 @@ func (cmd CmdRestore) Execute(args []string) error { return err } + // TODO: a filter against the full path sucks as filepath.Match doesn't match + // directory separators on '*'. still, it's better than nothing. + if len(args) > 2 { + res.Filter = func(item string, dstpath string, node *restic.Node) bool { + matched, err := filepath.Match(item, args[2]) + if err != nil { + panic(err) + } + return matched + } + } + fmt.Printf("restoring %s to %s\n", res.Snapshot(), target) err = res.RestoreTo(target) diff --git a/restorer.go b/restorer.go index f26b30354..b57d83e77 100644 --- a/restorer.go +++ b/restorer.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "syscall" "github.com/juju/arrar" "github.com/restic/restic/backend" @@ -16,7 +17,7 @@ type Restorer struct { sn *Snapshot Error func(dir string, node *Node, err error) error - Filter func(item string, node *Node) bool + Filter func(item string, dstpath string, node *Node) bool } // NewRestorer creates a restorer preloaded with the content from the snapshot snid. @@ -36,13 +37,11 @@ func NewRestorer(s Server, snid backend.ID) (*Restorer, error) { // abort on all errors r.Error = func(string, *Node, error) error { return err } - // allow all files - r.Filter = func(string, *Node) bool { return true } return r, nil } -func (res *Restorer) to(dir string, tree_id backend.ID) error { +func (res *Restorer) to(dst string, dir string, tree_id backend.ID) error { tree := Tree{} err := res.ch.LoadJSON(backend.Tree, tree_id, &tree) if err != nil { @@ -50,16 +49,32 @@ func (res *Restorer) to(dir string, tree_id backend.ID) error { } for _, node := range tree { - p := filepath.Join(dir, node.Name) - if !res.Filter(p, node) { - continue - } + dstpath := filepath.Join(dst, dir, node.Name) + + if res.Filter == nil || + res.Filter(filepath.Join(res.sn.Dir, dir, node.Name), dstpath, node) { + err := node.CreateAt(res.ch, dstpath) + + // Did it fail because of ENOENT? + if arrar.Check(err, func(err error) bool { + if pe, ok := err.(*os.PathError); ok { + errn, ok := pe.Err.(syscall.Errno) + return ok && errn == syscall.ENOENT + } + return false + }) { + // Create parent directories and retry + err = os.MkdirAll(filepath.Dir(dstpath), 0700) + if err == nil || err == os.ErrExist { + err = node.CreateAt(res.ch, dstpath) + } + } - err := node.CreateAt(res.ch, p) - if err != nil { - err = res.Error(p, node, arrar.Annotate(err, "create node")) if err != nil { - return err + err = res.Error(dstpath, node, arrar.Annotate(err, "create node")) + if err != nil { + return err + } } } @@ -68,9 +83,10 @@ func (res *Restorer) to(dir string, tree_id backend.ID) error { return errors.New(fmt.Sprintf("Dir without subtree in tree %s", tree_id)) } - err = res.to(p, node.Subtree) + subp := filepath.Join(dir, node.Name) + err = res.to(dst, subp, node.Subtree) if err != nil { - err = res.Error(p, node, arrar.Annotate(err, "restore subtree")) + err = res.Error(subp, node, arrar.Annotate(err, "restore subtree")) if err != nil { return err } @@ -84,12 +100,7 @@ func (res *Restorer) to(dir string, tree_id backend.ID) error { // RestoreTo creates the directories and files in the snapshot below dir. // Before an item is created, res.Filter is called. func (res *Restorer) RestoreTo(dir string) error { - err := os.MkdirAll(dir, 0700) - if err != nil && err != os.ErrExist { - return err - } - - return res.to(dir, res.sn.Tree) + return res.to(dir, "", res.sn.Tree) } func (res *Restorer) Snapshot() *Snapshot {