From 49f82f54b057e5ba87f6f10b5010c364982470f6 Mon Sep 17 00:00:00 2001 From: Gerdus van Zyl Date: Tue, 10 May 2016 21:23:18 +0200 Subject: [PATCH 1/4] rebase, change source to host and add description to manual --- doc/Manual.md | 33 +++++++++++++++- src/cmds/restic/cmd_backup.go | 49 ------------------------ src/cmds/restic/cmd_restore.go | 18 +++++++-- src/cmds/restic/cmd_snapshots.go | 26 ++++++++----- src/cmds/restic/integration_test.go | 59 +++++++++++++++++++++++++++++ src/restic/snapshot.go | 56 ++++++++++++++++++++++++++- 6 files changed, 177 insertions(+), 64 deletions(-) diff --git a/doc/Manual.md b/doc/Manual.md index 6ee5db947..cac48d967 100644 --- a/doc/Manual.md +++ b/doc/Manual.md @@ -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 are also possible. # Restore a snapshot @@ -218,6 +241,14 @@ restore the contents of the latest snapshot to `/tmp/restore-work`: enter password for repository: restoring 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 + + + # Manage repository keys The `key` command allows you to set multiple access keys or passwords per diff --git a/src/cmds/restic/cmd_backup.go b/src/cmds/restic/cmd_backup.go index 462f4698a..d68672b8b 100644 --- a/src/cmds/restic/cmd_backup.go +++ b/src/cmds/restic/cmd_backup.go @@ -218,55 +218,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) { diff --git a/src/cmds/restic/cmd_restore.go b/src/cmds/restic/cmd_restore.go index 64626e1c2..2d925b753 100644 --- a/src/cmds/restic/cmd_restore.go +++ b/src/cmds/restic/cmd_restore.go @@ -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) diff --git a/src/cmds/restic/cmd_snapshots.go b/src/cmds/restic/cmd_snapshots.go index 95bd598a8..309e3bb56 100644 --- a/src/cmds/restic/cmd_snapshots.go +++ b/src/cmds/restic/cmd_snapshots.go @@ -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) diff --git a/src/cmds/restic/integration_test.go b/src/cmds/restic/integration_test.go index 8c42c7548..94992104f 100644 --- a/src/cmds/restic/integration_test.go +++ b/src/cmds/restic/integration_test.go @@ -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") diff --git a/src/restic/snapshot.go b/src/restic/snapshot.go index 18aa9cad0..9c95e84cf 100644 --- a/src/restic/snapshot.go +++ b/src/restic/snapshot.go @@ -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("", sn.Paths, sn.Time) + return fmt.Sprintf("", 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 { From 3cb68ddb0db1a6063eea93cd53d08ba1976d721c Mon Sep 17 00:00:00 2001 From: Gerdus van Zyl Date: Wed, 27 Apr 2016 18:36:48 +0200 Subject: [PATCH 2/4] 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" --- src/cmds/restic/cmd_backup.go | 90 ++--------------------------- src/cmds/restic/cmd_restore.go | 6 +- src/cmds/restic/cmd_snapshots.go | 6 +- src/cmds/restic/integration_test.go | 20 +++---- src/restic/snapshot.go | 15 ++--- 5 files changed, 26 insertions(+), 111 deletions(-) diff --git a/src/cmds/restic/cmd_backup.go b/src/cmds/restic/cmd_backup.go index d68672b8b..7e83dfb6a 100644 --- a/src/cmds/restic/cmd_backup.go +++ b/src/cmds/restic/cmd_backup.go @@ -10,7 +10,6 @@ import ( "restic/backend" "restic/debug" "restic/filter" - "restic/repository" "strings" "time" @@ -18,12 +17,10 @@ import ( ) type CmdBackup struct { - 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"` - 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"` - 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"` + 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"` + 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"` global *GlobalOptions } @@ -177,47 +174,6 @@ func (cmd CmdBackup) newArchiveProgress(todo restic.Stat) *restic.Progress { 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 // items exist at all. func filterExisting(items []string) (result []string, err error) { @@ -237,41 +193,7 @@ func filterExisting(items []string) (result []string, err error) { 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 { - if cmd.Stdin { - return cmd.readFromStdin(args) - } - if len(args) == 0 { 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 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 } } diff --git a/src/cmds/restic/cmd_restore.go b/src/cmds/restic/cmd_restore.go index 2d925b753..b6c31a924 100644 --- a/src/cmds/restic/cmd_restore.go +++ b/src/cmds/restic/cmd_restore.go @@ -14,7 +14,7 @@ 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)"` + 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)"` global *GlobalOptions @@ -72,9 +72,9 @@ func (cmd CmdRestore) Execute(args []string) error { var id backend.ID if snapshotIDString == "latest" { - id, err = restic.FindLatestSnapshot(repo, cmd.Paths, cmd.Host) + id, err = restic.FindLatestSnapshot(repo, cmd.Paths, cmd.Source) 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 { id, err = restic.FindSnapshot(repo, snapshotIDString) diff --git a/src/cmds/restic/cmd_snapshots.go b/src/cmds/restic/cmd_snapshots.go index 309e3bb56..886467bea 100644 --- a/src/cmds/restic/cmd_snapshots.go +++ b/src/cmds/restic/cmd_snapshots.go @@ -48,7 +48,7 @@ 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"` + 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)"` global *GlobalOptions @@ -85,7 +85,7 @@ func (cmd CmdSnapshots) Execute(args []string) error { } 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" done := make(chan struct{}) @@ -99,7 +99,7 @@ func (cmd CmdSnapshots) Execute(args []string) error { 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 { return list[i].Time.After(sn.Time) }) diff --git a/src/cmds/restic/integration_test.go b/src/cmds/restic/integration_test.go index 94992104f..b8d896a1b 100644 --- a/src/cmds/restic/integration_test.go +++ b/src/cmds/restic/integration_test.go @@ -77,8 +77,8 @@ 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} +func cmdRestoreLatest(t testing.TB, global GlobalOptions, dir string, paths []string, source string) { + cmd := &CmdRestore{global: &global, Target: dir, Source: source, Paths: paths} OK(t, cmd.Execute([]string{"latest"})) } @@ -665,21 +665,21 @@ func TestRestoreLatest(t *testing.T) { 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") + p1r_abs := filepath.Join(env.base, "restore1", "p1/testfile.c") + p2r_abs := 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) { + OK(t, testFileSize(p1r_abs, int64(102))) + if _, err := os.Stat(p2r_abs); 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)}, "") - OK(t, testFileSize(p2rAbs, int64(103))) - if _, err := os.Stat(p1rAbs); os.IsNotExist(err) { + OK(t, testFileSize(p2r_abs, int64(103))) + if _, err := os.Stat(p1r_abs); 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) } }) diff --git a/src/restic/snapshot.go b/src/restic/snapshot.go index 9c95e84cf..3f762e621 100644 --- a/src/restic/snapshot.go +++ b/src/restic/snapshot.go @@ -103,21 +103,16 @@ 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 } + if len(expected) != len(actual) { + return false + } for i := range expected { - found := false - for j := range actual { - if expected[i] == actual[j] { - found = true - break - } - } - if !found { + if expected[i] != actual[i] { return false } } @@ -125,10 +120,8 @@ func SamePaths(expected, actual []string) bool { 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 From 8010a0d90c36051ed83c8bc40325066dbea2414e Mon Sep 17 00:00:00 2001 From: Gerdus van Zyl Date: Tue, 10 May 2016 21:51:56 +0200 Subject: [PATCH 3/4] fix --- src/cmds/restic/cmd_backup.go | 85 +++++++++++++++++++++++++++-- src/cmds/restic/cmd_restore.go | 6 +- src/cmds/restic/cmd_snapshots.go | 6 +- src/cmds/restic/integration_test.go | 20 +++---- src/restic/snapshot.go | 15 +++-- 5 files changed, 108 insertions(+), 24 deletions(-) diff --git a/src/cmds/restic/cmd_backup.go b/src/cmds/restic/cmd_backup.go index 7e83dfb6a..400608b97 100644 --- a/src/cmds/restic/cmd_backup.go +++ b/src/cmds/restic/cmd_backup.go @@ -17,10 +17,12 @@ import ( ) type CmdBackup struct { - 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"` - 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"` + 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"` + 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"` + 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 } @@ -174,6 +176,47 @@ func (cmd CmdBackup) newArchiveProgress(todo restic.Stat) *restic.Progress { 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 // items exist at all. func filterExisting(items []string) (result []string, err error) { @@ -193,7 +236,41 @@ func filterExisting(items []string) (result []string, err error) { 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 { + if cmd.Stdin { + return cmd.readFromStdin(args) + } + if len(args) == 0 { return fmt.Errorf("wrong number of parameters, Usage: %s", cmd.Usage()) } diff --git a/src/cmds/restic/cmd_restore.go b/src/cmds/restic/cmd_restore.go index b6c31a924..2d925b753 100644 --- a/src/cmds/restic/cmd_restore.go +++ b/src/cmds/restic/cmd_restore.go @@ -14,7 +14,7 @@ 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"` - Source string `short:"s" long:"source" description:"Source Filter (for id=latest)"` + 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 @@ -72,9 +72,9 @@ func (cmd CmdRestore) Execute(args []string) error { var id backend.ID if snapshotIDString == "latest" { - id, err = restic.FindLatestSnapshot(repo, cmd.Paths, cmd.Source) + 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 Source:%v", err, cmd.Paths, cmd.Source) + 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) diff --git a/src/cmds/restic/cmd_snapshots.go b/src/cmds/restic/cmd_snapshots.go index 886467bea..309e3bb56 100644 --- a/src/cmds/restic/cmd_snapshots.go +++ b/src/cmds/restic/cmd_snapshots.go @@ -48,7 +48,7 @@ func (t Table) Write(w io.Writer) error { const TimeFormat = "2006-01-02 15:04:05" type CmdSnapshots struct { - Source string `short:"s" long:"source" description:"Source Filter"` + 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 @@ -85,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{}) @@ -99,7 +99,7 @@ func (cmd CmdSnapshots) Execute(args []string) error { continue } - if restic.SamePaths(sn.Paths, cmd.Paths) && (cmd.Source == "" || cmd.Source == sn.Hostname) { + 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) }) diff --git a/src/cmds/restic/integration_test.go b/src/cmds/restic/integration_test.go index b8d896a1b..94992104f 100644 --- a/src/cmds/restic/integration_test.go +++ b/src/cmds/restic/integration_test.go @@ -77,8 +77,8 @@ 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, source string) { - cmd := &CmdRestore{global: &global, Target: dir, Source: source, Paths: paths} +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"})) } @@ -665,21 +665,21 @@ func TestRestoreLatest(t *testing.T) { cmdBackup(t, global, []string{filepath.Dir(p2)}, nil) cmdCheck(t, global) - p1r_abs := filepath.Join(env.base, "restore1", "p1/testfile.c") - p2r_abs := filepath.Join(env.base, "restore2", "p2/testfile.c") + 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(p1r_abs, int64(102))) - if _, err := os.Stat(p2r_abs); os.IsNotExist(err) { + 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", p2r_abs, 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(p2r_abs, int64(103))) - if _, err := os.Stat(p1r_abs); os.IsNotExist(err) { + 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", p1r_abs, err) + "expected %v to not exist in restore, but it exists, err %v", p1rAbs, err) } }) diff --git a/src/restic/snapshot.go b/src/restic/snapshot.go index 3f762e621..9c95e84cf 100644 --- a/src/restic/snapshot.go +++ b/src/restic/snapshot.go @@ -103,16 +103,21 @@ 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 } - if len(expected) != len(actual) { - return false - } for i := range expected { - if expected[i] != actual[i] { + found := false + for j := range actual { + if expected[i] == actual[j] { + found = true + break + } + } + if !found { return false } } @@ -120,8 +125,10 @@ func SamePaths(expected, actual []string) bool { 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 From 73e9cac5c425b59594e692fd9d8404f2b1507e72 Mon Sep 17 00:00:00 2001 From: Gerdus van Zyl Date: Tue, 10 May 2016 22:12:33 +0200 Subject: [PATCH 4/4] gofmt + small doc fix --- doc/Manual.md | 9 +++++---- src/cmds/restic/cmd_restore.go | 2 +- src/cmds/restic/cmd_snapshots.go | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/doc/Manual.md b/doc/Manual.md index cac48d967..5e5d495c7 100644 --- a/doc/Manual.md +++ b/doc/Manual.md @@ -230,7 +230,7 @@ Or filter by host: bdbd3439 2015-05-08 21:45:17 luigi /home/art 9f0bc19e 2015-05-08 21:46:11 luigi /srv -Combining filters are also possible. +Combining filters is also possible. # Restore a snapshot @@ -241,12 +241,13 @@ restore the contents of the latest snapshot to `/tmp/restore-work`: enter password for repository: restoring 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 +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 to /tmp/restore-work # Manage repository keys diff --git a/src/cmds/restic/cmd_restore.go b/src/cmds/restic/cmd_restore.go index 2d925b753..97b77ec1b 100644 --- a/src/cmds/restic/cmd_restore.go +++ b/src/cmds/restic/cmd_restore.go @@ -14,7 +14,7 @@ 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)"` + 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 diff --git a/src/cmds/restic/cmd_snapshots.go b/src/cmds/restic/cmd_snapshots.go index 309e3bb56..abca558f5 100644 --- a/src/cmds/restic/cmd_snapshots.go +++ b/src/cmds/restic/cmd_snapshots.go @@ -48,8 +48,8 @@ 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)"` + 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 }