Add option to restore latest snapshot with optional path and source filters
eg restic -r r1 restore latest --target restore2 --path "D:\dev\restic\bin\s1" path and source filters also added to snapshot cmd eg restic -r r1 snapshots --source nucore --path="D:\dev\restic\bin\s1" Add option to restore latest snapshot with optional path and source filters eg restic -r r1 restore latest --target restore2 --path "D:\dev\restic\bin\s1" path and source filters also added to snapshot cmd eg restic -r r1 snapshots --source nucore --path="D:\dev\restic\bin\s1"
This commit is contained in:
parent
49f82f54b0
commit
3cb68ddb0d
5 changed files with 26 additions and 111 deletions
|
@ -10,7 +10,6 @@ import (
|
||||||
"restic/backend"
|
"restic/backend"
|
||||||
"restic/debug"
|
"restic/debug"
|
||||||
"restic/filter"
|
"restic/filter"
|
||||||
"restic/repository"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -18,12 +17,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type CmdBackup struct {
|
type CmdBackup struct {
|
||||||
Parent string `short:"p" long:"parent" description:"use this parent snapshot (default: last snapshot in repo that has the same target)"`
|
Parent string `short:"p" long:"parent" description:"use this parent snapshot (default: last snapshot in repo that has the same target)"`
|
||||||
Force bool `short:"f" long:"force" description:"Force re-reading the target. Overrides the \"parent\" flag"`
|
Force bool `short:"f" long:"force" description:"Force re-reading the target. Overrides the \"parent\" flag"`
|
||||||
Excludes []string `short:"e" long:"exclude" description:"Exclude a pattern (can be specified multiple times)"`
|
Excludes []string `short:"e" long:"exclude" description:"Exclude a pattern (can be specified multiple times)"`
|
||||||
ExcludeFile string `long:"exclude-file" description:"Read exclude-patterns from file"`
|
ExcludeFile string `long:"exclude-file" description:"Read exclude-patterns from file"`
|
||||||
Stdin bool `long:"stdin" description:"read backup data from stdin"`
|
|
||||||
StdinFilename string `long:"stdin-filename" default:"stdin" description:"file name to use when reading from stdin"`
|
|
||||||
|
|
||||||
global *GlobalOptions
|
global *GlobalOptions
|
||||||
}
|
}
|
||||||
|
@ -177,47 +174,6 @@ func (cmd CmdBackup) newArchiveProgress(todo restic.Stat) *restic.Progress {
|
||||||
return archiveProgress
|
return archiveProgress
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cmd CmdBackup) newArchiveStdinProgress() *restic.Progress {
|
|
||||||
if !cmd.global.ShowProgress() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
archiveProgress := restic.NewProgress(time.Second)
|
|
||||||
|
|
||||||
var bps uint64
|
|
||||||
|
|
||||||
archiveProgress.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) {
|
|
||||||
sec := uint64(d / time.Second)
|
|
||||||
if s.Bytes > 0 && sec > 0 && ticker {
|
|
||||||
bps = s.Bytes / sec
|
|
||||||
}
|
|
||||||
|
|
||||||
status1 := fmt.Sprintf("[%s] %s %s/s", formatDuration(d),
|
|
||||||
formatBytes(s.Bytes),
|
|
||||||
formatBytes(bps))
|
|
||||||
|
|
||||||
w, _, err := terminal.GetSize(int(os.Stdout.Fd()))
|
|
||||||
if err == nil {
|
|
||||||
maxlen := w - len(status1)
|
|
||||||
|
|
||||||
if maxlen < 4 {
|
|
||||||
status1 = ""
|
|
||||||
} else if len(status1) > maxlen {
|
|
||||||
status1 = status1[:maxlen-4]
|
|
||||||
status1 += "... "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\x1b[2K%s\r", status1)
|
|
||||||
}
|
|
||||||
|
|
||||||
archiveProgress.OnDone = func(s restic.Stat, d time.Duration, ticker bool) {
|
|
||||||
fmt.Printf("\nduration: %s, %s\n", formatDuration(d), formatRate(s.Bytes, d))
|
|
||||||
}
|
|
||||||
|
|
||||||
return archiveProgress
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
||||||
|
@ -237,41 +193,7 @@ func filterExisting(items []string) (result []string, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cmd CmdBackup) readFromStdin(args []string) error {
|
|
||||||
if len(args) != 0 {
|
|
||||||
return fmt.Errorf("when reading from stdin, no additional files can be specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
repo, err := cmd.global.OpenRepository()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
lock, err := lockRepo(repo)
|
|
||||||
defer unlockRepo(lock)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = repo.LoadIndex()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, id, err := restic.ArchiveReader(repo, cmd.newArchiveStdinProgress(), os.Stdin, cmd.StdinFilename)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("archived as %v\n", id.Str())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cmd CmdBackup) Execute(args []string) error {
|
func (cmd CmdBackup) Execute(args []string) error {
|
||||||
if cmd.Stdin {
|
|
||||||
return cmd.readFromStdin(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return fmt.Errorf("wrong number of parameters, Usage: %s", cmd.Usage())
|
return fmt.Errorf("wrong number of parameters, Usage: %s", cmd.Usage())
|
||||||
}
|
}
|
||||||
|
@ -319,10 +241,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ 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)"`
|
Source string `short:"s" long:"source" 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)"`
|
Paths []string `short:"p" long:"path" description:"Path Filter (absolute path;for id=latest) (can be specified multiple times)"`
|
||||||
|
|
||||||
global *GlobalOptions
|
global *GlobalOptions
|
||||||
|
@ -72,9 +72,9 @@ func (cmd CmdRestore) Execute(args []string) error {
|
||||||
var id backend.ID
|
var id backend.ID
|
||||||
|
|
||||||
if snapshotIDString == "latest" {
|
if snapshotIDString == "latest" {
|
||||||
id, err = restic.FindLatestSnapshot(repo, cmd.Paths, cmd.Host)
|
id, err = restic.FindLatestSnapshot(repo, cmd.Paths, cmd.Source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cmd.global.Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, cmd.Paths, cmd.Host)
|
cmd.global.Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Source:%v", err, cmd.Paths, cmd.Source)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
id, err = restic.FindSnapshot(repo, snapshotIDString)
|
id, err = restic.FindSnapshot(repo, snapshotIDString)
|
||||||
|
|
|
@ -48,7 +48,7 @@ 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"`
|
Source string `short:"s" long:"source" description:"Source Filter"`
|
||||||
Paths []string `short:"p" long:"path" description:"Path Filter (absolute path) (can be specified multiple times)"`
|
Paths []string `short:"p" long:"path" description:"Path Filter (absolute path) (can be specified multiple times)"`
|
||||||
|
|
||||||
global *GlobalOptions
|
global *GlobalOptions
|
||||||
|
@ -85,7 +85,7 @@ func (cmd CmdSnapshots) Execute(args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
tab := NewTable()
|
tab := NewTable()
|
||||||
tab.Header = fmt.Sprintf("%-8s %-19s %-10s %s", "ID", "Date", "Host", "Directory")
|
tab.Header = fmt.Sprintf("%-8s %-19s %-10s %s", "ID", "Date", "Source", "Directory")
|
||||||
tab.RowFormat = "%-8s %-19s %-10s %s"
|
tab.RowFormat = "%-8s %-19s %-10s %s"
|
||||||
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
@ -99,7 +99,7 @@ func (cmd CmdSnapshots) Execute(args []string) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if restic.SamePaths(sn.Paths, cmd.Paths) && (cmd.Host == "" || cmd.Host == sn.Hostname) {
|
if restic.SamePaths(sn.Paths, cmd.Paths) && (cmd.Source == "" || cmd.Source == sn.Hostname) {
|
||||||
pos := sort.Search(len(list), func(i int) bool {
|
pos := sort.Search(len(list), func(i int) bool {
|
||||||
return list[i].Time.After(sn.Time)
|
return list[i].Time.After(sn.Time)
|
||||||
})
|
})
|
||||||
|
|
|
@ -77,8 +77,8 @@ 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) {
|
func cmdRestoreLatest(t testing.TB, global GlobalOptions, dir string, paths []string, source string) {
|
||||||
cmd := &CmdRestore{global: &global, Target: dir, Host: host, Paths: paths}
|
cmd := &CmdRestore{global: &global, Target: dir, Source: source, Paths: paths}
|
||||||
OK(t, cmd.Execute([]string{"latest"}))
|
OK(t, cmd.Execute([]string{"latest"}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -665,21 +665,21 @@ func TestRestoreLatest(t *testing.T) {
|
||||||
cmdBackup(t, global, []string{filepath.Dir(p2)}, nil)
|
cmdBackup(t, global, []string{filepath.Dir(p2)}, nil)
|
||||||
cmdCheck(t, global)
|
cmdCheck(t, global)
|
||||||
|
|
||||||
p1rAbs := filepath.Join(env.base, "restore1", "p1/testfile.c")
|
p1r_abs := filepath.Join(env.base, "restore1", "p1/testfile.c")
|
||||||
p2rAbs := filepath.Join(env.base, "restore2", "p2/testfile.c")
|
p2r_abs := filepath.Join(env.base, "restore2", "p2/testfile.c")
|
||||||
|
|
||||||
cmdRestoreLatest(t, global, filepath.Join(env.base, "restore1"), []string{filepath.Dir(p1)}, "")
|
cmdRestoreLatest(t, global, filepath.Join(env.base, "restore1"), []string{filepath.Dir(p1)}, "")
|
||||||
OK(t, testFileSize(p1rAbs, int64(102)))
|
OK(t, testFileSize(p1r_abs, int64(102)))
|
||||||
if _, err := os.Stat(p2rAbs); os.IsNotExist(err) {
|
if _, err := os.Stat(p2r_abs); os.IsNotExist(err) {
|
||||||
Assert(t, os.IsNotExist(err),
|
Assert(t, os.IsNotExist(err),
|
||||||
"expected %v to not exist in restore, but it exists, err %v", p2rAbs, err)
|
"expected %v to not exist in restore, but it exists, err %v", p2r_abs, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdRestoreLatest(t, global, filepath.Join(env.base, "restore2"), []string{filepath.Dir(p2)}, "")
|
cmdRestoreLatest(t, global, filepath.Join(env.base, "restore2"), []string{filepath.Dir(p2)}, "")
|
||||||
OK(t, testFileSize(p2rAbs, int64(103)))
|
OK(t, testFileSize(p2r_abs, int64(103)))
|
||||||
if _, err := os.Stat(p1rAbs); os.IsNotExist(err) {
|
if _, err := os.Stat(p1r_abs); os.IsNotExist(err) {
|
||||||
Assert(t, os.IsNotExist(err),
|
Assert(t, os.IsNotExist(err),
|
||||||
"expected %v to not exist in restore, but it exists, err %v", p1rAbs, err)
|
"expected %v to not exist in restore, but it exists, err %v", p1r_abs, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -103,21 +103,16 @@ 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 {
|
func SamePaths(expected, actual []string) bool {
|
||||||
if expected == nil || actual == nil {
|
if expected == nil || actual == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(expected) != len(actual) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
for i := range expected {
|
for i := range expected {
|
||||||
found := false
|
if expected[i] != actual[i] {
|
||||||
for j := range actual {
|
|
||||||
if expected[i] == actual[j] {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,10 +120,8 @@ func SamePaths(expected, actual []string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error when no snapshot is found for the given criteria
|
|
||||||
var ErrNoSnapshotFound = errors.New("no snapshot found")
|
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) {
|
func FindLatestSnapshot(repo *repository.Repository, targets []string, source string) (backend.ID, error) {
|
||||||
var (
|
var (
|
||||||
latest time.Time
|
latest time.Time
|
||||||
|
|
Loading…
Reference in a new issue