github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/find-main.go (about) 1 // Copyright (c) 2015-2022 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "context" 22 "regexp" 23 "strings" 24 "time" 25 26 "github.com/dustin/go-humanize" 27 "github.com/fatih/color" 28 "github.com/minio/cli" 29 "github.com/minio/mc/pkg/probe" 30 "github.com/minio/pkg/v2/console" 31 ) 32 33 // List of all flags supported by find command. 34 var ( 35 findFlags = []cli.Flag{ 36 cli.StringFlag{ 37 Name: "exec", 38 Usage: "spawn an external process for each matching object (see FORMAT)", 39 }, 40 cli.StringFlag{ 41 Name: "ignore", 42 Usage: "exclude objects matching the wildcard pattern", 43 }, 44 cli.BoolFlag{ 45 Name: "versions", 46 Usage: "include all objects versions", 47 }, 48 cli.StringFlag{ 49 Name: "name", 50 Usage: "find object names matching wildcard pattern", 51 }, 52 cli.StringFlag{ 53 Name: "newer-than", 54 Usage: "match all objects newer than value in duration string (e.g. 7d10h31s)", 55 }, 56 cli.StringFlag{ 57 Name: "older-than", 58 Usage: "match all objects older than value in duration string (e.g. 7d10h31s)", 59 }, 60 cli.StringFlag{ 61 Name: "path", 62 Usage: "match directory names matching wildcard pattern", 63 }, 64 cli.StringFlag{ 65 Name: "print", 66 Usage: "print in custom format to STDOUT (see FORMAT)", 67 }, 68 cli.StringFlag{ 69 Name: "regex", 70 Usage: "match directory and object name with RE2 regex pattern", 71 }, 72 cli.StringFlag{ 73 Name: "larger", 74 Usage: "match all objects larger than specified size in units (see UNITS)", 75 }, 76 cli.StringFlag{ 77 Name: "smaller", 78 Usage: "match all objects smaller than specified size in units (see UNITS)", 79 }, 80 cli.UintFlag{ 81 Name: "maxdepth", 82 Usage: "limit directory navigation to specified depth", 83 }, 84 cli.BoolFlag{ 85 Name: "watch", 86 Usage: "monitor a specified path for newly created object(s)", 87 }, 88 cli.StringSliceFlag{ 89 Name: "metadata", 90 Usage: "match metadata with RE2 regex pattern. Specify each with key=regex. MinIO server only.", 91 }, 92 cli.StringSliceFlag{ 93 Name: "tags", 94 Usage: "match tags with RE2 regex pattern. Specify each with key=regex. MinIO server only.", 95 }, 96 } 97 ) 98 99 var findCmd = cli.Command{ 100 Name: "find", 101 Usage: "search for objects", 102 Action: mainFind, 103 OnUsageError: onUsageError, 104 Before: setGlobalsFromContext, 105 Flags: append(findFlags, globalFlags...), 106 CustomHelpTemplate: `NAME: 107 {{.HelpName}} - {{.Usage}} 108 109 USAGE: 110 {{.HelpName}} [FLAGS] TARGET 111 112 FLAGS: 113 {{range .VisibleFlags}}{{.}} 114 {{end}} 115 UNITS 116 --smaller, --larger flags accept human-readable case-insensitive number 117 suffixes such as "k", "m", "g" and "t" referring to the metric units KB, 118 MB, GB and TB respectively. Adding an "i" to these prefixes, uses the IEC 119 units, so that "gi" refers to "gibibyte" or "GiB". A "b" at the end is 120 also accepted. Without suffixes the unit is bytes. 121 122 --older-than, --newer-than flags accept the string for days, hours and minutes 123 i.e. 1d2h30m states 1 day, 2 hours and 30 minutes. 124 125 FORMAT 126 Support string substitutions with special interpretations for following keywords. 127 Keywords supported if target is filesystem or object storage: 128 129 {} --> Substitutes to full path. 130 {base} --> Substitutes to basename of path. 131 {dir} --> Substitutes to dirname of the path. 132 {size} --> Substitutes to object size of the path. 133 {time} --> Substitutes to object modified time of the path. 134 {version} --> Substitutes to object version identifier. 135 136 Keywords supported if target is object storage: 137 138 {url} --> Substitutes to a shareable URL of the path. 139 140 EXAMPLES: 141 01. Find all "foo.jpg" in all buckets under "s3" account. 142 {{.Prompt}} {{.HelpName}} s3 --name "foo.jpg" 143 144 02. Find all objects with ".txt" extension under "s3/mybucket". 145 {{.Prompt}} {{.HelpName}} s3/mybucket --name "*.txt" 146 147 03. Find only the object names without the directory component under "s3/mybucket". 148 {{.Prompt}} {{.HelpName}} s3/mybucket --name "*" -print {base} 149 150 04. Find all images with ".jpg" extension under "s3/photos", prefixed with "album". 151 {{.Prompt}} {{.HelpName}} s3/photos --name "*.jpg" --path "*/album*/*" 152 153 05. Find all images with ".jpg", ".png", and ".gif" extensions, using regex under "s3/photos". 154 {{.Prompt}} {{.HelpName}} s3/photos --regex "(?i)\.(jpg|png|gif)$" 155 156 06. Find all images with ".jpg" extension under "s3/bucket" and copy to "play/bucket" *continuously*. 157 {{.Prompt}} {{.HelpName}} s3/bucket --name "*.jpg" --watch --exec "mc cp {} play/bucket" 158 159 07. Find and generate public URLs valid for 7 days, for all objects between 64 MB, and 1 GB in size under "s3" account. 160 {{.Prompt}} {{.HelpName}} s3 --larger 64MB --smaller 1GB --print {url} 161 162 08. Find all objects created in the last week under "s3/bucket". 163 {{.Prompt}} {{.HelpName}} s3/bucket --newer-than 7d 164 165 09. Find all objects which were created are older than 2 days, 5 hours and 10 minutes and exclude the ones with ".jpg" 166 extension under "s3". 167 {{.Prompt}} {{.HelpName}} s3 --older-than 2d5h10m --ignore "*.jpg" 168 169 10. List all objects up to 3 levels sub-directory deep under "s3/bucket". 170 {{.Prompt}} {{.HelpName}} s3/bucket --maxdepth 3 171 172 11. Copy all versions of all objects in bucket in the local machine 173 {{.Prompt}} {{.HelpName}} s3/bucket --versions --exec "mc cp --version-id {version} {} /tmp/dir/{}.{version}" 174 `, 175 } 176 177 // checkFindSyntax - validate the passed arguments 178 func checkFindSyntax(ctx context.Context, cliCtx *cli.Context, encKeyDB map[string][]prefixSSEPair) { 179 args := cliCtx.Args() 180 if !args.Present() { 181 args = []string{"./"} // No args just default to present directory. 182 } else if args.Get(0) == "." { 183 args[0] = "./" // If the arg is '.' treat it as './'. 184 } 185 186 for _, arg := range args { 187 if strings.TrimSpace(arg) == "" { 188 fatalIf(errInvalidArgument().Trace(args...), "Unable to validate empty argument.") 189 } 190 } 191 192 // Extract input URLs and validate. 193 for _, url := range args { 194 _, _, err := url2Stat(ctx, url2StatOptions{urlStr: url, versionID: "", fileAttr: false, encKeyDB: encKeyDB, timeRef: time.Time{}, isZip: false, ignoreBucketExistsCheck: false}) 195 if err != nil { 196 // Bucket name empty is a valid error for 'find myminio' unless we are using watch, treat it as such. 197 if _, ok := err.ToGoError().(BucketNameEmpty); ok && !cliCtx.Bool("watch") { 198 continue 199 } 200 fatalIf(err.Trace(url), "Unable to stat `"+url+"`.") 201 } 202 } 203 } 204 205 // Find context is container to hold all parsed input arguments, 206 // each parsed input is stored in its native typed form for 207 // ease of repurposing. 208 type findContext struct { 209 *cli.Context 210 execCmd string 211 ignorePattern string 212 namePattern string 213 pathPattern string 214 regexPattern *regexp.Regexp 215 maxDepth uint 216 printFmt string 217 olderThan string 218 newerThan string 219 largerSize uint64 220 smallerSize uint64 221 watch bool 222 withOlderVersions bool 223 matchMeta map[string]*regexp.Regexp 224 matchTags map[string]*regexp.Regexp 225 226 // Internal values 227 targetAlias string 228 targetURL string 229 targetFullURL string 230 clnt Client 231 } 232 233 // mainFind - handler for mc find commands 234 func mainFind(cliCtx *cli.Context) error { 235 ctx, cancelFind := context.WithCancel(globalContext) 236 defer cancelFind() 237 238 // Additional command specific theme customization. 239 console.SetColor("Find", color.New(color.FgGreen, color.Bold)) 240 console.SetColor("FindExecErr", color.New(color.FgRed, color.Italic, color.Bold)) 241 242 // Parse encryption keys per command. 243 encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx) 244 fatalIf(err, "Unable to parse encryption keys.") 245 246 checkFindSyntax(ctx, cliCtx, encKeyDB) 247 248 args := cliCtx.Args() 249 if !args.Present() { 250 args = []string{"./"} // Not args present default to present directory. 251 } else if args.Get(0) == "." { 252 args[0] = "./" // If the arg is '.' treat it as './'. 253 } 254 255 clnt, err := newClient(args[0]) 256 fatalIf(err.Trace(args...), "Unable to initialize `"+args[0]+"`.") 257 258 var olderThan, newerThan string 259 260 if cliCtx.String("older-than") != "" { 261 olderThan = cliCtx.String("older-than") 262 } 263 if cliCtx.String("newer-than") != "" { 264 newerThan = cliCtx.String("newer-than") 265 } 266 267 // Use 'e' to indicate Go error, this is a convention followed in `mc`. For probe.Error we call it 268 // 'err' and regular Go error is called as 'e'. 269 var e error 270 var largerSize, smallerSize uint64 271 272 if cliCtx.String("larger") != "" { 273 largerSize, e = humanize.ParseBytes(cliCtx.String("larger")) 274 fatalIf(probe.NewError(e).Trace(cliCtx.String("larger")), "Unable to parse input bytes.") 275 } 276 277 if cliCtx.String("smaller") != "" { 278 smallerSize, e = humanize.ParseBytes(cliCtx.String("smaller")) 279 fatalIf(probe.NewError(e).Trace(cliCtx.String("smaller")), "Unable to parse input bytes.") 280 } 281 282 // Get --versions flag 283 withVersions := cliCtx.Bool("versions") 284 285 targetAlias, _, hostCfg, err := expandAlias(args[0]) 286 fatalIf(err.Trace(args[0]), "Unable to expand alias.") 287 288 var targetFullURL string 289 if hostCfg != nil { 290 targetFullURL = hostCfg.URL 291 } 292 var regMatch *regexp.Regexp 293 if cliCtx.String("regex") != "" { 294 regMatch = regexp.MustCompile(cliCtx.String("regex")) 295 } 296 297 return doFind(ctx, &findContext{ 298 Context: cliCtx, 299 maxDepth: cliCtx.Uint("maxdepth"), 300 execCmd: cliCtx.String("exec"), 301 printFmt: cliCtx.String("print"), 302 namePattern: cliCtx.String("name"), 303 pathPattern: cliCtx.String("path"), 304 regexPattern: regMatch, 305 ignorePattern: cliCtx.String("ignore"), 306 withOlderVersions: withVersions, 307 olderThan: olderThan, 308 newerThan: newerThan, 309 largerSize: largerSize, 310 smallerSize: smallerSize, 311 watch: cliCtx.Bool("watch"), 312 targetAlias: targetAlias, 313 targetURL: args[0], 314 targetFullURL: targetFullURL, 315 clnt: clnt, 316 matchMeta: getRegexMap(cliCtx, "metadata"), 317 matchTags: getRegexMap(cliCtx, "tags"), 318 }) 319 }