github.com/advanderveer/restic@v0.8.1-0.20171209104529-42a8c19aaea6/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.Str()) 186 return nil 187 } 188 189 debug.Log("%v checking tree %v\n", prefix, treeID.Str()) 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 if err := f.findInTree(ctx, *sn.Tree, string(filepath.Separator)); err != nil { 245 return err 246 } 247 return nil 248 } 249 250 func runFind(opts FindOptions, gopts GlobalOptions, args []string) error { 251 if len(args) != 1 { 252 return errors.Fatal("wrong number of arguments") 253 } 254 255 var err error 256 pat := findPattern{pattern: args[0]} 257 if opts.CaseInsensitive { 258 pat.pattern = strings.ToLower(pat.pattern) 259 pat.ignoreCase = true 260 } 261 262 if opts.Oldest != "" { 263 if pat.oldest, err = parseTime(opts.Oldest); err != nil { 264 return err 265 } 266 } 267 268 if opts.Newest != "" { 269 if pat.newest, err = parseTime(opts.Newest); err != nil { 270 return err 271 } 272 } 273 274 repo, err := OpenRepository(gopts) 275 if err != nil { 276 return err 277 } 278 279 if !gopts.NoLock { 280 lock, err := lockRepo(repo) 281 defer unlockRepo(lock) 282 if err != nil { 283 return err 284 } 285 } 286 287 if err = repo.LoadIndex(gopts.ctx); err != nil { 288 return err 289 } 290 291 ctx, cancel := context.WithCancel(gopts.ctx) 292 defer cancel() 293 294 f := &Finder{ 295 repo: repo, 296 pat: pat, 297 out: statefulOutput{ListLong: opts.ListLong, JSON: globalOptions.JSON}, 298 notfound: restic.NewIDSet(), 299 } 300 for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, opts.Snapshots) { 301 if err = f.findInSnapshot(ctx, sn); err != nil { 302 return err 303 } 304 } 305 f.out.Finish() 306 307 return nil 308 }