forked from TrueCloudLab/restic
Merge pull request #503 from gerdus/restore-latest
Add option to restore latest snapshot with optional path and source filters
This commit is contained in:
commit
795e3d5b6c
6 changed files with 180 additions and 67 deletions
|
@ -204,10 +204,33 @@ Now, you can list all the snapshots stored in the repository:
|
|||
|
||||
$ restic -r /tmp/backup snapshots
|
||||
enter password for repository:
|
||||
ID Date Source Directory
|
||||
ID Date Host Directory
|
||||
----------------------------------------------------------------------
|
||||
40dc1520 2015-05-08 21:38:30 kasimir /home/user/work
|
||||
79766175 2015-05-08 21:40:19 kasimir /home/user/work
|
||||
bdbd3439 2015-05-08 21:45:17 luigi /home/art
|
||||
590c8fc8 2015-05-08 21:47:38 kazik /srv
|
||||
9f0bc19e 2015-05-08 21:46:11 luigi /srv
|
||||
|
||||
You can filter the listing by directory path:
|
||||
|
||||
$ restic -r /tmp/backup snapshots --path="/srv"
|
||||
enter password for repository:
|
||||
ID Date Host Directory
|
||||
----------------------------------------------------------------------
|
||||
590c8fc8 2015-05-08 21:47:38 kazik /srv
|
||||
9f0bc19e 2015-05-08 21:46:11 luigi /srv
|
||||
|
||||
Or filter by host:
|
||||
|
||||
$ restic -r /tmp/backup snapshots --host luigi
|
||||
enter password for repository:
|
||||
ID Date Host Directory
|
||||
----------------------------------------------------------------------
|
||||
bdbd3439 2015-05-08 21:45:17 luigi /home/art
|
||||
9f0bc19e 2015-05-08 21:46:11 luigi /srv
|
||||
|
||||
Combining filters is also possible.
|
||||
|
||||
# Restore a snapshot
|
||||
|
||||
|
@ -218,6 +241,15 @@ restore the contents of the latest snapshot to `/tmp/restore-work`:
|
|||
enter password for repository:
|
||||
restoring <Snapshot of [/home/user/work] at 2015-05-08 21:40:19.884408621 +0200 CEST> to /tmp/restore-work
|
||||
|
||||
Use the word `latest` to restore the last backup. You can also combine `latest`
|
||||
with the `--host` and `--path` filters to choose the last backup for a specific
|
||||
host, path or both.
|
||||
|
||||
$ restic -r /tmp/backup restore latest --target ~/tmp/restore-work --path "/home/art" --host luigi
|
||||
enter password for repository:
|
||||
restoring <Snapshot of [/home/art] at 2015-05-08 21:45:17.884408621 +0200 CEST> to /tmp/restore-work
|
||||
|
||||
|
||||
# Manage repository keys
|
||||
|
||||
The `key` command allows you to set multiple access keys or passwords per
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"restic/backend"
|
||||
"restic/debug"
|
||||
"restic/filter"
|
||||
"restic/repository"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -218,55 +217,6 @@ func (cmd CmdBackup) newArchiveStdinProgress() *restic.Progress {
|
|||
return archiveProgress
|
||||
}
|
||||
|
||||
func samePaths(expected, actual []string) bool {
|
||||
if expected == nil || actual == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
for i := range expected {
|
||||
found := false
|
||||
for j := range actual {
|
||||
if expected[i] == actual[j] {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
var errNoSnapshotFound = errors.New("no snapshot found")
|
||||
|
||||
func findLatestSnapshot(repo *repository.Repository, targets []string) (backend.ID, error) {
|
||||
var (
|
||||
latest time.Time
|
||||
latestID backend.ID
|
||||
found bool
|
||||
)
|
||||
|
||||
for snapshotID := range repo.List(backend.Snapshot, make(chan struct{})) {
|
||||
snapshot, err := restic.LoadSnapshot(repo, snapshotID)
|
||||
if err != nil {
|
||||
return backend.ID{}, fmt.Errorf("Error listing snapshot: %v", err)
|
||||
}
|
||||
if snapshot.Time.After(latest) && samePaths(snapshot.Paths, targets) {
|
||||
latest = snapshot.Time
|
||||
latestID = snapshotID
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return backend.ID{}, errNoSnapshotFound
|
||||
}
|
||||
|
||||
return latestID, nil
|
||||
}
|
||||
|
||||
// filterExisting returns a slice of all existing items, or an error if no
|
||||
// items exist at all.
|
||||
func filterExisting(items []string) (result []string, err error) {
|
||||
|
@ -368,10 +318,10 @@ func (cmd CmdBackup) Execute(args []string) error {
|
|||
|
||||
// Find last snapshot to set it as parent, if not already set
|
||||
if !cmd.Force && parentSnapshotID == nil {
|
||||
id, err := findLatestSnapshot(repo, target)
|
||||
id, err := restic.FindLatestSnapshot(repo, target, "")
|
||||
if err == nil {
|
||||
parentSnapshotID = &id
|
||||
} else if err != errNoSnapshotFound {
|
||||
} else if err != restic.ErrNoSnapshotFound {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"restic"
|
||||
"restic/backend"
|
||||
"restic/debug"
|
||||
"restic/filter"
|
||||
)
|
||||
|
@ -13,6 +14,8 @@ type CmdRestore struct {
|
|||
Exclude []string `short:"e" long:"exclude" description:"Exclude a pattern (can be specified multiple times)"`
|
||||
Include []string `short:"i" long:"include" description:"Include a pattern, exclude everything else (can be specified multiple times)"`
|
||||
Target string `short:"t" long:"target" description:"Directory to restore to"`
|
||||
Host string `short:"h" long:"host" description:"Source Filter (for id=latest)"`
|
||||
Paths []string `short:"p" long:"path" description:"Path Filter (absolute path;for id=latest) (can be specified multiple times)"`
|
||||
|
||||
global *GlobalOptions
|
||||
}
|
||||
|
@ -66,9 +69,18 @@ func (cmd CmdRestore) Execute(args []string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
id, err := restic.FindSnapshot(repo, snapshotIDString)
|
||||
if err != nil {
|
||||
cmd.global.Exitf(1, "invalid id %q: %v", snapshotIDString, err)
|
||||
var id backend.ID
|
||||
|
||||
if snapshotIDString == "latest" {
|
||||
id, err = restic.FindLatestSnapshot(repo, cmd.Paths, cmd.Host)
|
||||
if err != nil {
|
||||
cmd.global.Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, cmd.Paths, cmd.Host)
|
||||
}
|
||||
} else {
|
||||
id, err = restic.FindSnapshot(repo, snapshotIDString)
|
||||
if err != nil {
|
||||
cmd.global.Exitf(1, "invalid id %q: %v", snapshotIDString, err)
|
||||
}
|
||||
}
|
||||
|
||||
res, err := restic.NewRestorer(repo, id)
|
||||
|
|
|
@ -48,6 +48,9 @@ func (t Table) Write(w io.Writer) error {
|
|||
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
|
||||
}
|
||||
|
||||
|
@ -82,7 +85,7 @@ func (cmd CmdSnapshots) Execute(args []string) error {
|
|||
}
|
||||
|
||||
tab := NewTable()
|
||||
tab.Header = fmt.Sprintf("%-8s %-19s %-10s %s", "ID", "Date", "Source", "Directory")
|
||||
tab.Header = fmt.Sprintf("%-8s %-19s %-10s %s", "ID", "Date", "Host", "Directory")
|
||||
tab.RowFormat = "%-8s %-19s %-10s %s"
|
||||
|
||||
done := make(chan struct{})
|
||||
|
@ -96,17 +99,20 @@ func (cmd CmdSnapshots) Execute(args []string) error {
|
|||
continue
|
||||
}
|
||||
|
||||
pos := sort.Search(len(list), func(i int) bool {
|
||||
return list[i].Time.After(sn.Time)
|
||||
})
|
||||
if restic.SamePaths(sn.Paths, cmd.Paths) && (cmd.Host == "" || cmd.Host == sn.Hostname) {
|
||||
pos := sort.Search(len(list), func(i int) bool {
|
||||
return list[i].Time.After(sn.Time)
|
||||
})
|
||||
|
||||
if pos < len(list) {
|
||||
list = append(list, nil)
|
||||
copy(list[pos+1:], list[pos:])
|
||||
list[pos] = sn
|
||||
} else {
|
||||
list = append(list, sn)
|
||||
if pos < len(list) {
|
||||
list = append(list, nil)
|
||||
copy(list[pos+1:], list[pos:])
|
||||
list[pos] = sn
|
||||
} else {
|
||||
list = append(list, sn)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
plen, err := repo.PrefixLength(backend.Snapshot)
|
||||
|
|
|
@ -77,6 +77,11 @@ func cmdRestore(t testing.TB, global GlobalOptions, dir string, snapshotID backe
|
|||
cmdRestoreExcludes(t, global, dir, snapshotID, nil)
|
||||
}
|
||||
|
||||
func cmdRestoreLatest(t testing.TB, global GlobalOptions, dir string, paths []string, host string) {
|
||||
cmd := &CmdRestore{global: &global, Target: dir, Host: host, Paths: paths}
|
||||
OK(t, cmd.Execute([]string{"latest"}))
|
||||
}
|
||||
|
||||
func cmdRestoreExcludes(t testing.TB, global GlobalOptions, dir string, snapshotID backend.ID, excludes []string) {
|
||||
cmd := &CmdRestore{global: &global, Target: dir, Exclude: excludes}
|
||||
OK(t, cmd.Execute([]string{snapshotID.String()}))
|
||||
|
@ -626,6 +631,60 @@ func TestRestoreFilter(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestRestoreLatest(t *testing.T) {
|
||||
|
||||
withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) {
|
||||
cmdInit(t, global)
|
||||
|
||||
p := filepath.Join(env.testdata, "testfile.c")
|
||||
OK(t, os.MkdirAll(filepath.Dir(p), 0755))
|
||||
OK(t, appendRandomData(p, 100))
|
||||
|
||||
cmdBackup(t, global, []string{env.testdata}, nil)
|
||||
cmdCheck(t, global)
|
||||
|
||||
os.Remove(p)
|
||||
OK(t, appendRandomData(p, 101))
|
||||
cmdBackup(t, global, []string{env.testdata}, nil)
|
||||
cmdCheck(t, global)
|
||||
|
||||
// Restore latest without any filters
|
||||
cmdRestoreLatest(t, global, filepath.Join(env.base, "restore0"), nil, "")
|
||||
OK(t, testFileSize(filepath.Join(env.base, "restore0", "testdata", "testfile.c"), int64(101)))
|
||||
|
||||
// Setup test files in different directories backed up in different snapshots
|
||||
p1 := filepath.Join(env.testdata, "p1/testfile.c")
|
||||
OK(t, os.MkdirAll(filepath.Dir(p1), 0755))
|
||||
OK(t, appendRandomData(p1, 102))
|
||||
cmdBackup(t, global, []string{filepath.Dir(p1)}, nil)
|
||||
cmdCheck(t, global)
|
||||
|
||||
p2 := filepath.Join(env.testdata, "p2/testfile.c")
|
||||
OK(t, os.MkdirAll(filepath.Dir(p2), 0755))
|
||||
OK(t, appendRandomData(p2, 103))
|
||||
cmdBackup(t, global, []string{filepath.Dir(p2)}, nil)
|
||||
cmdCheck(t, global)
|
||||
|
||||
p1rAbs := filepath.Join(env.base, "restore1", "p1/testfile.c")
|
||||
p2rAbs := filepath.Join(env.base, "restore2", "p2/testfile.c")
|
||||
|
||||
cmdRestoreLatest(t, global, filepath.Join(env.base, "restore1"), []string{filepath.Dir(p1)}, "")
|
||||
OK(t, testFileSize(p1rAbs, int64(102)))
|
||||
if _, err := os.Stat(p2rAbs); os.IsNotExist(err) {
|
||||
Assert(t, os.IsNotExist(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)}, "")
|
||||
OK(t, testFileSize(p2rAbs, int64(103)))
|
||||
if _, err := os.Stat(p1rAbs); os.IsNotExist(err) {
|
||||
Assert(t, os.IsNotExist(err),
|
||||
"expected %v to not exist in restore, but it exists, err %v", p1rAbs, err)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func TestRestoreWithPermissionFailure(t *testing.T) {
|
||||
withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) {
|
||||
datafile := filepath.Join("testdata", "repo-restore-permissions-test.tar.gz")
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package restic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
|
@ -82,7 +83,7 @@ func LoadAllSnapshots(repo *repository.Repository) (snapshots []*Snapshot, err e
|
|||
}
|
||||
|
||||
func (sn Snapshot) String() string {
|
||||
return fmt.Sprintf("<Snapshot of %v at %s>", sn.Paths, sn.Time)
|
||||
return fmt.Sprintf("<Snapshot %s of %v at %s>", sn.id.Str(), sn.Paths, sn.Time)
|
||||
}
|
||||
|
||||
// ID retuns the snapshot's ID.
|
||||
|
@ -102,9 +103,62 @@ func (sn *Snapshot) fillUserInfo() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// SamePaths compares the Snapshot's paths and provided paths are exactly the same
|
||||
func SamePaths(expected, actual []string) bool {
|
||||
if expected == nil || actual == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
for i := range expected {
|
||||
found := false
|
||||
for j := range actual {
|
||||
if expected[i] == actual[j] {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Error when no snapshot is found for the given criteria
|
||||
var ErrNoSnapshotFound = errors.New("no snapshot found")
|
||||
|
||||
// FindLatestSnapshot finds latest snapshot with optional target/directory and source filters
|
||||
func FindLatestSnapshot(repo *repository.Repository, targets []string, source string) (backend.ID, error) {
|
||||
var (
|
||||
latest time.Time
|
||||
latestID backend.ID
|
||||
found bool
|
||||
)
|
||||
|
||||
for snapshotID := range repo.List(backend.Snapshot, make(chan struct{})) {
|
||||
snapshot, err := LoadSnapshot(repo, snapshotID)
|
||||
if err != nil {
|
||||
return backend.ID{}, fmt.Errorf("Error listing snapshot: %v", err)
|
||||
}
|
||||
if snapshot.Time.After(latest) && SamePaths(snapshot.Paths, targets) && (source == "" || source == snapshot.Hostname) {
|
||||
latest = snapshot.Time
|
||||
latestID = snapshotID
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return backend.ID{}, ErrNoSnapshotFound
|
||||
}
|
||||
|
||||
return latestID, nil
|
||||
}
|
||||
|
||||
// FindSnapshot takes a string and tries to find a snapshot whose ID matches
|
||||
// the string as closely as possible.
|
||||
func FindSnapshot(repo *repository.Repository, s string) (backend.ID, error) {
|
||||
|
||||
// find snapshot id with prefix
|
||||
name, err := backend.Find(repo.Backend(), backend.Snapshot, s)
|
||||
if err != nil {
|
||||
|
|
Loading…
Reference in a new issue