// 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" "path/filepath" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/pkg/v3/console" ) // stat specific flags. var ( statFlags = []cli.Flag{ cli.StringFlag{ Name: "rewind", Usage: "stat on older version(s)", }, cli.BoolFlag{ Name: "versions", Usage: "stat all versions", }, cli.StringFlag{ Name: "version-id, vid", Usage: "stat a specific object version", }, cli.BoolFlag{ Name: "recursive, r", Usage: "stat all objects recursively", }, cli.BoolFlag{ Name: "verbose, v", Usage: "show extended bucket(s) stat", }, cli.BoolFlag{ Name: "no-list", Usage: "disable all LIST operations for stat", }, } ) // show object metadata var statCmd = cli.Command{ Name: "stat", Usage: "show object metadata", Action: mainStat, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(append(statFlags, encCFlag), globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET [TARGET ...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Stat all contents of mybucket on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} s3/mybucket/ 2. Stat all contents of all buckets on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} s3 --verbose 3. Stat all contents of mybucket on Amazon S3 cloud storage on Microsoft Windows. {{.Prompt}} {{.HelpName}} s3\mybucket\ 4. Stat files recursively on a local filesystem on Microsoft Windows. {{.Prompt}} {{.HelpName}} --recursive C:\Users\mydocuments\ 5. Stat encrypted files on Amazon S3 cloud storage. In case the encryption key contains non-printable character like tab, pass the base64 encoded string as key. {{.Prompt}} {{.HelpName}} --enc-c "s3/personal-document/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" s3/personal-document/2019-account_report.docx 6. Stat a specific object version. {{.Prompt}} {{.HelpName}} --version-id "CL3sWgdSN2pNntSf6UnZAuh2kcu8E8si" s3/personal-docs/2018-account_report.docx 7. Stat all objects versions recursively created before 1st January 2020. {{.Prompt}} {{.HelpName}} --versions --rewind 2020.01.01T00:00 s3/personal-docs/ `, } // parseAndCheckStatSyntax - parse and validate all the passed arguments func parseAndCheckStatSyntax(ctx context.Context, cliCtx *cli.Context) ([]string, bool, string, time.Time, bool) { if !cliCtx.Args().Present() { showCommandHelpAndExit(cliCtx, 1) // last argument is exit code } args := cliCtx.Args() for _, arg := range args { if strings.TrimSpace(arg) == "" { fatalIf(errInvalidArgument().Trace(args...), "Unable to validate empty argument.") } } recursive := cliCtx.Bool("recursive") versionID := cliCtx.String("version-id") withVersions := cliCtx.Bool("versions") headOnly := cliCtx.Bool("no-list") rewind := parseRewindFlag(cliCtx.String("rewind")) // extract URLs. URLs := cliCtx.Args() if versionID != "" && len(args) > 1 { fatalIf(errInvalidArgument().Trace(args...), "You cannot specify --version-id with multiple arguments.") } if versionID != "" && (recursive || withVersions || !rewind.IsZero()) { fatalIf(errInvalidArgument().Trace(args...), "You cannot specify --version-id with either --rewind, --versions or --recursive.") } if (recursive || withVersions) && headOnly { fatalIf(errInvalidArgument().Trace(args...), "You cannot specify --no-list with either --versions or --recursive.") } var targetUrls []string for _, url := range URLs { _, path := url2Alias(url) if path != "" || !cliCtx.Bool("verbose") { targetUrls = append(targetUrls, url) continue } clnt, err := newClient(url) fatalIf(err.Trace(args...), "Unable to initialize `"+url+"`.") buckets, e := clnt.ListBuckets(ctx) if e != nil || len(buckets) == 0 { targetUrls = append(targetUrls, url) continue } for _, bucket := range buckets { targetUrls = append(targetUrls, filepath.Join(url, bucket.BucketName)) } } return targetUrls, recursive, versionID, rewind, withVersions } // mainStat - is a handler for mc stat command func mainStat(cliCtx *cli.Context) error { ctx, cancelStat := context.WithCancel(globalContext) defer cancelStat() // Additional command specific theme customization. console.SetColor("Name", color.New(color.Bold, color.FgCyan)) console.SetColor("Date", color.New(color.FgWhite)) console.SetColor("Size", color.New(color.FgWhite)) console.SetColor("ETag", color.New(color.FgWhite)) console.SetColor("Metadata", color.New(color.FgWhite)) // theme specific to stat bucket console.SetColor("Key", color.New(color.FgCyan)) console.SetColor("Value", color.New(color.FgYellow)) console.SetColor("Unset", color.New(color.FgRed)) console.SetColor("Set", color.New(color.FgGreen)) console.SetColor("Title", color.New(color.Bold, color.FgBlue)) console.SetColor("Count", color.New(color.FgGreen)) // Parse encryption keys per command. encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx) fatalIf(err, "Unable to parse encryption keys.") // check 'stat' cli arguments. args, isRecursive, versionID, rewind, withVersions := parseAndCheckStatSyntax(ctx, cliCtx) // mimic operating system tool behavior. if len(args) == 0 { args = []string{"."} } headOnly := cliCtx.Bool("no-list") for _, targetURL := range args { fatalIf(statURL(ctx, targetURL, versionID, rewind, withVersions, false, isRecursive, headOnly, encKeyDB), "Unable to stat `"+targetURL+"`.") } return nil }