2
2
mirror of https://github.com/octoleo/restic.git synced 2024-11-05 04:47:51 +00:00

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:
Alexander Neumann 2016-05-11 20:48:56 +02:00
commit 795e3d5b6c
6 changed files with 180 additions and 67 deletions

View File

@ -204,10 +204,33 @@ Now, you can list all the snapshots stored in the repository:
$ restic -r /tmp/backup snapshots $ restic -r /tmp/backup snapshots
enter password for repository: enter password for repository:
ID Date Source Directory ID Date Host Directory
---------------------------------------------------------------------- ----------------------------------------------------------------------
40dc1520 2015-05-08 21:38:30 kasimir /home/user/work 40dc1520 2015-05-08 21:38:30 kasimir /home/user/work
79766175 2015-05-08 21:40:19 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 # Restore a snapshot
@ -218,6 +241,15 @@ restore the contents of the latest snapshot to `/tmp/restore-work`:
enter password for repository: enter password for repository:
restoring <Snapshot of [/home/user/work] at 2015-05-08 21:40:19.884408621 +0200 CEST> to /tmp/restore-work 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 # Manage repository keys
The `key` command allows you to set multiple access keys or passwords per The `key` command allows you to set multiple access keys or passwords per

View File

@ -10,7 +10,6 @@ import (
"restic/backend" "restic/backend"
"restic/debug" "restic/debug"
"restic/filter" "restic/filter"
"restic/repository"
"strings" "strings"
"time" "time"
@ -218,55 +217,6 @@ func (cmd CmdBackup) newArchiveStdinProgress() *restic.Progress {
return archiveProgress 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 // filterExisting returns a slice of all existing items, or an error if no
// items exist at all. // items exist at all.
func filterExisting(items []string) (result []string, err error) { 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 // Find last snapshot to set it as parent, if not already set
if !cmd.Force && parentSnapshotID == nil { if !cmd.Force && parentSnapshotID == nil {
id, err := findLatestSnapshot(repo, target) id, err := restic.FindLatestSnapshot(repo, target, "")
if err == nil { if err == nil {
parentSnapshotID = &id parentSnapshotID = &id
} else if err != errNoSnapshotFound { } else if err != restic.ErrNoSnapshotFound {
return err return err
} }
} }

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"restic" "restic"
"restic/backend"
"restic/debug" "restic/debug"
"restic/filter" "restic/filter"
) )
@ -13,6 +14,8 @@ type CmdRestore struct {
Exclude []string `short:"e" long:"exclude" description:"Exclude a pattern (can be specified multiple times)"` 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)"` 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"` 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 global *GlobalOptions
} }
@ -66,9 +69,18 @@ func (cmd CmdRestore) Execute(args []string) error {
return err return err
} }
id, err := restic.FindSnapshot(repo, snapshotIDString) var id backend.ID
if err != nil {
cmd.global.Exitf(1, "invalid id %q: %v", snapshotIDString, err) 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) res, err := restic.NewRestorer(repo, id)

View File

@ -48,6 +48,9 @@ func (t Table) Write(w io.Writer) error {
const TimeFormat = "2006-01-02 15:04:05" const TimeFormat = "2006-01-02 15:04:05"
type CmdSnapshots struct { 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 global *GlobalOptions
} }
@ -82,7 +85,7 @@ func (cmd CmdSnapshots) Execute(args []string) error {
} }
tab := NewTable() 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" tab.RowFormat = "%-8s %-19s %-10s %s"
done := make(chan struct{}) done := make(chan struct{})
@ -96,17 +99,20 @@ func (cmd CmdSnapshots) Execute(args []string) error {
continue continue
} }
pos := sort.Search(len(list), func(i int) bool { if restic.SamePaths(sn.Paths, cmd.Paths) && (cmd.Host == "" || cmd.Host == sn.Hostname) {
return list[i].Time.After(sn.Time) pos := sort.Search(len(list), func(i int) bool {
}) return list[i].Time.After(sn.Time)
})
if pos < len(list) { if pos < len(list) {
list = append(list, nil) list = append(list, nil)
copy(list[pos+1:], list[pos:]) copy(list[pos+1:], list[pos:])
list[pos] = sn list[pos] = sn
} else { } else {
list = append(list, sn) list = append(list, sn)
}
} }
} }
plen, err := repo.PrefixLength(backend.Snapshot) plen, err := repo.PrefixLength(backend.Snapshot)

View File

@ -77,6 +77,11 @@ func cmdRestore(t testing.TB, global GlobalOptions, dir string, snapshotID backe
cmdRestoreExcludes(t, global, dir, snapshotID, nil) 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) { func cmdRestoreExcludes(t testing.TB, global GlobalOptions, dir string, snapshotID backend.ID, excludes []string) {
cmd := &CmdRestore{global: &global, Target: dir, Exclude: excludes} cmd := &CmdRestore{global: &global, Target: dir, Exclude: excludes}
OK(t, cmd.Execute([]string{snapshotID.String()})) 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) { func TestRestoreWithPermissionFailure(t *testing.T) {
withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) {
datafile := filepath.Join("testdata", "repo-restore-permissions-test.tar.gz") datafile := filepath.Join("testdata", "repo-restore-permissions-test.tar.gz")

View File

@ -1,6 +1,7 @@
package restic package restic
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"os/user" "os/user"
@ -82,7 +83,7 @@ func LoadAllSnapshots(repo *repository.Repository) (snapshots []*Snapshot, err e
} }
func (sn Snapshot) String() string { 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. // ID retuns the snapshot's ID.
@ -102,9 +103,62 @@ func (sn *Snapshot) fillUserInfo() error {
return err 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 // FindSnapshot takes a string and tries to find a snapshot whose ID matches
// the string as closely as possible. // the string as closely as possible.
func FindSnapshot(repo *repository.Repository, s string) (backend.ID, error) { func FindSnapshot(repo *repository.Repository, s string) (backend.ID, error) {
// find snapshot id with prefix // find snapshot id with prefix
name, err := backend.Find(repo.Backend(), backend.Snapshot, s) name, err := backend.Find(repo.Backend(), backend.Snapshot, s)
if err != nil { if err != nil {