restic/src/cmds/restic/cmd_find.go

286 lines
6.8 KiB
Go
Raw Normal View History

2014-12-07 13:44:01 +00:00
package main
import (
"context"
"encoding/json"
2014-12-07 13:44:01 +00:00
"path/filepath"
"strings"
2014-12-07 15:30:52 +00:00
"time"
2014-12-07 13:44:01 +00:00
2016-09-17 10:36:05 +00:00
"github.com/spf13/cobra"
"restic"
"restic/debug"
2016-09-01 20:17:37 +00:00
"restic/errors"
"restic/repository"
2014-12-07 13:44:01 +00:00
)
2016-09-17 10:36:05 +00:00
var cmdFind = &cobra.Command{
Use: "find [flags] PATTERN",
Short: "find a file or directory",
Long: `
The "find" command searches for files or directories in snapshots stored in the
repo. `,
RunE: func(cmd *cobra.Command, args []string) error {
return runFind(findOptions, globalOptions, args)
},
2014-12-07 13:44:01 +00:00
}
2017-03-08 19:09:24 +00:00
// FindOptions bundles all options for the find command.
2016-09-17 10:36:05 +00:00
type FindOptions struct {
Oldest string
Newest string
Snapshots []string
CaseInsensitive bool
ListLong bool
Host string
Paths []string
Tags []string
2016-09-17 10:36:05 +00:00
}
var findOptions FindOptions
func init() {
cmdRoot.AddCommand(cmdFind)
f := cmdFind.Flags()
f.StringVarP(&findOptions.Oldest, "oldest", "o", "", "oldest modification date/time")
f.StringVarP(&findOptions.Newest, "newest", "n", "", "newest modification date/time")
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")
2016-09-17 10:36:05 +00:00
}
2014-12-07 16:11:01 +00:00
2016-09-17 10:36:05 +00:00
type findPattern struct {
2014-12-07 16:11:01 +00:00
oldest, newest time.Time
pattern string
ignoreCase bool
2016-09-17 10:36:05 +00:00
}
2014-12-07 16:11:01 +00:00
var timeFormats = []string{
"2006-01-02",
"2006-01-02 15:04",
"2006-01-02 15:04:05",
"2006-01-02 15:04:05 -0700",
"2006-01-02 15:04:05 MST",
"02.01.2006",
"02.01.2006 15:04",
"02.01.2006 15:04:05",
"02.01.2006 15:04:05 -0700",
"02.01.2006 15:04:05 MST",
"Mon Jan 2 15:04:05 -0700 MST 2006",
2014-12-07 15:30:52 +00:00
}
2014-12-07 16:11:01 +00:00
func parseTime(str string) (time.Time, error) {
for _, fmt := range timeFormats {
if t, err := time.ParseInLocation(fmt, str, time.Local); err == nil {
return t, nil
}
}
2016-09-01 20:17:37 +00:00
return time.Time{}, errors.Fatalf("unable to parse time: %q", str)
2014-12-07 16:11:01 +00:00
}
type statefulOutput struct {
ListLong bool
JSON bool
inuse bool
newsn *restic.Snapshot
oldsn *restic.Snapshot
hits int
}
func (s *statefulOutput) PrintJSON(prefix string, node *restic.Node) {
type findNode restic.Node
b, err := json.Marshal(struct {
// Add these attributes
Path string `json:"path,omitempty"`
Permissions string `json:"permissions,omitempty"`
*findNode
// Make the following attributes disappear
Name byte `json:"name,omitempty"`
Inode byte `json:"inode,omitempty"`
ExtendedAttributes byte `json:"extended_attributes,omitempty"`
Device byte `json:"device,omitempty"`
Content byte `json:"content,omitempty"`
Subtree byte `json:"subtree,omitempty"`
}{
Path: filepath.Join(prefix, node.Name),
Permissions: node.Mode.String(),
findNode: (*findNode)(node),
})
if err != nil {
Warnf("Marshall failed: %v\n", err)
return
}
if !s.inuse {
Printf("[")
s.inuse = true
}
if s.newsn != s.oldsn {
if s.oldsn != nil {
Printf("],\"hits\":%d,\"snapshot\":%q},", s.hits, s.oldsn.ID())
}
Printf(`{"matches":[`)
s.oldsn = s.newsn
s.hits = 0
}
if s.hits > 0 {
Printf(",")
}
Printf(string(b))
s.hits++
}
func (s *statefulOutput) PrintNormal(prefix string, node *restic.Node) {
if s.newsn != s.oldsn {
if s.oldsn != nil {
Verbosef("\n")
}
s.oldsn = s.newsn
Verbosef("Found matching entries in snapshot %s\n", s.oldsn.ID())
}
Printf(formatNode(prefix, node, s.ListLong) + "\n")
}
func (s *statefulOutput) Print(prefix string, node *restic.Node) {
if s.JSON {
s.PrintJSON(prefix, node)
} else {
s.PrintNormal(prefix, node)
}
}
func (s *statefulOutput) Finish() {
if s.JSON {
// do some finishing up
if s.oldsn != nil {
Printf("],\"hits\":%d,\"snapshot\":%q}", s.hits, s.oldsn.ID())
}
if s.inuse {
Printf("]\n")
} else {
Printf("[]\n")
}
return
}
}
func findInTree(repo *repository.Repository, pat *findPattern, id restic.ID, prefix string, state *statefulOutput) error {
2016-09-27 20:35:08 +00:00
debug.Log("checking tree %v\n", id)
tree, err := repo.LoadTree(id)
2014-12-07 13:44:01 +00:00
if err != nil {
return err
2014-12-07 13:44:01 +00:00
}
for _, node := range tree.Nodes {
2016-09-27 20:35:08 +00:00
debug.Log(" testing entry %q\n", node.Name)
2014-12-07 16:11:01 +00:00
name := node.Name
if pat.ignoreCase {
name = strings.ToLower(name)
}
m, err := filepath.Match(pat.pattern, name)
2014-12-07 13:44:01 +00:00
if err != nil {
return err
2014-12-07 13:44:01 +00:00
}
if m {
2016-09-27 20:35:08 +00:00
debug.Log(" pattern matches\n")
2016-09-17 10:36:05 +00:00
if !pat.oldest.IsZero() && node.ModTime.Before(pat.oldest) {
2016-09-27 20:35:08 +00:00
debug.Log(" ModTime is older than %s\n", pat.oldest)
2014-12-07 16:11:01 +00:00
continue
}
2016-09-17 10:36:05 +00:00
if !pat.newest.IsZero() && node.ModTime.After(pat.newest) {
2016-09-27 20:35:08 +00:00
debug.Log(" ModTime is newer than %s\n", pat.newest)
2014-12-07 16:11:01 +00:00
continue
}
state.Print(prefix, node)
2014-12-07 16:11:01 +00:00
} else {
2016-09-27 20:35:08 +00:00
debug.Log(" pattern does not match\n")
2014-12-07 13:44:01 +00:00
}
2016-09-01 19:20:03 +00:00
if node.Type == "dir" {
if err := findInTree(repo, pat, *node.Subtree, filepath.Join(prefix, node.Name), state); err != nil {
return err
2014-12-07 13:44:01 +00:00
}
}
}
return nil
2014-12-07 13:44:01 +00:00
}
func findInSnapshot(repo *repository.Repository, sn *restic.Snapshot, pat findPattern, state *statefulOutput) error {
debug.Log("searching in snapshot %s\n for entries within [%s %s]", sn.ID(), pat.oldest, pat.newest)
2014-12-07 13:44:01 +00:00
state.newsn = sn
if err := findInTree(repo, &pat, *sn.Tree, string(filepath.Separator), state); err != nil {
2014-12-07 13:44:01 +00:00
return err
}
return nil
}
2016-09-17 10:36:05 +00:00
func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
2014-12-07 16:11:01 +00:00
if len(args) != 1 {
return errors.Fatal("wrong number of arguments")
2014-12-07 16:11:01 +00:00
}
var err error
pat := findPattern{pattern: args[0]}
if opts.CaseInsensitive {
pat.pattern = strings.ToLower(pat.pattern)
pat.ignoreCase = true
}
2014-12-07 16:11:01 +00:00
2016-09-17 10:36:05 +00:00
if opts.Oldest != "" {
if pat.oldest, err = parseTime(opts.Oldest); err != nil {
2014-12-07 16:11:01 +00:00
return err
}
}
2016-09-17 10:36:05 +00:00
if opts.Newest != "" {
if pat.newest, err = parseTime(opts.Newest); err != nil {
2014-12-07 16:11:01 +00:00
return err
}
2014-12-07 15:30:52 +00:00
}
2016-09-17 10:36:05 +00:00
repo, err := OpenRepository(gopts)
2014-12-07 15:30:52 +00:00
if err != nil {
return err
2014-12-07 13:44:01 +00:00
}
2016-09-17 10:36:05 +00:00
if !gopts.NoLock {
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
}
2015-06-27 12:40:18 +00:00
}
if err = repo.LoadIndex(); err != nil {
2015-08-27 21:21:44 +00:00
return err
}
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
state := statefulOutput{ListLong: opts.ListLong, JSON: globalOptions.JSON}
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, opts.Snapshots) {
if err = findInSnapshot(repo, sn, pat, &state); err != nil {
2014-12-07 13:44:01 +00:00
return err
}
}
state.Finish()
2014-12-07 13:44:01 +00:00
return nil
}