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 }