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  }