github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/cat-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 "bytes" 22 "context" 23 "errors" 24 "fmt" 25 "io" 26 "os" 27 "strings" 28 "syscall" 29 "time" 30 "unicode" 31 "unicode/utf8" 32 33 "github.com/minio/cli" 34 "github.com/minio/mc/pkg/probe" 35 ) 36 37 var catFlags = []cli.Flag{ 38 cli.StringFlag{ 39 Name: "rewind", 40 Usage: "display an earlier object version", 41 }, 42 cli.StringFlag{ 43 Name: "version-id, vid", 44 Usage: "display a specific version of an object", 45 }, 46 cli.BoolFlag{ 47 Name: "zip", 48 Usage: "extract from remote zip file (MinIO server source only)", 49 }, 50 cli.Int64Flag{ 51 Name: "offset", 52 Usage: "start offset", 53 }, 54 cli.Int64Flag{ 55 Name: "tail", 56 Usage: "tail number of bytes at ending of file", 57 }, 58 } 59 60 // Display contents of a file. 61 var catCmd = cli.Command{ 62 Name: "cat", 63 Usage: "display object contents", 64 Action: mainCat, 65 OnUsageError: onUsageError, 66 Before: setGlobalsFromContext, 67 Flags: append(append(catFlags, encCFlag), globalFlags...), 68 CustomHelpTemplate: `NAME: 69 {{.HelpName}} - {{.Usage}} 70 71 USAGE: 72 {{.HelpName}} [FLAGS] TARGET [TARGET...] 73 74 FLAGS: 75 {{range .VisibleFlags}}{{.}} 76 {{end}} 77 78 EXAMPLES: 79 1. Stream an object from Amazon S3 cloud storage to mplayer standard input. 80 {{.Prompt}} {{.HelpName}} s3/mysql-backups/kubecon-mysql-operator.mpv | mplayer - 81 82 2. Concatenate contents of file1.txt and stdin to standard output. 83 {{.Prompt}} {{.HelpName}} file1.txt - > file.txt 84 85 3. Concatenate multiple files to one. 86 {{.Prompt}} {{.HelpName}} part.* > complete.img 87 88 4. Save an encrypted object from Amazon S3 cloud storage to a local file. 89 {{.Prompt}} {{.HelpName}} --enc-c "play/my-bucket/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" s3/mysql-backups/backups-201810.gz > /mnt/data/recent.gz 90 91 5. Display the content of encrypted object. In case the encryption key contains non-printable character like tab, pass the 92 base64 encoded string as key. 93 {{.Prompt}} {{.HelpName}} --enc-c "play/my-bucket/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" play/my-bucket/my-object 94 95 6. Display the content of an object 10 days earlier 96 {{.Prompt}} {{.HelpName}} --rewind 10d play/my-bucket/my-object 97 98 7. Display the content of a particular object version 99 {{.Prompt}} {{.HelpName}} --vid "3ddac055-89a7-40fa-8cd3-530a5581b6b8" play/my-bucket/my-object 100 `, 101 } 102 103 // checkCatSyntax - validate all the passed arguments 104 func checkCatSyntax(ctx *cli.Context) { 105 if len(ctx.Args()) == 0 { 106 showCommandHelpAndExit(ctx, 1) // last argument is exit code 107 } 108 } 109 110 // prettyStdout replaces some non printable characters 111 // with <hex> format to be better viewable by the user 112 type prettyStdout struct { 113 // Internal data to pretty-print 114 writer io.Writer 115 // Internal buffer which contains pretty printed 116 // form of binary (no printable) characters 117 buffer *bytes.Buffer 118 } 119 120 // newPrettyStdout returns an initialized prettyStdout struct 121 func newPrettyStdout(w io.Writer) *prettyStdout { 122 return &prettyStdout{ 123 writer: w, 124 buffer: bytes.NewBuffer([]byte{}), 125 } 126 } 127 128 // Read() returns pretty printed binary characters 129 func (s prettyStdout) Write(input []byte) (int, error) { 130 inputLen := len(input) 131 132 // Convert no printable characters to '^?' 133 // and fill into s.buffer 134 for len(input) > 0 { 135 r, size := utf8.DecodeRune(input) 136 if unicode.IsPrint(r) || unicode.IsSpace(r) { 137 s.buffer.WriteRune(r) 138 } else { 139 s.buffer.WriteString("^?") 140 } 141 input = input[size:] 142 } 143 144 bufLen := s.buffer.Len() 145 146 // Copy all buffer content to the writer (stdout) 147 n, e := s.buffer.WriteTo(s.writer) 148 if e != nil { 149 return 0, e 150 } 151 152 if int(n) != bufLen { 153 return 0, errors.New("error when writing to stdout") 154 } 155 156 return inputLen, nil 157 } 158 159 type catOpts struct { 160 args []string 161 versionID string 162 timeRef time.Time 163 startO int64 164 tailO int64 165 isZip bool 166 stdinMode bool 167 } 168 169 // parseCatSyntax performs command-line input validation for cat command. 170 func parseCatSyntax(ctx *cli.Context) catOpts { 171 // Validate command-line arguments. 172 checkCatSyntax(ctx) 173 174 var o catOpts 175 o.args = ctx.Args() 176 177 o.versionID = ctx.String("version-id") 178 rewind := ctx.String("rewind") 179 180 if o.versionID != "" && rewind != "" { 181 fatalIf(errInvalidArgument().Trace(), "You cannot specify --version-id and --rewind at the same time") 182 } 183 184 if o.versionID != "" && len(o.args) != 1 { 185 fatalIf(errInvalidArgument().Trace(), "You need to pass at least one argument if --version-id is specified") 186 } 187 188 for _, arg := range o.args { 189 if strings.HasPrefix(arg, "-") && len(arg) > 1 { 190 fatalIf(probe.NewError(errors.New("")), fmt.Sprintf("Unknown flag `%s` passed.", arg)) 191 } 192 } 193 194 o.stdinMode = len(o.args) == 0 195 196 o.timeRef = parseRewindFlag(rewind) 197 o.isZip = ctx.Bool("zip") 198 o.startO = ctx.Int64("offset") 199 o.tailO = ctx.Int64("tail") 200 if o.tailO != 0 && o.startO != 0 { 201 fatalIf(errInvalidArgument().Trace(), "You cannot specify both --tail and --offset") 202 } 203 if o.tailO < 0 || o.startO < 0 { 204 fatalIf(errInvalidArgument().Trace(), "You cannot specify negative --tail or --offset") 205 } 206 if o.isZip && (o.tailO != 0 || o.startO != 0) { 207 fatalIf(errInvalidArgument().Trace(), "You cannot combine --zip with --tail or --offset") 208 } 209 if o.stdinMode && (o.isZip || o.startO != 0 || o.tailO != 0) { 210 fatalIf(errInvalidArgument().Trace(), "You cannot use --zip --tail or --offset with stdin") 211 } 212 213 return o 214 } 215 216 // catURL displays contents of a URL to stdout. 217 func catURL(ctx context.Context, sourceURL string, encKeyDB map[string][]prefixSSEPair, o catOpts) *probe.Error { 218 var reader io.ReadCloser 219 size := int64(-1) 220 switch sourceURL { 221 case "-": 222 reader = os.Stdin 223 default: 224 versionID := o.versionID 225 var err *probe.Error 226 // Try to stat the object, the purpose is to: 227 // 1. extract the size of S3 object so we can check if the size of the 228 // downloaded object is equal to the original one. FS files 229 // are ignored since some of them have zero size though they 230 // have contents like files under /proc. 231 // 2. extract the version ID if rewind flag is passed 232 if client, content, err := url2Stat(ctx, url2StatOptions{ 233 urlStr: sourceURL, 234 versionID: o.versionID, 235 fileAttr: false, 236 encKeyDB: encKeyDB, 237 timeRef: o.timeRef, 238 isZip: o.isZip, 239 ignoreBucketExistsCheck: false, 240 }); err == nil { 241 if o.versionID == "" { 242 versionID = content.VersionID 243 } 244 if o.tailO > 0 && content.Size > 0 { 245 o.startO = content.Size - o.tailO 246 if o.startO < 0 { 247 // Return all. 248 o.startO = 0 249 } 250 } 251 252 if client.GetURL().Type == objectStorage { 253 size = content.Size - o.startO 254 if size < 0 { 255 err := probe.NewError(fmt.Errorf("specified offset (%d) bigger than file (%d)", o.startO, content.Size)) 256 return err.Trace(sourceURL) 257 } 258 } 259 } else { 260 return err.Trace(sourceURL) 261 } 262 gopts := GetOptions{VersionID: versionID, Zip: o.isZip, RangeStart: o.startO} 263 if reader, err = getSourceStreamFromURL(ctx, sourceURL, encKeyDB, getSourceOpts{ 264 GetOptions: gopts, 265 preserve: false, 266 }); err != nil { 267 return err.Trace(sourceURL) 268 } 269 defer reader.Close() 270 } 271 return catOut(reader, size).Trace(sourceURL) 272 } 273 274 // catOut reads from reader stream and writes to stdout. Also check the length of the 275 // read bytes against size parameter (if not -1) and return the appropriate error 276 func catOut(r io.Reader, size int64) *probe.Error { 277 var n int64 278 var e error 279 280 var stdout io.Writer 281 282 // In case of a user showing the object content in a terminal, 283 // avoid printing control and other bad characters to avoid 284 // terminal session corruption 285 if isTerminal() { 286 stdout = newPrettyStdout(os.Stdout) 287 } else { 288 stdout = os.Stdout 289 } 290 291 // Read till EOF. 292 if n, e = io.Copy(stdout, r); e != nil { 293 switch e := e.(type) { 294 case *os.PathError: 295 if e.Err == syscall.EPIPE { 296 // stdout closed by the user. Gracefully exit. 297 return nil 298 } 299 return probe.NewError(e) 300 default: 301 return probe.NewError(e) 302 } 303 } 304 if size != -1 && n < size { 305 return probe.NewError(UnexpectedEOF{ 306 TotalSize: size, 307 TotalWritten: n, 308 }) 309 } 310 if size != -1 && n > size { 311 return probe.NewError(UnexpectedEOF{ 312 TotalSize: size, 313 TotalWritten: n, 314 }) 315 } 316 return nil 317 } 318 319 // mainCat is the main entry point for cat command. 320 func mainCat(cliCtx *cli.Context) error { 321 ctx, cancelCat := context.WithCancel(globalContext) 322 defer cancelCat() 323 324 encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx) 325 fatalIf(err, "Unable to parse encryption keys.") 326 327 // check 'cat' cli arguments. 328 o := parseCatSyntax(cliCtx) 329 330 // handle std input data. 331 if o.stdinMode { 332 fatalIf(catOut(os.Stdin, -1).Trace(), "Unable to read from standard input.") 333 return nil 334 } 335 336 // if Args contain `-`, we need to preserve its order specially. 337 if len(o.args) > 0 && o.args[0] == "-" { 338 for i, arg := range os.Args { 339 if arg == "cat" { 340 // Overwrite cliCtx.Args with os.Args. 341 o.args = os.Args[i+1:] 342 break 343 } 344 } 345 } 346 347 // Convert arguments to URLs: expand alias, fix format. 348 for _, url := range o.args { 349 fatalIf(catURL(ctx, url, encKeyDB, o).Trace(url), "Unable to read from `"+url+"`.") 350 } 351 352 return nil 353 }