github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/head-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 "bufio" 22 "compress/bzip2" 23 "compress/gzip" 24 "context" 25 "io" 26 "os" 27 "strings" 28 "syscall" 29 "time" 30 31 "github.com/minio/cli" 32 "github.com/minio/mc/pkg/probe" 33 ) 34 35 var headFlags = []cli.Flag{ 36 cli.Int64Flag{ 37 Name: "n,lines", 38 Usage: "print the first 'n' lines", 39 Value: 10, 40 }, 41 cli.StringFlag{ 42 Name: "rewind", 43 Usage: "select an object version at specified time", 44 }, 45 cli.StringFlag{ 46 Name: "version-id, vid", 47 Usage: "select an object version to display", 48 }, 49 cli.BoolFlag{ 50 Name: "zip", 51 Usage: "extract from remote zip file (MinIO server source only)", 52 }, 53 } 54 55 // Display contents of a file. 56 var headCmd = cli.Command{ 57 Name: "head", 58 Usage: "display first 'n' lines of an object", 59 Action: mainHead, 60 OnUsageError: onUsageError, 61 Before: setGlobalsFromContext, 62 Flags: append(append(headFlags, encCFlag), globalFlags...), 63 CustomHelpTemplate: `NAME: 64 {{.HelpName}} - {{.Usage}} 65 66 USAGE: 67 {{.HelpName}} [FLAGS] TARGET [TARGET...] 68 69 FLAGS: 70 {{range .VisibleFlags}}{{.}} 71 {{end}} 72 73 NOTE: 74 '{{.HelpName}}' automatically decompresses 'gzip', 'bzip2' compressed objects. 75 76 EXAMPLES: 77 1. Display only first line from a 'gzip' compressed object on Amazon S3. 78 {{.Prompt}} {{.HelpName}} -n 1 s3/csv-data/population.csv.gz 79 80 2. Display only first line from server encrypted object on Amazon S3. 81 {{.Prompt}} {{.HelpName}} -n 1 --enc-c 's3/csv-data=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA' s3/csv-data/population.csv 82 83 3. Display only first line from server encrypted object on Amazon S3. 84 {{.Prompt}} {{.HelpName}} --enc-c "s3/json-data=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" s3/json-data/population.json 85 86 4. Display the first lines of a specific object version. 87 {{.Prompt}} {{.HelpName}} --version-id "3ddac055-89a7-40fa-8cd3-530a5581b6b8" s3/json-data/population.json 88 `, 89 } 90 91 // headURL displays contents of a URL to stdout. 92 func headURL(sourceURL, sourceVersion string, timeRef time.Time, encKeyDB map[string][]prefixSSEPair, nlines int64, zip bool) *probe.Error { 93 var reader io.ReadCloser 94 switch sourceURL { 95 case "-": 96 reader = os.Stdin 97 default: 98 var err *probe.Error 99 var content *ClientContent 100 if reader, content, err = getSourceStreamMetadataFromURL(context.Background(), sourceURL, sourceVersion, timeRef, encKeyDB, zip); err != nil { 101 return err.Trace(sourceURL) 102 } 103 104 ctype := content.Metadata["Content-Type"] 105 if strings.Contains(ctype, "gzip") { 106 var e error 107 reader, e = gzip.NewReader(reader) 108 if e != nil { 109 return probe.NewError(e) 110 } 111 defer reader.Close() 112 } else if strings.Contains(ctype, "bzip") { 113 defer reader.Close() 114 reader = io.NopCloser(bzip2.NewReader(reader)) 115 } else { 116 defer reader.Close() 117 } 118 } 119 return headOut(reader, nlines).Trace(sourceURL) 120 } 121 122 // headOut reads from reader stream and writes to stdout. Also check the length of the 123 // read bytes against size parameter (if not -1) and return the appropriate error 124 func headOut(r io.Reader, nlines int64) *probe.Error { 125 var stdout io.Writer 126 127 // In case of a user showing the object content in a terminal, 128 // avoid printing control and other bad characters to avoid 129 // terminal session corruption 130 if isTerminal() { 131 stdout = newPrettyStdout(os.Stdout) 132 } else { 133 stdout = os.Stdout 134 } 135 136 // Initialize a new scanner. 137 br := bufio.NewReader(r) 138 139 // Negative number of lines means default number of lines. 140 if nlines < 0 { 141 nlines = 10 142 } 143 144 for nlines > 0 { 145 line, _, e := br.ReadLine() 146 if e != nil { 147 return probe.NewError(e) 148 } 149 if _, e := stdout.Write(line); e != nil { 150 switch e := e.(type) { 151 case *os.PathError: 152 if e.Err == syscall.EPIPE { 153 // stdout closed by the user. Gracefully exit. 154 return nil 155 } 156 return probe.NewError(e) 157 default: 158 return probe.NewError(e) 159 } 160 } 161 stdout.Write([]byte("\n")) 162 nlines-- 163 } 164 return nil 165 } 166 167 // parseHeadSyntax performs command-line input validation for head command. 168 func parseHeadSyntax(ctx *cli.Context) (args []string, versionID string, timeRef time.Time) { 169 args = ctx.Args() 170 171 versionID = ctx.String("version-id") 172 rewind := ctx.String("rewind") 173 174 if versionID != "" && rewind != "" { 175 fatalIf(errInvalidArgument().Trace(), "You cannot specify --version-id and --rewind at the same time") 176 } 177 178 if versionID != "" && len(args) != 1 { 179 fatalIf(errInvalidArgument().Trace(), "You need to pass at least one argument if --version-id is specified") 180 } 181 182 timeRef = parseRewindFlag(rewind) 183 return 184 } 185 186 // mainHead is the main entry point for head command. 187 func mainHead(ctx *cli.Context) error { 188 // Parse encryption keys per command. 189 encryptionKeys, err := validateAndCreateEncryptionKeys(ctx) 190 fatalIf(err, "Unable to parse encryption keys.") 191 192 args, versionID, timeRef := parseHeadSyntax(ctx) 193 194 stdinMode := len(args) == 0 195 196 // handle std input data. 197 if stdinMode { 198 fatalIf(headOut(os.Stdin, ctx.Int64("lines")).Trace(), "Unable to read from standard input.") 199 return nil 200 } 201 202 // Convert arguments to URLs: expand alias, fix format. 203 for _, url := range ctx.Args() { 204 err = headURL( 205 url, 206 versionID, 207 timeRef, 208 encryptionKeys, 209 ctx.Int64("lines"), 210 ctx.Bool("zip"), 211 ) 212 fatalIf(err.Trace(url), "Unable to read from `"+url+"`.") 213 } 214 215 return nil 216 }