diff --git a/src/cmds/restic/cmd_find.go b/src/cmds/restic/cmd_find.go index 26ae92ad4..23c39485d 100644 --- a/src/cmds/restic/cmd_find.go +++ b/src/cmds/restic/cmd_find.go @@ -1,6 +1,7 @@ package main import ( + "context" "path/filepath" "strings" "time" @@ -28,8 +29,12 @@ repo. `, type FindOptions struct { Oldest string Newest string - Snapshot string + Snapshots []string CaseInsensitive bool + ListLong bool + Host string + Paths []string + Tags []string } var findOptions FindOptions @@ -40,8 +45,13 @@ func init() { f := cmdFind.Flags() f.StringVarP(&findOptions.Oldest, "oldest", "o", "", "oldest modification date/time") f.StringVarP(&findOptions.Newest, "newest", "n", "", "newest modification date/time") - f.StringVarP(&findOptions.Snapshot, "snapshot", "s", "", "snapshot ID to search in") + f.StringSliceVarP(&findOptions.Snapshots, "snapshot", "s", nil, "snapshot `id` to search in (can be given multiple times)") f.BoolVarP(&findOptions.CaseInsensitive, "ignore-case", "i", false, "ignore case for pattern") + f.BoolVarP(&findOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode") + + f.StringVarP(&findOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given") + f.StringSliceVar(&findOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot-ID is given") + f.StringSliceVar(&findOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given") } type findPattern struct { @@ -50,11 +60,6 @@ type findPattern struct { ignoreCase bool } -type findResult struct { - node *restic.Node - path string -} - var timeFormats = []string{ "2006-01-02", "2006-01-02 15:04", @@ -79,14 +84,14 @@ func parseTime(str string) (time.Time, error) { return time.Time{}, errors.Fatalf("unable to parse time: %q", str) } -func findInTree(repo *repository.Repository, pat findPattern, id restic.ID, path string) ([]findResult, error) { +func findInTree(repo *repository.Repository, pat findPattern, id restic.ID, prefix string, snapshotID *string) error { debug.Log("checking tree %v\n", id) + tree, err := repo.LoadTree(id) if err != nil { - return nil, err + return err } - results := []findResult{} for _, node := range tree.Nodes { debug.Log(" testing entry %q\n", node.Name) @@ -97,7 +102,7 @@ func findInTree(repo *repository.Repository, pat findPattern, id restic.ID, path m, err := filepath.Match(pat.pattern, name) if err != nil { - return nil, err + return err } if m { @@ -112,46 +117,32 @@ func findInTree(repo *repository.Repository, pat findPattern, id restic.ID, path continue } - results = append(results, findResult{node: node, path: path}) + if snapshotID != nil { + Verbosef("Found matching entries in snapshot %s\n", *snapshotID) + snapshotID = nil + } + Printf(formatNode(prefix, node, findOptions.ListLong) + "\n") } else { debug.Log(" pattern does not match\n") } if node.Type == "dir" { - subdirResults, err := findInTree(repo, pat, *node.Subtree, filepath.Join(path, node.Name)) - if err != nil { - return nil, err + if err := findInTree(repo, pat, *node.Subtree, filepath.Join(prefix, node.Name), snapshotID); err != nil { + return err } - - results = append(results, subdirResults...) } } - return results, nil + return nil } -func findInSnapshot(repo *repository.Repository, pat findPattern, id restic.ID) error { - debug.Log("searching in snapshot %s\n for entries within [%s %s]", id.Str(), pat.oldest, pat.newest) +func findInSnapshot(repo *repository.Repository, sn *restic.Snapshot, pat findPattern) error { + debug.Log("searching in snapshot %s\n for entries within [%s %s]", sn.ID(), pat.oldest, pat.newest) - sn, err := restic.LoadSnapshot(repo, id) - if err != nil { + snapshotID := sn.ID().Str() + if err := findInTree(repo, pat, *sn.Tree, string(filepath.Separator), &snapshotID); err != nil { return err } - - results, err := findInTree(repo, pat, *sn.Tree, string(filepath.Separator)) - if err != nil { - return err - } - - if len(results) == 0 { - return nil - } - Verbosef("found %d matching entries in snapshot %s\n", len(results), id) - for _, res := range results { - res.node.Name = filepath.Join(res.path, res.node.Name) - Printf(" %s\n", res.node) - } - return nil } @@ -160,21 +151,21 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error { return errors.Fatal("wrong number of arguments") } - var ( - err error - pat findPattern - ) + var err error + pat := findPattern{pattern: args[0]} + if opts.CaseInsensitive { + pat.pattern = strings.ToLower(pat.pattern) + pat.ignoreCase = true + } if opts.Oldest != "" { - pat.oldest, err = parseTime(opts.Oldest) - if err != nil { + if pat.oldest, err = parseTime(opts.Oldest); err != nil { return err } } if opts.Newest != "" { - pat.newest, err = parseTime(opts.Newest) - if err != nil { + if pat.newest, err = parseTime(opts.Newest); err != nil { return err } } @@ -192,33 +183,14 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error { } } - err = repo.LoadIndex() - if err != nil { + if err = repo.LoadIndex(); err != nil { return err } - pat.pattern = args[0] - - if opts.CaseInsensitive { - pat.pattern = strings.ToLower(pat.pattern) - pat.ignoreCase = true - } - - if opts.Snapshot != "" { - snapshotID, err := restic.FindSnapshot(repo, opts.Snapshot) - if err != nil { - return errors.Fatalf("invalid id %q: %v", args[1], err) - } - - return findInSnapshot(repo, pat, snapshotID) - } - - done := make(chan struct{}) - defer close(done) - for snapshotID := range repo.List(restic.SnapshotFile, done) { - err := findInSnapshot(repo, pat, snapshotID) - - if err != nil { + ctx, cancel := context.WithCancel(gopts.ctx) + defer cancel() + for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, opts.Snapshots) { + if err = findInSnapshot(repo, sn, pat); err != nil { return err } } diff --git a/src/cmds/restic/cmd_ls.go b/src/cmds/restic/cmd_ls.go index c6c05bec1..7d613b45c 100644 --- a/src/cmds/restic/cmd_ls.go +++ b/src/cmds/restic/cmd_ls.go @@ -1,8 +1,7 @@ package main import ( - "fmt" - "os" + "context" "path/filepath" "github.com/spf13/cobra" @@ -13,7 +12,7 @@ import ( ) var cmdLs = &cobra.Command{ - Use: "ls [flags] snapshot-ID", + Use: "ls [flags] [snapshot-ID ...]", Short: "list files in a snapshot", Long: ` The "ls" command allows listing files and directories in a snapshot. @@ -21,7 +20,7 @@ The "ls" command allows listing files and directories in a snapshot. The special snapshot-ID "latest" can be used to list files and directories of the latest snapshot in the repository. `, RunE: func(cmd *cobra.Command, args []string) error { - return runLs(globalOptions, args) + return runLs(lsOptions, globalOptions, args) }, } @@ -29,6 +28,7 @@ The special snapshot-ID "latest" can be used to list files and directories of th type LsOptions struct { ListLong bool Host string + Tags []string Paths []string } @@ -40,42 +40,22 @@ func init() { flags := cmdLs.Flags() flags.BoolVarP(&lsOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode") - flags.StringVarP(&lsOptions.Host, "host", "H", "", `only consider snapshots for this host when the snapshot ID is "latest"`) - flags.StringSliceVar(&lsOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"") + flags.StringVarP(&lsOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given") + flags.StringSliceVar(&lsOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot ID is given") + flags.StringSliceVar(&lsOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot ID is given") } -func printNode(prefix string, n *restic.Node) string { - if !lsOptions.ListLong { - return filepath.Join(prefix, n.Name) - } - - switch n.Type { - case "file": - return fmt.Sprintf("%s %5d %5d %6d %s %s", - n.Mode, n.UID, n.GID, n.Size, n.ModTime.Format(TimeFormat), filepath.Join(prefix, n.Name)) - case "dir": - return fmt.Sprintf("%s %5d %5d %6d %s %s", - n.Mode|os.ModeDir, n.UID, n.GID, n.Size, n.ModTime.Format(TimeFormat), filepath.Join(prefix, n.Name)) - case "symlink": - return fmt.Sprintf("%s %5d %5d %6d %s %s -> %s", - n.Mode|os.ModeSymlink, n.UID, n.GID, n.Size, n.ModTime.Format(TimeFormat), filepath.Join(prefix, n.Name), n.LinkTarget) - default: - return fmt.Sprintf("", n.Type, n.Name) - } -} - -func printTree(prefix string, repo *repository.Repository, id restic.ID) error { - tree, err := repo.LoadTree(id) +func printTree(repo *repository.Repository, id *restic.ID, prefix string) error { + tree, err := repo.LoadTree(*id) if err != nil { return err } for _, entry := range tree.Nodes { - Printf(printNode(prefix, entry) + "\n") + Printf(formatNode(prefix, entry, lsOptions.ListLong) + "\n") if entry.Type == "dir" && entry.Subtree != nil { - err = printTree(filepath.Join(prefix, entry.Name), repo, *entry.Subtree) - if err != nil { + if err = printTree(repo, entry.Subtree, filepath.Join(prefix, entry.Name)); err != nil { return err } } @@ -84,9 +64,9 @@ func printTree(prefix string, repo *repository.Repository, id restic.ID) error { return nil } -func runLs(gopts GlobalOptions, args []string) error { - if len(args) < 1 || len(args) > 2 { - return errors.Fatal("no snapshot ID given") +func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { + if len(args) == 0 && opts.Host == "" && len(opts.Tags) == 0 && len(opts.Paths) == 0 { + return errors.Fatal("Invalid arguments, either give one or more snapshot IDs or set filters.") } repo, err := OpenRepository(gopts) @@ -94,32 +74,18 @@ func runLs(gopts GlobalOptions, args []string) error { return err } - err = repo.LoadIndex() - if err != nil { + if err = repo.LoadIndex(); err != nil { return err } - snapshotIDString := args[0] - var id restic.ID + ctx, cancel := context.WithCancel(gopts.ctx) + defer cancel() + for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) { + Verbosef("snapshot %s of %v at %s):\n", sn.ID().Str(), sn.Paths, sn.Time) - if snapshotIDString == "latest" { - id, err = restic.FindLatestSnapshot(repo, lsOptions.Paths, lsOptions.Host) - if err != nil { - Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, lsOptions.Paths, lsOptions.Host) - } - } else { - id, err = restic.FindSnapshot(repo, snapshotIDString) - if err != nil { - Exitf(1, "invalid id %q: %v", snapshotIDString, err) + if err = printTree(repo, sn.Tree, string(filepath.Separator)); err != nil { + return err } } - - sn, err := restic.LoadSnapshot(repo, id) - if err != nil { - return err - } - - Verbosef("snapshot of %v at %s:\n", sn.Paths, sn.Time) - - return printTree(string(filepath.Separator), repo, *sn.Tree) + return nil } diff --git a/src/cmds/restic/format.go b/src/cmds/restic/format.go index 68fa29fb3..16c374699 100644 --- a/src/cmds/restic/format.go +++ b/src/cmds/restic/format.go @@ -2,7 +2,11 @@ package main import ( "fmt" + "os" + "path/filepath" "time" + + "restic" ) func formatBytes(c uint64) string { @@ -58,3 +62,23 @@ func formatDuration(d time.Duration) string { sec := uint64(d / time.Second) return formatSeconds(sec) } + +func formatNode(prefix string, n *restic.Node, long bool) string { + if !long { + return filepath.Join(prefix, n.Name) + } + + switch n.Type { + case "file": + return fmt.Sprintf("%s %5d %5d %6d %s %s", + n.Mode, n.UID, n.GID, n.Size, n.ModTime.Format(TimeFormat), filepath.Join(prefix, n.Name)) + case "dir": + return fmt.Sprintf("%s %5d %5d %6d %s %s", + n.Mode|os.ModeDir, n.UID, n.GID, n.Size, n.ModTime.Format(TimeFormat), filepath.Join(prefix, n.Name)) + case "symlink": + return fmt.Sprintf("%s %5d %5d %6d %s %s -> %s", + n.Mode|os.ModeSymlink, n.UID, n.GID, n.Size, n.ModTime.Format(TimeFormat), filepath.Join(prefix, n.Name), n.LinkTarget) + default: + return fmt.Sprintf("", n.Type, n.Name) + } +} diff --git a/src/cmds/restic/integration_test.go b/src/cmds/restic/integration_test.go index 9d7ea55fe..0adf495a3 100644 --- a/src/cmds/restic/integration_test.go +++ b/src/cmds/restic/integration_test.go @@ -142,7 +142,9 @@ func testRunLs(t testing.TB, gopts GlobalOptions, snapshotID string) []string { globalOptions.Quiet = quiet }() - OK(t, runLs(gopts, []string{snapshotID})) + opts := LsOptions{} + + OK(t, runLs(opts, gopts, []string{snapshotID})) return strings.Split(string(buf.Bytes()), "\n") }