mirror of
https://github.com/octoleo/restic.git
synced 2024-11-02 19:49:44 +00:00
Refactor find
and ls
commands
Implement filtering by using `FindFilteredSnapshots()` to iterate over the snapshots Refactor cmd_ls' `PrintNode()` into format.go, reuse its pretty printing in both `find` and `ls` commands. Use contexts.
This commit is contained in:
parent
3432e7edcd
commit
8a92687d9a
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -28,8 +29,12 @@ repo. `,
|
|||||||
type FindOptions struct {
|
type FindOptions struct {
|
||||||
Oldest string
|
Oldest string
|
||||||
Newest string
|
Newest string
|
||||||
Snapshot string
|
Snapshots []string
|
||||||
CaseInsensitive bool
|
CaseInsensitive bool
|
||||||
|
ListLong bool
|
||||||
|
Host string
|
||||||
|
Paths []string
|
||||||
|
Tags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
var findOptions FindOptions
|
var findOptions FindOptions
|
||||||
@ -40,8 +45,13 @@ func init() {
|
|||||||
f := cmdFind.Flags()
|
f := cmdFind.Flags()
|
||||||
f.StringVarP(&findOptions.Oldest, "oldest", "o", "", "oldest modification date/time")
|
f.StringVarP(&findOptions.Oldest, "oldest", "o", "", "oldest modification date/time")
|
||||||
f.StringVarP(&findOptions.Newest, "newest", "n", "", "newest 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.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 {
|
type findPattern struct {
|
||||||
@ -50,11 +60,6 @@ type findPattern struct {
|
|||||||
ignoreCase bool
|
ignoreCase bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type findResult struct {
|
|
||||||
node *restic.Node
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
|
|
||||||
var timeFormats = []string{
|
var timeFormats = []string{
|
||||||
"2006-01-02",
|
"2006-01-02",
|
||||||
"2006-01-02 15:04",
|
"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)
|
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)
|
debug.Log("checking tree %v\n", id)
|
||||||
|
|
||||||
tree, err := repo.LoadTree(id)
|
tree, err := repo.LoadTree(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
results := []findResult{}
|
|
||||||
for _, node := range tree.Nodes {
|
for _, node := range tree.Nodes {
|
||||||
debug.Log(" testing entry %q\n", node.Name)
|
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)
|
m, err := filepath.Match(pat.pattern, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if m {
|
if m {
|
||||||
@ -112,46 +117,32 @@ func findInTree(repo *repository.Repository, pat findPattern, id restic.ID, path
|
|||||||
continue
|
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 {
|
} else {
|
||||||
debug.Log(" pattern does not match\n")
|
debug.Log(" pattern does not match\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.Type == "dir" {
|
if node.Type == "dir" {
|
||||||
subdirResults, err := findInTree(repo, pat, *node.Subtree, filepath.Join(path, node.Name))
|
if err := findInTree(repo, pat, *node.Subtree, filepath.Join(prefix, node.Name), snapshotID); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
results = append(results, subdirResults...)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func findInSnapshot(repo *repository.Repository, pat findPattern, id restic.ID) error {
|
func findInSnapshot(repo *repository.Repository, sn *restic.Snapshot, pat findPattern) error {
|
||||||
debug.Log("searching in snapshot %s\n for entries within [%s %s]", id.Str(), pat.oldest, pat.newest)
|
debug.Log("searching in snapshot %s\n for entries within [%s %s]", sn.ID(), pat.oldest, pat.newest)
|
||||||
|
|
||||||
sn, err := restic.LoadSnapshot(repo, id)
|
snapshotID := sn.ID().Str()
|
||||||
if err != nil {
|
if err := findInTree(repo, pat, *sn.Tree, string(filepath.Separator), &snapshotID); err != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,21 +151,21 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
|
|||||||
return errors.Fatal("wrong number of arguments")
|
return errors.Fatal("wrong number of arguments")
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var err error
|
||||||
err error
|
pat := findPattern{pattern: args[0]}
|
||||||
pat findPattern
|
if opts.CaseInsensitive {
|
||||||
)
|
pat.pattern = strings.ToLower(pat.pattern)
|
||||||
|
pat.ignoreCase = true
|
||||||
|
}
|
||||||
|
|
||||||
if opts.Oldest != "" {
|
if opts.Oldest != "" {
|
||||||
pat.oldest, err = parseTime(opts.Oldest)
|
if pat.oldest, err = parseTime(opts.Oldest); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Newest != "" {
|
if opts.Newest != "" {
|
||||||
pat.newest, err = parseTime(opts.Newest)
|
if pat.newest, err = parseTime(opts.Newest); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -192,33 +183,14 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = repo.LoadIndex()
|
if err = repo.LoadIndex(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pat.pattern = args[0]
|
ctx, cancel := context.WithCancel(gopts.ctx)
|
||||||
|
defer cancel()
|
||||||
if opts.CaseInsensitive {
|
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, opts.Snapshots) {
|
||||||
pat.pattern = strings.ToLower(pat.pattern)
|
if err = findInSnapshot(repo, sn, pat); err != nil {
|
||||||
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 {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"context"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -13,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var cmdLs = &cobra.Command{
|
var cmdLs = &cobra.Command{
|
||||||
Use: "ls [flags] snapshot-ID",
|
Use: "ls [flags] [snapshot-ID ...]",
|
||||||
Short: "list files in a snapshot",
|
Short: "list files in a snapshot",
|
||||||
Long: `
|
Long: `
|
||||||
The "ls" command allows listing files and directories in a snapshot.
|
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.
|
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 {
|
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 {
|
type LsOptions struct {
|
||||||
ListLong bool
|
ListLong bool
|
||||||
Host string
|
Host string
|
||||||
|
Tags []string
|
||||||
Paths []string
|
Paths []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,42 +40,22 @@ func init() {
|
|||||||
flags := cmdLs.Flags()
|
flags := cmdLs.Flags()
|
||||||
flags.BoolVarP(&lsOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode")
|
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.StringVarP(&lsOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given")
|
||||||
flags.StringSliceVar(&lsOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"")
|
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 {
|
func printTree(repo *repository.Repository, id *restic.ID, prefix string) error {
|
||||||
if !lsOptions.ListLong {
|
tree, err := repo.LoadTree(*id)
|
||||||
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("<Node(%s) %s>", n.Type, n.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printTree(prefix string, repo *repository.Repository, id restic.ID) error {
|
|
||||||
tree, err := repo.LoadTree(id)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, entry := range tree.Nodes {
|
for _, entry := range tree.Nodes {
|
||||||
Printf(printNode(prefix, entry) + "\n")
|
Printf(formatNode(prefix, entry, lsOptions.ListLong) + "\n")
|
||||||
|
|
||||||
if entry.Type == "dir" && entry.Subtree != nil {
|
if entry.Type == "dir" && entry.Subtree != nil {
|
||||||
err = printTree(filepath.Join(prefix, entry.Name), repo, *entry.Subtree)
|
if err = printTree(repo, entry.Subtree, filepath.Join(prefix, entry.Name)); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,9 +64,9 @@ func printTree(prefix string, repo *repository.Repository, id restic.ID) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runLs(gopts GlobalOptions, args []string) error {
|
func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
|
||||||
if len(args) < 1 || len(args) > 2 {
|
if len(args) == 0 && opts.Host == "" && len(opts.Tags) == 0 && len(opts.Paths) == 0 {
|
||||||
return errors.Fatal("no snapshot ID given")
|
return errors.Fatal("Invalid arguments, either give one or more snapshot IDs or set filters.")
|
||||||
}
|
}
|
||||||
|
|
||||||
repo, err := OpenRepository(gopts)
|
repo, err := OpenRepository(gopts)
|
||||||
@ -94,32 +74,18 @@ func runLs(gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = repo.LoadIndex()
|
if err = repo.LoadIndex(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshotIDString := args[0]
|
ctx, cancel := context.WithCancel(gopts.ctx)
|
||||||
var id restic.ID
|
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" {
|
if err = printTree(repo, sn.Tree, string(filepath.Separator)); err != nil {
|
||||||
id, err = restic.FindLatestSnapshot(repo, lsOptions.Paths, lsOptions.Host)
|
return err
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"restic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func formatBytes(c uint64) string {
|
func formatBytes(c uint64) string {
|
||||||
@ -58,3 +62,23 @@ func formatDuration(d time.Duration) string {
|
|||||||
sec := uint64(d / time.Second)
|
sec := uint64(d / time.Second)
|
||||||
return formatSeconds(sec)
|
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("<Node(%s) %s>", n.Type, n.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -142,7 +142,9 @@ func testRunLs(t testing.TB, gopts GlobalOptions, snapshotID string) []string {
|
|||||||
globalOptions.Quiet = quiet
|
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")
|
return strings.Split(string(buf.Bytes()), "\n")
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user