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