// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "regexp" "strings" "time" "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v2/console" ) // List of all flags supported by find command. var ( findFlags = []cli.Flag{ cli.StringFlag{ Name: "exec", Usage: "spawn an external process for each matching object (see FORMAT)", }, cli.StringFlag{ Name: "ignore", Usage: "exclude objects matching the wildcard pattern", }, cli.BoolFlag{ Name: "versions", Usage: "include all objects versions", }, cli.StringFlag{ Name: "name", Usage: "find object names matching wildcard pattern", }, cli.StringFlag{ Name: "newer-than", Usage: "match all objects newer than value in duration string (e.g. 7d10h31s)", }, cli.StringFlag{ Name: "older-than", Usage: "match all objects older than value in duration string (e.g. 7d10h31s)", }, cli.StringFlag{ Name: "path", Usage: "match directory names matching wildcard pattern", }, cli.StringFlag{ Name: "print", Usage: "print in custom format to STDOUT (see FORMAT)", }, cli.StringFlag{ Name: "regex", Usage: "match directory and object name with RE2 regex pattern", }, cli.StringFlag{ Name: "larger", Usage: "match all objects larger than specified size in units (see UNITS)", }, cli.StringFlag{ Name: "smaller", Usage: "match all objects smaller than specified size in units (see UNITS)", }, cli.UintFlag{ Name: "maxdepth", Usage: "limit directory navigation to specified depth", }, cli.BoolFlag{ Name: "watch", Usage: "monitor a specified path for newly created object(s)", }, cli.StringSliceFlag{ Name: "metadata", Usage: "match metadata with RE2 regex pattern. Specify each with key=regex. MinIO server only.", }, cli.StringSliceFlag{ Name: "tags", Usage: "match tags with RE2 regex pattern. Specify each with key=regex. MinIO server only.", }, } ) var findCmd = cli.Command{ Name: "find", Usage: "search for objects", Action: mainFind, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(findFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} UNITS --smaller, --larger flags accept human-readable case-insensitive number suffixes such as "k", "m", "g" and "t" referring to the metric units KB, MB, GB and TB respectively. Adding an "i" to these prefixes, uses the IEC units, so that "gi" refers to "gibibyte" or "GiB". A "b" at the end is also accepted. Without suffixes the unit is bytes. --older-than, --newer-than flags accept the string for days, hours and minutes i.e. 1d2h30m states 1 day, 2 hours and 30 minutes. FORMAT Support string substitutions with special interpretations for following keywords. Keywords supported if target is filesystem or object storage: {} --> Substitutes to full path. {base} --> Substitutes to basename of path. {dir} --> Substitutes to dirname of the path. {size} --> Substitutes to object size of the path. {time} --> Substitutes to object modified time of the path. {version} --> Substitutes to object version identifier. Keywords supported if target is object storage: {url} --> Substitutes to a shareable URL of the path. EXAMPLES: 01. Find all "foo.jpg" in all buckets under "s3" account. {{.Prompt}} {{.HelpName}} s3 --name "foo.jpg" 02. Find all objects with ".txt" extension under "s3/mybucket". {{.Prompt}} {{.HelpName}} s3/mybucket --name "*.txt" 03. Find only the object names without the directory component under "s3/mybucket". {{.Prompt}} {{.HelpName}} s3/mybucket --name "*" -print {base} 04. Find all images with ".jpg" extension under "s3/photos", prefixed with "album". {{.Prompt}} {{.HelpName}} s3/photos --name "*.jpg" --path "*/album*/*" 05. Find all images with ".jpg", ".png", and ".gif" extensions, using regex under "s3/photos". {{.Prompt}} {{.HelpName}} s3/photos --regex "(?i)\.(jpg|png|gif)$" 06. Find all images with ".jpg" extension under "s3/bucket" and copy to "play/bucket" *continuously*. {{.Prompt}} {{.HelpName}} s3/bucket --name "*.jpg" --watch --exec "mc cp {} play/bucket" 07. Find and generate public URLs valid for 7 days, for all objects between 64 MB, and 1 GB in size under "s3" account. {{.Prompt}} {{.HelpName}} s3 --larger 64MB --smaller 1GB --print {url} 08. Find all objects created in the last week under "s3/bucket". {{.Prompt}} {{.HelpName}} s3/bucket --newer-than 7d 09. Find all objects which were created are older than 2 days, 5 hours and 10 minutes and exclude the ones with ".jpg" extension under "s3". {{.Prompt}} {{.HelpName}} s3 --older-than 2d5h10m --ignore "*.jpg" 10. List all objects up to 3 levels sub-directory deep under "s3/bucket". {{.Prompt}} {{.HelpName}} s3/bucket --maxdepth 3 11. Copy all versions of all objects in bucket in the local machine {{.Prompt}} {{.HelpName}} s3/bucket --versions --exec "mc cp --version-id {version} {} /tmp/dir/{}.{version}" `, } // checkFindSyntax - validate the passed arguments func checkFindSyntax(ctx context.Context, cliCtx *cli.Context, encKeyDB map[string][]prefixSSEPair) { args := cliCtx.Args() if !args.Present() { args = []string{"./"} // No args just default to present directory. } else if args.Get(0) == "." { args[0] = "./" // If the arg is '.' treat it as './'. } for _, arg := range args { if strings.TrimSpace(arg) == "" { fatalIf(errInvalidArgument().Trace(args...), "Unable to validate empty argument.") } } // Extract input URLs and validate. for _, url := range args { _, _, err := url2Stat(ctx, url2StatOptions{urlStr: url, versionID: "", fileAttr: false, encKeyDB: encKeyDB, timeRef: time.Time{}, isZip: false, ignoreBucketExistsCheck: false}) if err != nil { // Bucket name empty is a valid error for 'find myminio' unless we are using watch, treat it as such. if _, ok := err.ToGoError().(BucketNameEmpty); ok && !cliCtx.Bool("watch") { continue } fatalIf(err.Trace(url), "Unable to stat `"+url+"`.") } } } // Find context is container to hold all parsed input arguments, // each parsed input is stored in its native typed form for // ease of repurposing. type findContext struct { *cli.Context execCmd string ignorePattern string namePattern string pathPattern string regexPattern *regexp.Regexp maxDepth uint printFmt string olderThan string newerThan string largerSize uint64 smallerSize uint64 watch bool withOlderVersions bool matchMeta map[string]*regexp.Regexp matchTags map[string]*regexp.Regexp // Internal values targetAlias string targetURL string targetFullURL string clnt Client } // mainFind - handler for mc find commands func mainFind(cliCtx *cli.Context) error { ctx, cancelFind := context.WithCancel(globalContext) defer cancelFind() // Additional command specific theme customization. console.SetColor("Find", color.New(color.FgGreen, color.Bold)) console.SetColor("FindExecErr", color.New(color.FgRed, color.Italic, color.Bold)) // Parse encryption keys per command. encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx) fatalIf(err, "Unable to parse encryption keys.") checkFindSyntax(ctx, cliCtx, encKeyDB) args := cliCtx.Args() if !args.Present() { args = []string{"./"} // Not args present default to present directory. } else if args.Get(0) == "." { args[0] = "./" // If the arg is '.' treat it as './'. } clnt, err := newClient(args[0]) fatalIf(err.Trace(args...), "Unable to initialize `"+args[0]+"`.") var olderThan, newerThan string if cliCtx.String("older-than") != "" { olderThan = cliCtx.String("older-than") } if cliCtx.String("newer-than") != "" { newerThan = cliCtx.String("newer-than") } // Use 'e' to indicate Go error, this is a convention followed in `mc`. For probe.Error we call it // 'err' and regular Go error is called as 'e'. var e error var largerSize, smallerSize uint64 if cliCtx.String("larger") != "" { largerSize, e = humanize.ParseBytes(cliCtx.String("larger")) fatalIf(probe.NewError(e).Trace(cliCtx.String("larger")), "Unable to parse input bytes.") } if cliCtx.String("smaller") != "" { smallerSize, e = humanize.ParseBytes(cliCtx.String("smaller")) fatalIf(probe.NewError(e).Trace(cliCtx.String("smaller")), "Unable to parse input bytes.") } // Get --versions flag withVersions := cliCtx.Bool("versions") targetAlias, _, hostCfg, err := expandAlias(args[0]) fatalIf(err.Trace(args[0]), "Unable to expand alias.") var targetFullURL string if hostCfg != nil { targetFullURL = hostCfg.URL } var regMatch *regexp.Regexp if cliCtx.String("regex") != "" { regMatch = regexp.MustCompile(cliCtx.String("regex")) } return doFind(ctx, &findContext{ Context: cliCtx, maxDepth: cliCtx.Uint("maxdepth"), execCmd: cliCtx.String("exec"), printFmt: cliCtx.String("print"), namePattern: cliCtx.String("name"), pathPattern: cliCtx.String("path"), regexPattern: regMatch, ignorePattern: cliCtx.String("ignore"), withOlderVersions: withVersions, olderThan: olderThan, newerThan: newerThan, largerSize: largerSize, smallerSize: smallerSize, watch: cliCtx.Bool("watch"), targetAlias: targetAlias, targetURL: args[0], targetFullURL: targetFullURL, clnt: clnt, matchMeta: getRegexMap(cliCtx, "metadata"), matchTags: getRegexMap(cliCtx, "tags"), }) }