github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/share-upload-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 "fmt" 23 "regexp" 24 "strings" 25 "time" 26 27 "github.com/minio/cli" 28 "github.com/minio/mc/pkg/probe" 29 ) 30 31 var shareUploadFlags = []cli.Flag{ 32 cli.BoolFlag{ 33 Name: "recursive, r", 34 Usage: "recursively upload any object matching the prefix", 35 }, 36 shareFlagExpire, 37 shareFlagContentType, 38 } 39 40 // Share documents via URL. 41 var shareUpload = cli.Command{ 42 Name: "upload", 43 Usage: "generate `curl` command to upload objects without requiring access/secret keys", 44 Action: mainShareUpload, 45 OnUsageError: onUsageError, 46 Before: setGlobalsFromContext, 47 Flags: append(shareUploadFlags, globalFlags...), 48 CustomHelpTemplate: `NAME: 49 {{.HelpName}} - {{.Usage}} 50 51 USAGE: 52 {{.HelpName}} [FLAGS] TARGET [TARGET...] 53 54 FLAGS: 55 {{range .VisibleFlags}}{{.}} 56 {{end}} 57 EXAMPLES: 58 1. Generate a curl command to allow upload access for a single object. Command expires in 7 days (default). 59 {{.Prompt}} {{.HelpName}} s3/backup/2006-Mar-1/backup.tar.gz 60 61 2. Generate a curl command to allow upload access to a folder. Command expires in 120 hours. 62 {{.Prompt}} {{.HelpName}} --expire=120h s3/backup/2007-Mar-2/ 63 64 3. Generate a curl command to allow upload access of only '.png' images to a folder. Command expires in 2 hours. 65 {{.Prompt}} {{.HelpName}} --expire=2h --content-type=image/png s3/backup/2007-Mar-2/ 66 67 4. Generate a curl command to allow upload access to any objects matching the key prefix 'backup/'. Command expires in 2 hours. 68 {{.Prompt}} {{.HelpName}} --recursive --expire=2h s3/backup/2007-Mar-2/backup/ 69 `, 70 } 71 72 var shellQuoteRegex = regexp.MustCompile("([&;#$` \t\n<>()|'\"])") 73 74 func shellQuote(s string) string { 75 return shellQuoteRegex.ReplaceAllString(s, "\\$1") 76 } 77 78 // checkShareUploadSyntax - validate command-line args. 79 func checkShareUploadSyntax(ctx *cli.Context) { 80 args := ctx.Args() 81 if !args.Present() { 82 showCommandHelpAndExit(ctx, 1) // last argument is exit code. 83 } 84 85 // Set command flags from context. 86 isRecursive := ctx.Bool("recursive") 87 expireArg := ctx.String("expire") 88 89 // Parse expiry. 90 expiry := shareDefaultExpiry 91 if expireArg != "" { 92 var e error 93 expiry, e = time.ParseDuration(expireArg) 94 fatalIf(probe.NewError(e), "Unable to parse expire=`"+expireArg+"`.") 95 } 96 97 // Validate expiry. 98 if expiry.Seconds() < 1 { 99 fatalIf(errDummy().Trace(expiry.String()), 100 "Expiry cannot be lesser than 1 second.") 101 } 102 if expiry.Seconds() > 604800 { 103 fatalIf(errDummy().Trace(expiry.String()), 104 "Expiry cannot be larger than 7 days.") 105 } 106 107 for _, targetURL := range ctx.Args() { 108 url := newClientURL(targetURL) 109 if strings.HasSuffix(targetURL, string(url.Separator)) && !isRecursive { 110 fatalIf(errInvalidArgument().Trace(targetURL), 111 "Use --recursive flag to generate curl command for prefixes.") 112 } 113 } 114 } 115 116 // makeCurlCmd constructs curl command-line. 117 func makeCurlCmd(key, postURL string, isRecursive bool, uploadInfo map[string]string) (string, *probe.Error) { 118 postURL += " " 119 curlCommand := "curl " + postURL 120 for k, v := range uploadInfo { 121 if k == "key" { 122 key = v 123 continue 124 } 125 curlCommand += fmt.Sprintf("-F %s=%s ", k, v) 126 } 127 // If key starts with is enabled prefix it with the output. 128 if isRecursive { 129 curlCommand += fmt.Sprintf("-F key=%s<NAME> ", shellQuote(key)) // Object name. 130 } else { 131 curlCommand += fmt.Sprintf("-F key=%s ", shellQuote(key)) // Object name. 132 } 133 curlCommand += "-F file=@<FILE>" // File to upload. 134 return curlCommand, nil 135 } 136 137 // save shared URL to disk. 138 func saveSharedURL(objectURL, shareURL string, expiry time.Duration, contentType string) *probe.Error { 139 // Load previously saved upload-shares. 140 shareDB := newShareDBV1() 141 if err := shareDB.Load(getShareUploadsFile()); err != nil { 142 return err.Trace(getShareUploadsFile()) 143 } 144 145 // Make new entries to uploadsDB. 146 shareDB.Set(objectURL, shareURL, expiry, contentType) 147 shareDB.Save(getShareUploadsFile()) 148 149 return nil 150 } 151 152 // doShareUploadURL uploads files to the target. 153 func doShareUploadURL(ctx context.Context, objectURL string, isRecursive bool, expiry time.Duration, contentType string) *probe.Error { 154 clnt, err := newClient(objectURL) 155 if err != nil { 156 return err.Trace(objectURL) 157 } 158 159 // Generate pre-signed access info. 160 shareURL, uploadInfo, err := clnt.ShareUpload(ctx, isRecursive, expiry, contentType) 161 if err != nil { 162 return err.Trace(objectURL, "expiry="+expiry.String(), "contentType="+contentType) 163 } 164 165 // Get the new expanded url. 166 objectURL = clnt.GetURL().String() 167 168 // Generate curl command. 169 curlCmd, err := makeCurlCmd(objectURL, shareURL, isRecursive, uploadInfo) 170 if err != nil { 171 return err.Trace(objectURL) 172 } 173 174 printMsg(shareMessage{ 175 ObjectURL: objectURL, 176 ShareURL: curlCmd, 177 TimeLeft: expiry, 178 ContentType: contentType, 179 }) 180 181 // save shared URL to disk. 182 return saveSharedURL(objectURL, curlCmd, expiry, contentType) 183 } 184 185 // main for share upload command. 186 func mainShareUpload(cliCtx *cli.Context) error { 187 ctx, cancelShareDownload := context.WithCancel(globalContext) 188 defer cancelShareDownload() 189 190 // check input arguments. 191 checkShareUploadSyntax(cliCtx) 192 193 // Initialize share config folder. 194 initShareConfig() 195 196 // Additional command speific theme customization. 197 shareSetColor() 198 199 // Set command flags from context. 200 isRecursive := cliCtx.Bool("recursive") 201 expireArg := cliCtx.String("expire") 202 expiry := shareDefaultExpiry 203 contentType := cliCtx.String("content-type") 204 if expireArg != "" { 205 var e error 206 expiry, e = time.ParseDuration(expireArg) 207 fatalIf(probe.NewError(e), "Unable to parse expire=`"+expireArg+"`.") 208 } 209 210 for _, targetURL := range cliCtx.Args() { 211 err := doShareUploadURL(ctx, targetURL, isRecursive, expiry, contentType) 212 if err != nil { 213 switch err.ToGoError().(type) { 214 case APINotImplemented: 215 fatalIf(err.Trace(), "Unable to share a non S3 url `"+targetURL+"`.") 216 default: 217 fatalIf(err.Trace(targetURL), "Unable to generate curl command for upload `"+targetURL+"`.") 218 } 219 } 220 } 221 return nil 222 }