github.com/mckael/restic@v0.8.3/cmd/restic/cmd_find.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"path/filepath"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/spf13/cobra"
    11  
    12  	"github.com/restic/restic/internal/debug"
    13  	"github.com/restic/restic/internal/errors"
    14  	"github.com/restic/restic/internal/restic"
    15  )
    16  
    17  var cmdFind = &cobra.Command{
    18  	Use:   "find [flags] PATTERN",
    19  	Short: "Find a file or directory",
    20  	Long: `
    21  The "find" command searches for files or directories in snapshots stored in the
    22  repo. `,
    23  	DisableAutoGenTag: true,
    24  	RunE: func(cmd *cobra.Command, args []string) error {
    25  		return runFind(findOptions, globalOptions, args)
    26  	},
    27  }
    28  
    29  // FindOptions bundles all options for the find command.
    30  type FindOptions struct {
    31  	Oldest          string
    32  	Newest          string
    33  	Snapshots       []string
    34  	CaseInsensitive bool
    35  	ListLong        bool
    36  	Host            string
    37  	Paths           []string
    38  	Tags            restic.TagLists
    39  }
    40  
    41  var findOptions FindOptions
    42  
    43  func init() {
    44  	cmdRoot.AddCommand(cmdFind)
    45  
    46  	f := cmdFind.Flags()
    47  	f.StringVarP(&findOptions.Oldest, "oldest", "O", "", "oldest modification date/time")
    48  	f.StringVarP(&findOptions.Newest, "newest", "N", "", "newest modification date/time")
    49  	f.StringArrayVarP(&findOptions.Snapshots, "snapshot", "s", nil, "snapshot `id` to search in (can be given multiple times)")
    50  	f.BoolVarP(&findOptions.CaseInsensitive, "ignore-case", "i", false, "ignore case for pattern")
    51  	f.BoolVarP(&findOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode")
    52  
    53  	f.StringVarP(&findOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given")
    54  	f.Var(&findOptions.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot-ID is given")
    55  	f.StringArrayVar(&findOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given")
    56  }
    57  
    58  type findPattern struct {
    59  	oldest, newest time.Time
    60  	pattern        string
    61  	ignoreCase     bool
    62  }
    63  
    64  var timeFormats = []string{
    65  	"2006-01-02",
    66  	"2006-01-02 15:04",
    67  	"2006-01-02 15:04:05",
    68  	"2006-01-02 15:04:05 -0700",
    69  	"2006-01-02 15:04:05 MST",
    70  	"02.01.2006",
    71  	"02.01.2006 15:04",
    72  	"02.01.2006 15:04:05",
    73  	"02.01.2006 15:04:05 -0700",
    74  	"02.01.2006 15:04:05 MST",
    75  	"Mon Jan 2 15:04:05 -0700 MST 2006",
    76  }
    77  
    78  func parseTime(str string) (time.Time, error) {
    79  	for _, fmt := range timeFormats {
    80  		if t, err := time.ParseInLocation(fmt, str, time.Local); err == nil {
    81  			return t, nil
    82  		}
    83  	}
    84  
    85  	return time.Time{}, errors.Fatalf("unable to parse time: %q", str)
    86  }
    87  
    88  type statefulOutput struct {
    89  	ListLong bool
    90  	JSON     bool
    91  	inuse    bool
    92  	newsn    *restic.Snapshot
    93  	oldsn    *restic.Snapshot
    94  	hits     int
    95  }
    96  
    97  func (s *statefulOutput) PrintJSON(prefix string, node *restic.Node) {
    98  	type findNode restic.Node
    99  	b, err := json.Marshal(struct {
   100  		// Add these attributes
   101  		Path        string `json:"path,omitempty"`
   102  		Permissions string `json:"permissions,omitempty"`
   103  
   104  		*findNode
   105  
   106  		// Make the following attributes disappear
   107  		Name               byte `json:"name,omitempty"`
   108  		Inode              byte `json:"inode,omitempty"`
   109  		ExtendedAttributes byte `json:"extended_attributes,omitempty"`
   110  		Device             byte `json:"device,omitempty"`
   111  		Content            byte `json:"content,omitempty"`
   112  		Subtree            byte `json:"subtree,omitempty"`
   113  	}{
   114  		Path:        filepath.Join(prefix, node.Name),
   115  		Permissions: node.Mode.String(),
   116  		findNode:    (*findNode)(node),
   117  	})
   118  	if err != nil {
   119  		Warnf("Marshall failed: %v\n", err)
   120  		return
   121  	}
   122  	if !s.inuse {
   123  		Printf("[")
   124  		s.inuse = true
   125  	}
   126  	if s.newsn != s.oldsn {
   127  		if s.oldsn != nil {
   128  			Printf("],\"hits\":%d,\"snapshot\":%q},", s.hits, s.oldsn.ID())
   129  		}
   130  		Printf(`{"matches":[`)
   131  		s.oldsn = s.newsn
   132  		s.hits = 0
   133  	}
   134  	if s.hits > 0 {
   135  		Printf(",")
   136  	}
   137  	Printf(string(b))
   138  	s.hits++
   139  }
   140  
   141  func (s *statefulOutput) PrintNormal(prefix string, node *restic.Node) {
   142  	if s.newsn != s.oldsn {
   143  		if s.oldsn != nil {
   144  			Verbosef("\n")
   145  		}
   146  		s.oldsn = s.newsn
   147  		Verbosef("Found matching entries in snapshot %s\n", s.oldsn.ID())
   148  	}
   149  	Printf(formatNode(prefix, node, s.ListLong) + "\n")
   150  }
   151  
   152  func (s *statefulOutput) Print(prefix string, node *restic.Node) {
   153  	if s.JSON {
   154  		s.PrintJSON(prefix, node)
   155  	} else {
   156  		s.PrintNormal(prefix, node)
   157  	}
   158  }
   159  
   160  func (s *statefulOutput) Finish() {
   161  	if s.JSON {
   162  		// do some finishing up
   163  		if s.oldsn != nil {
   164  			Printf("],\"hits\":%d,\"snapshot\":%q}", s.hits, s.oldsn.ID())
   165  		}
   166  		if s.inuse {
   167  			Printf("]\n")
   168  		} else {
   169  			Printf("[]\n")
   170  		}
   171  		return
   172  	}
   173  }
   174  
   175  // Finder bundles information needed to find a file or directory.
   176  type Finder struct {
   177  	repo     restic.Repository
   178  	pat      findPattern
   179  	out      statefulOutput
   180  	notfound restic.IDSet
   181  }
   182  
   183  func (f *Finder) findInTree(ctx context.Context, treeID restic.ID, prefix string) error {
   184  	if f.notfound.Has(treeID) {
   185  		debug.Log("%v skipping tree %v, has already been checked", prefix, treeID)
   186  		return nil
   187  	}
   188  
   189  	debug.Log("%v checking tree %v\n", prefix, treeID)
   190  
   191  	tree, err := f.repo.LoadTree(ctx, treeID)
   192  	if err != nil {
   193  		return err
   194  	}
   195  
   196  	var found bool
   197  	for _, node := range tree.Nodes {
   198  		debug.Log("  testing entry %q\n", node.Name)
   199  
   200  		name := node.Name
   201  		if f.pat.ignoreCase {
   202  			name = strings.ToLower(name)
   203  		}
   204  
   205  		m, err := filepath.Match(f.pat.pattern, name)
   206  		if err != nil {
   207  			return err
   208  		}
   209  
   210  		if m {
   211  			if !f.pat.oldest.IsZero() && node.ModTime.Before(f.pat.oldest) {
   212  				debug.Log("    ModTime is older than %s\n", f.pat.oldest)
   213  				continue
   214  			}
   215  
   216  			if !f.pat.newest.IsZero() && node.ModTime.After(f.pat.newest) {
   217  				debug.Log("    ModTime is newer than %s\n", f.pat.newest)
   218  				continue
   219  			}
   220  
   221  			debug.Log("    found match\n")
   222  			found = true
   223  			f.out.Print(prefix, node)
   224  		}
   225  
   226  		if node.Type == "dir" {
   227  			if err := f.findInTree(ctx, *node.Subtree, filepath.Join(prefix, node.Name)); err != nil {
   228  				return err
   229  			}
   230  		}
   231  	}
   232  
   233  	if !found {
   234  		f.notfound.Insert(treeID)
   235  	}
   236  
   237  	return nil
   238  }
   239  
   240  func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error {
   241  	debug.Log("searching in snapshot %s\n  for entries within [%s %s]", sn.ID(), f.pat.oldest, f.pat.newest)
   242  
   243  	f.out.newsn = sn
   244  	return f.findInTree(ctx, *sn.Tree, string(filepath.Separator))
   245  }
   246  
   247  func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
   248  	if len(args) != 1 {
   249  		return errors.Fatal("wrong number of arguments")
   250  	}
   251  
   252  	var err error
   253  	pat := findPattern{pattern: args[0]}
   254  	if opts.CaseInsensitive {
   255  		pat.pattern = strings.ToLower(pat.pattern)
   256  		pat.ignoreCase = true
   257  	}
   258  
   259  	if opts.Oldest != "" {
   260  		if pat.oldest, err = parseTime(opts.Oldest); err != nil {
   261  			return err
   262  		}
   263  	}
   264  
   265  	if opts.Newest != "" {
   266  		if pat.newest, err = parseTime(opts.Newest); err != nil {
   267  			return err
   268  		}
   269  	}
   270  
   271  	repo, err := OpenRepository(gopts)
   272  	if err != nil {
   273  		return err
   274  	}
   275  
   276  	if !gopts.NoLock {
   277  		lock, err := lockRepo(repo)
   278  		defer unlockRepo(lock)
   279  		if err != nil {
   280  			return err
   281  		}
   282  	}
   283  
   284  	if err = repo.LoadIndex(gopts.ctx); err != nil {
   285  		return err
   286  	}
   287  
   288  	ctx, cancel := context.WithCancel(gopts.ctx)
   289  	defer cancel()
   290  
   291  	f := &Finder{
   292  		repo:     repo,
   293  		pat:      pat,
   294  		out:      statefulOutput{ListLong: opts.ListLong, JSON: globalOptions.JSON},
   295  		notfound: restic.NewIDSet(),
   296  	}
   297  	for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, opts.Snapshots) {
   298  		if err = f.findInSnapshot(ctx, sn); err != nil {
   299  			return err
   300  		}
   301  	}
   302  	f.out.Finish()
   303  
   304  	return nil
   305  }