package main import ( "context" "encoding/json" "path/filepath" "strings" "time" "github.com/spf13/cobra" "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" ) 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. `, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { return runFind(findOptions, globalOptions, args) }, } // FindOptions bundles all options for the find command. type FindOptions struct { Oldest string Newest string Snapshots []string CaseInsensitive bool ListLong bool Host string Paths []string Tags restic.TagLists } 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.StringArrayVarP(&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.Var(&findOptions.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot-ID is given") f.StringArrayVar(&findOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given") } type findPattern struct { oldest, newest time.Time pattern string ignoreCase bool } 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", } 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 } } return time.Time{}, errors.Fatalf("unable to parse time: %q", str) } 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 } } // Finder bundles information needed to find a file or directory. type Finder struct { repo restic.Repository pat findPattern out statefulOutput notfound restic.IDSet } func (f *Finder) findInTree(ctx context.Context, treeID restic.ID, prefix string) error { if f.notfound.Has(treeID) { debug.Log("%v skipping tree %v, has already been checked", prefix, treeID) return nil } debug.Log("%v checking tree %v\n", prefix, treeID) tree, err := f.repo.LoadTree(ctx, treeID) if err != nil { return err } var found bool for _, node := range tree.Nodes { debug.Log(" testing entry %q\n", node.Name) name := node.Name if f.pat.ignoreCase { name = strings.ToLower(name) } m, err := filepath.Match(f.pat.pattern, name) if err != nil { return err } if m { if !f.pat.oldest.IsZero() && node.ModTime.Before(f.pat.oldest) { debug.Log(" ModTime is older than %s\n", f.pat.oldest) continue } if !f.pat.newest.IsZero() && node.ModTime.After(f.pat.newest) { debug.Log(" ModTime is newer than %s\n", f.pat.newest) continue } debug.Log(" found match\n") found = true f.out.Print(prefix, node) } if node.Type == "dir" { if err := f.findInTree(ctx, *node.Subtree, filepath.Join(prefix, node.Name)); err != nil { return err } } } if !found { f.notfound.Insert(treeID) } return nil } func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error { debug.Log("searching in snapshot %s\n for entries within [%s %s]", sn.ID(), f.pat.oldest, f.pat.newest) f.out.newsn = sn return f.findInTree(ctx, *sn.Tree, string(filepath.Separator)) } func runFind(opts FindOptions, gopts GlobalOptions, args []string) error { if len(args) != 1 { return errors.Fatal("wrong number of arguments") } var err error pat := findPattern{pattern: args[0]} if opts.CaseInsensitive { pat.pattern = strings.ToLower(pat.pattern) pat.ignoreCase = true } if opts.Oldest != "" { if pat.oldest, err = parseTime(opts.Oldest); err != nil { return err } } if opts.Newest != "" { if pat.newest, err = parseTime(opts.Newest); err != nil { return err } } repo, err := OpenRepository(gopts) if err != nil { return err } if !gopts.NoLock { lock, err := lockRepo(repo) defer unlockRepo(lock) if err != nil { return err } } if err = repo.LoadIndex(gopts.ctx); err != nil { return err } ctx, cancel := context.WithCancel(gopts.ctx) defer cancel() f := &Finder{ repo: repo, pat: pat, out: statefulOutput{ListLong: opts.ListLong, JSON: globalOptions.JSON}, notfound: restic.NewIDSet(), } for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, opts.Snapshots) { if err = f.findInSnapshot(ctx, sn); err != nil { return err } } f.out.Finish() return nil }