github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/pipe-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 "io" 22 "os" 23 "runtime/debug" 24 "syscall" 25 26 "github.com/dustin/go-humanize" 27 "github.com/minio/cli" 28 "github.com/minio/mc/pkg/probe" 29 "github.com/minio/minio-go/v7" 30 ) 31 32 func defaultPartSize() string { 33 _, partSize, _, _ := minio.OptimalPartInfo(-1, 0) 34 return humanize.IBytes(uint64(partSize)) 35 } 36 37 var pipeFlags = []cli.Flag{ 38 cli.StringFlag{ 39 Name: "storage-class, sc", 40 Usage: "set storage class for new object(s) on target", 41 }, 42 cli.StringFlag{ 43 Name: "attr", 44 Usage: "add custom metadata for the object", 45 }, 46 cli.StringFlag{ 47 Name: "tags", 48 Usage: "apply one or more tags to the uploaded objects", 49 }, 50 cli.IntFlag{ 51 Name: "concurrent", 52 Value: 1, 53 Usage: "allow N concurrent uploads [WARNING: will use more memory use it with caution]", 54 }, 55 cli.StringFlag{ 56 Name: "part-size", 57 Value: defaultPartSize(), 58 Usage: "customize chunk size for each concurrent upload", 59 }, 60 cli.IntFlag{ 61 Name: "pipe-max-size", 62 Usage: "increase the pipe buffer size to a custom value", 63 Hidden: true, 64 }, 65 } 66 67 // Display contents of a file. 68 var pipeCmd = cli.Command{ 69 Name: "pipe", 70 Usage: "stream STDIN to an object", 71 Action: mainPipe, 72 OnUsageError: onUsageError, 73 Before: setGlobalsFromContext, 74 Flags: append(append(pipeFlags, encFlags...), globalFlags...), 75 CustomHelpTemplate: `NAME: 76 {{.HelpName}} - {{.Usage}} 77 78 USAGE: 79 {{.HelpName}} [FLAGS] [TARGET] 80 {{if .VisibleFlags}} 81 FLAGS: 82 {{range .VisibleFlags}}{{.}} 83 {{end}}{{end}} 84 85 ENVIRONMENT VARIABLES: 86 MC_ENC_KMS: KMS encryption key in the form of (alias/prefix=key). 87 MC_ENC_S3: S3 encryption key in the form of (alias/prefix=key). 88 89 EXAMPLES: 90 1. Write contents of stdin to a file on local filesystem. 91 {{.Prompt}} {{.HelpName}} /tmp/hello-world.go 92 93 2. Write contents of stdin to an object on Amazon S3 cloud storage. 94 {{.Prompt}} {{.HelpName}} s3/personalbuck/meeting-notes.txt 95 96 3. Copy an ISO image to an object on Amazon S3 cloud storage. 97 {{.Prompt}} cat debian-8.2.iso | {{.HelpName}} s3/opensource-isos/gnuos.iso 98 99 4. Copy an ISO image to an object on minio storage using KMS encryption. 100 {{.Prompt}} cat debian-8.2.iso | {{.HelpName}} --enc-kms="minio/opensource-isos=my-key-name" minio/opensource-isos/gnuos.iso 101 102 5. Stream MySQL database dump to Amazon S3 directly. 103 {{.Prompt}} mysqldump -u root -p ******* accountsdb | {{.HelpName}} s3/sql-backups/backups/accountsdb-oct-9-2015.sql 104 105 6. Write contents of stdin to an object on Amazon S3 cloud storage and assign REDUCED_REDUNDANCY storage-class to the uploaded object. 106 {{.Prompt}} {{.HelpName}} --storage-class REDUCED_REDUNDANCY s3/personalbuck/meeting-notes.txt 107 108 7. Copy to MinIO cloud storage with specified metadata, separated by ";" 109 {{.Prompt}} cat music.mp3 | {{.HelpName}} --attr "Cache-Control=max-age=90000,min-fresh=9000;Artist=Unknown" play/mybucket/music.mp3 110 111 8. Set tags to the uploaded objects 112 {{.Prompt}} tar cvf - . | {{.HelpName}} --tags "category=prod&type=backup" play/mybucket/backup.tar 113 `, 114 } 115 116 func pipe(ctx *cli.Context, targetURL string, encKeyDB map[string][]prefixSSEPair, meta map[string]string, quiet bool) *probe.Error { 117 // If possible increase the pipe buffer size 118 if e := increasePipeBufferSize(os.Stdin, ctx.Int("pipe-max-size")); e != nil { 119 fatalIf(probe.NewError(e), "Unable to increase custom pipe-max-size") 120 } 121 122 if targetURL == "" { 123 // When no target is specified, pipe cat's stdin to stdout. 124 return catOut(os.Stdin, -1).Trace() 125 } 126 127 storageClass := ctx.String("storage-class") 128 alias, _ := url2Alias(targetURL) 129 sseKey := getSSE(targetURL, encKeyDB[alias]) 130 131 multipartThreads := ctx.Int("concurrent") 132 if multipartThreads > 1 { 133 // We will be allocating large buffers, reduce default GC overhead 134 debug.SetGCPercent(20) 135 } 136 137 var multipartSize uint64 138 var e error 139 if partSizeStr := ctx.String("part-size"); partSizeStr != "" { 140 multipartSize, e = humanize.ParseBytes(partSizeStr) 141 if e != nil { 142 return probe.NewError(e) 143 } 144 } 145 146 // Stream from stdin to multiple objects until EOF. 147 // Ignore size, since os.Stat() would not return proper size all the time 148 // for local filesystem for example /proc files. 149 opts := PutOptions{ 150 sse: sseKey, 151 storageClass: storageClass, 152 metadata: meta, 153 multipartSize: multipartSize, 154 multipartThreads: uint(multipartThreads), 155 concurrentStream: ctx.IsSet("concurrent"), 156 } 157 158 var reader io.Reader 159 if !quiet { 160 pg := newProgressBar(0) 161 reader = io.TeeReader(os.Stdin, pg) 162 } else { 163 reader = os.Stdin 164 } 165 166 _, err := putTargetStreamWithURL(targetURL, reader, -1, opts) 167 // TODO: See if this check is necessary. 168 switch e := err.ToGoError().(type) { 169 case *os.PathError: 170 if e.Err == syscall.EPIPE { 171 // stdin closed by the user. Gracefully exit. 172 return nil 173 } 174 } 175 return err.Trace(targetURL) 176 } 177 178 // checkPipeSyntax - validate arguments passed by user 179 func checkPipeSyntax(ctx *cli.Context) { 180 if len(ctx.Args()) != 1 { 181 showCommandHelpAndExit(ctx, 1) // last argument is exit code. 182 } 183 } 184 185 // mainPipe is the main entry point for pipe command. 186 func mainPipe(ctx *cli.Context) error { 187 // validate pipe input arguments. 188 checkPipeSyntax(ctx) 189 190 encKeyDB, err := validateAndCreateEncryptionKeys(ctx) 191 fatalIf(err, "Unable to parse encryption keys.") 192 193 // globalQuiet is true for no window size to get. We just need --quiet here. 194 quiet := ctx.IsSet("quiet") 195 196 meta := map[string]string{} 197 if attr := ctx.String("attr"); attr != "" { 198 meta, err = getMetaDataEntry(attr) 199 fatalIf(err.Trace(attr), "Unable to parse --attr value") 200 } 201 if tags := ctx.String("tags"); tags != "" { 202 meta["X-Amz-Tagging"] = tags 203 } 204 if len(ctx.Args()) == 0 { 205 err = pipe(ctx, "", nil, meta, quiet) 206 fatalIf(err.Trace("stdout"), "Unable to write to one or more targets.") 207 } else { 208 // extract URLs. 209 URLs := ctx.Args() 210 err = pipe(ctx, URLs[0], encKeyDB, meta, quiet) 211 fatalIf(err.Trace(URLs[0]), "Unable to write to one or more targets.") 212 } 213 214 // Done. 215 return nil 216 }