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:
commit
795e3d5b6c
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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")
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user