2014-12-07 13:44:01 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2017-03-08 19:29:31 +00:00
|
|
|
"context"
|
2017-03-09 15:06:28 +00:00
|
|
|
"encoding/json"
|
2018-06-09 16:30:59 +00:00
|
|
|
"path"
|
2017-03-07 09:58:09 +00:00
|
|
|
"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"
|
|
|
|
|
2017-07-23 12:21:03 +00:00
|
|
|
"github.com/restic/restic/internal/debug"
|
|
|
|
"github.com/restic/restic/internal/errors"
|
2017-07-24 15:42:25 +00:00
|
|
|
"github.com/restic/restic/internal/restic"
|
2014-12-07 13:44:01 +00:00
|
|
|
)
|
|
|
|
|
2016-09-17 10:36:05 +00:00
|
|
|
var cmdFind = &cobra.Command{
|
|
|
|
Use: "find [flags] PATTERN",
|
2017-09-11 16:32:44 +00:00
|
|
|
Short: "Find a file or directory",
|
2016-09-17 10:36:05 +00:00
|
|
|
Long: `
|
|
|
|
The "find" command searches for files or directories in snapshots stored in the
|
|
|
|
repo. `,
|
2017-08-06 19:02:16 +00:00
|
|
|
DisableAutoGenTag: true,
|
2016-09-17 10:36:05 +00:00
|
|
|
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 {
|
2017-03-07 09:58:09 +00:00
|
|
|
Oldest string
|
|
|
|
Newest string
|
2017-03-08 19:29:31 +00:00
|
|
|
Snapshots []string
|
2017-03-07 09:58:09 +00:00
|
|
|
CaseInsensitive bool
|
2017-03-08 19:29:31 +00:00
|
|
|
ListLong bool
|
|
|
|
Host string
|
|
|
|
Paths []string
|
2017-07-09 10:45:49 +00:00
|
|
|
Tags restic.TagLists
|
2016-09-17 10:36:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var findOptions FindOptions
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
cmdRoot.AddCommand(cmdFind)
|
|
|
|
|
|
|
|
f := cmdFind.Flags()
|
2017-04-06 17:14:38 +00:00
|
|
|
f.StringVarP(&findOptions.Oldest, "oldest", "O", "", "oldest modification date/time")
|
|
|
|
f.StringVarP(&findOptions.Newest, "newest", "N", "", "newest modification date/time")
|
2017-07-07 01:19:06 +00:00
|
|
|
f.StringArrayVarP(&findOptions.Snapshots, "snapshot", "s", nil, "snapshot `id` to search in (can be given multiple times)")
|
2017-03-07 09:58:09 +00:00
|
|
|
f.BoolVarP(&findOptions.CaseInsensitive, "ignore-case", "i", false, "ignore case for pattern")
|
2017-03-08 19:29:31 +00:00
|
|
|
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")
|
2017-07-09 10:45:49 +00:00
|
|
|
f.Var(&findOptions.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot-ID is given")
|
2017-07-07 01:19:06 +00:00
|
|
|
f.StringArrayVar(&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
|
2017-03-07 09:58:09 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2017-03-09 15:06:28 +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"`
|
|
|
|
}{
|
2018-06-09 16:30:59 +00:00
|
|
|
Path: path.Join(prefix, node.Name),
|
2017-03-09 15:06:28 +00:00
|
|
|
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
|
2018-06-09 16:31:13 +00:00
|
|
|
Verbosef("Found matching entries in snapshot %s\n", s.oldsn.ID().Str())
|
2017-03-09 15:06:28 +00:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-04 09:38:46 +00:00
|
|
|
// Finder bundles information needed to find a file or directory.
|
|
|
|
type Finder struct {
|
2017-06-04 09:42:40 +00:00
|
|
|
repo restic.Repository
|
|
|
|
pat findPattern
|
|
|
|
out statefulOutput
|
|
|
|
notfound restic.IDSet
|
2017-06-04 09:38:46 +00:00
|
|
|
}
|
|
|
|
|
2018-06-09 16:31:13 +00:00
|
|
|
// findInTree traverses a tree and outputs matches. foundInSubtree is true if
|
|
|
|
// some match has been found within some subtree. If err is non-nil, the value
|
|
|
|
// of foundInSubtree is invalid.
|
|
|
|
func (f *Finder) findInTree(ctx context.Context, treeID restic.ID, prefix string) (foundInSubtree bool, err error) {
|
2017-06-04 09:42:40 +00:00
|
|
|
if f.notfound.Has(treeID) {
|
2018-01-25 19:49:41 +00:00
|
|
|
debug.Log("%v skipping tree %v, has already been checked", prefix, treeID)
|
2018-06-09 16:31:13 +00:00
|
|
|
return false, nil
|
2017-06-04 09:42:40 +00:00
|
|
|
}
|
|
|
|
|
2018-01-25 19:49:41 +00:00
|
|
|
debug.Log("%v checking tree %v\n", prefix, treeID)
|
2017-03-08 19:29:31 +00:00
|
|
|
|
use global context for check, debug, dump, find, forget, init, key,
list, mount, tag, unlock commands
gh-1434
2017-12-06 12:02:55 +00:00
|
|
|
tree, err := f.repo.LoadTree(ctx, treeID)
|
2014-12-07 13:44:01 +00:00
|
|
|
if err != nil {
|
2018-06-09 16:31:13 +00:00
|
|
|
return false, err
|
2014-12-07 13:44:01 +00:00
|
|
|
}
|
|
|
|
|
2017-06-04 09:42:40 +00:00
|
|
|
var found bool
|
2015-01-10 22:40:10 +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
|
|
|
|
2017-03-07 09:58:09 +00:00
|
|
|
name := node.Name
|
2017-06-04 09:38:46 +00:00
|
|
|
if f.pat.ignoreCase {
|
2017-03-07 09:58:09 +00:00
|
|
|
name = strings.ToLower(name)
|
|
|
|
}
|
|
|
|
|
2018-06-09 16:30:59 +00:00
|
|
|
m, err := path.Match(f.pat.pattern, name)
|
2014-12-07 13:44:01 +00:00
|
|
|
if err != nil {
|
2018-06-09 16:31:13 +00:00
|
|
|
return false, err
|
2014-12-07 13:44:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if m {
|
2017-06-04 09:38:46 +00:00
|
|
|
if !f.pat.oldest.IsZero() && node.ModTime.Before(f.pat.oldest) {
|
|
|
|
debug.Log(" ModTime is older than %s\n", f.pat.oldest)
|
2014-12-07 16:11:01 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2017-06-04 09:38:46 +00:00
|
|
|
if !f.pat.newest.IsZero() && node.ModTime.After(f.pat.newest) {
|
|
|
|
debug.Log(" ModTime is newer than %s\n", f.pat.newest)
|
2014-12-07 16:11:01 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2017-06-04 09:38:46 +00:00
|
|
|
debug.Log(" found match\n")
|
2017-06-04 09:42:40 +00:00
|
|
|
found = true
|
2017-06-04 09:38:46 +00:00
|
|
|
f.out.Print(prefix, node)
|
2014-12-07 13:44:01 +00:00
|
|
|
}
|
|
|
|
|
2016-09-01 19:20:03 +00:00
|
|
|
if node.Type == "dir" {
|
2018-06-09 16:31:13 +00:00
|
|
|
foundSubtree, err := f.findInTree(ctx, *node.Subtree, path.Join(prefix, node.Name))
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if foundSubtree {
|
|
|
|
found = true
|
2014-12-07 13:44:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-04 09:42:40 +00:00
|
|
|
if !found {
|
|
|
|
f.notfound.Insert(treeID)
|
|
|
|
}
|
|
|
|
|
2018-06-09 16:31:13 +00:00
|
|
|
return found, nil
|
2014-12-07 13:44:01 +00:00
|
|
|
}
|
|
|
|
|
use global context for check, debug, dump, find, forget, init, key,
list, mount, tag, unlock commands
gh-1434
2017-12-06 12:02:55 +00:00
|
|
|
func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error {
|
2017-06-04 09:38:46 +00:00
|
|
|
debug.Log("searching in snapshot %s\n for entries within [%s %s]", sn.ID(), f.pat.oldest, f.pat.newest)
|
2014-12-07 13:44:01 +00:00
|
|
|
|
2017-06-04 09:38:46 +00:00
|
|
|
f.out.newsn = sn
|
2018-06-09 16:31:13 +00:00
|
|
|
_, err := f.findInTree(ctx, *sn.Tree, "/")
|
|
|
|
return err
|
2014-12-07 13:44:01 +00:00
|
|
|
}
|
|
|
|
|
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 {
|
2017-02-10 18:39:49 +00:00
|
|
|
return errors.Fatal("wrong number of arguments")
|
2014-12-07 16:11:01 +00:00
|
|
|
}
|
|
|
|
|
2017-03-08 19:29:31 +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 != "" {
|
2017-03-08 19:29:31 +00:00
|
|
|
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 != "" {
|
2017-03-08 19:29:31 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
use global context for check, debug, dump, find, forget, init, key,
list, mount, tag, unlock commands
gh-1434
2017-12-06 12:02:55 +00:00
|
|
|
if err = repo.LoadIndex(gopts.ctx); err != nil {
|
2015-08-27 21:21:44 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-03-08 19:29:31 +00:00
|
|
|
ctx, cancel := context.WithCancel(gopts.ctx)
|
|
|
|
defer cancel()
|
2017-06-04 09:38:46 +00:00
|
|
|
|
|
|
|
f := &Finder{
|
2017-06-04 09:42:40 +00:00
|
|
|
repo: repo,
|
|
|
|
pat: pat,
|
|
|
|
out: statefulOutput{ListLong: opts.ListLong, JSON: globalOptions.JSON},
|
|
|
|
notfound: restic.NewIDSet(),
|
2017-06-04 09:38:46 +00:00
|
|
|
}
|
2017-07-09 10:45:49 +00:00
|
|
|
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, opts.Snapshots) {
|
use global context for check, debug, dump, find, forget, init, key,
list, mount, tag, unlock commands
gh-1434
2017-12-06 12:02:55 +00:00
|
|
|
if err = f.findInSnapshot(ctx, sn); err != nil {
|
2014-12-07 13:44:01 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2017-06-04 09:38:46 +00:00
|
|
|
f.out.Finish()
|
2014-12-07 13:44:01 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|