github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/share-download-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 "strings" 23 "time" 24 25 "github.com/minio/cli" 26 "github.com/minio/mc/pkg/probe" 27 ) 28 29 var shareDownloadFlags = []cli.Flag{ 30 cli.BoolFlag{ 31 Name: "recursive, r", 32 Usage: "share all objects recursively", 33 }, 34 cli.StringFlag{ 35 Name: "version-id, vid", 36 Usage: "share a particular object version", 37 }, 38 shareFlagExpire, 39 } 40 41 // Share documents via URL. 42 var shareDownload = cli.Command{ 43 Name: "download", 44 Usage: "generate URLs for download access", 45 Action: mainShareDownload, 46 OnUsageError: onUsageError, 47 Before: setGlobalsFromContext, 48 Flags: append(shareDownloadFlags, globalFlags...), 49 CustomHelpTemplate: `NAME: 50 {{.HelpName}} - {{.Usage}} 51 52 USAGE: 53 {{.HelpName}} [FLAGS] TARGET [TARGET...] 54 55 FLAGS: 56 {{range .VisibleFlags}}{{.}} 57 {{end}} 58 EXAMPLES: 59 1. Share this object with 7 days default expiry. 60 {{.Prompt}} {{.HelpName}} s3/backup/2006-Mar-1/backup.tar.gz 61 62 2. Share this object with 10 minutes expiry. 63 {{.Prompt}} {{.HelpName}} --expire=10m s3/backup/2006-Mar-1/backup.tar.gz 64 65 3. Share all objects under this folder with 5 days expiry. 66 {{.Prompt}} {{.HelpName}} --expire=120h s3/backup/2006-Mar-1/ 67 68 4. Share all objects under this bucket and all its folders and sub-folders with 5 days expiry. 69 {{.Prompt}} {{.HelpName}} --recursive --expire=120h s3/backup/ 70 `, 71 } 72 73 // checkShareDownloadSyntax - validate command-line args. 74 func checkShareDownloadSyntax(ctx context.Context, cliCtx *cli.Context, encKeyDB map[string][]prefixSSEPair) { 75 args := cliCtx.Args() 76 if !args.Present() { 77 showCommandHelpAndExit(cliCtx, 1) // last argument is exit code. 78 } 79 80 // Parse expiry. 81 expiry := shareDefaultExpiry 82 expireArg := cliCtx.String("expire") 83 if expireArg != "" { 84 var e error 85 expiry, e = time.ParseDuration(expireArg) 86 fatalIf(probe.NewError(e), "Unable to parse expire=`"+expireArg+"`.") 87 } 88 89 // Validate expiry. 90 if expiry.Seconds() < 1 { 91 fatalIf(errDummy().Trace(expiry.String()), "Expiry cannot be lesser than 1 second.") 92 } 93 if expiry.Seconds() > 604800 { 94 fatalIf(errDummy().Trace(expiry.String()), "Expiry cannot be larger than 7 days.") 95 } 96 97 isRecursive := cliCtx.Bool("recursive") 98 99 versionID := cliCtx.String("version-id") 100 if versionID != "" && isRecursive { 101 fatalIf(errDummy().Trace(), "--version-id cannot be specified with --recursive flag.") 102 } 103 104 // Validate if object exists only if the `--recursive` flag was NOT specified 105 if !isRecursive { 106 for _, url := range cliCtx.Args() { 107 _, _, err := url2Stat(ctx, url2StatOptions{urlStr: url, versionID: "", fileAttr: false, encKeyDB: encKeyDB, timeRef: time.Time{}, isZip: false, ignoreBucketExistsCheck: false}) 108 if err != nil { 109 fatalIf(err.Trace(url), "Unable to stat `"+url+"`.") 110 } 111 } 112 } 113 } 114 115 // doShareURL share files from target. 116 func doShareDownloadURL(ctx context.Context, targetURL, versionID string, isRecursive bool, expiry time.Duration) *probe.Error { 117 targetAlias, targetURLFull, _, err := expandAlias(targetURL) 118 if err != nil { 119 return err.Trace(targetURL) 120 } 121 clnt, err := newClientFromAlias(targetAlias, targetURLFull) 122 if err != nil { 123 return err.Trace(targetURL) 124 } 125 126 // Load previously saved upload-shares. Add new entries and write it back. 127 shareDB := newShareDBV1() 128 shareDownloadsFile := getShareDownloadsFile() 129 err = shareDB.Load(shareDownloadsFile) 130 if err != nil { 131 return err.Trace(shareDownloadsFile) 132 } 133 134 // Channel which will receive objects whose URLs need to be shared 135 objectsCh := make(chan *ClientContent) 136 137 content, err := clnt.Stat(ctx, StatOptions{versionID: versionID}) 138 if err != nil { 139 return err.Trace(clnt.GetURL().String()) 140 } 141 142 if !content.Type.IsDir() { 143 go func() { 144 defer close(objectsCh) 145 objectsCh <- content 146 }() 147 } else { 148 if !strings.HasSuffix(targetURLFull, string(clnt.GetURL().Separator)) { 149 targetURLFull = targetURLFull + string(clnt.GetURL().Separator) 150 } 151 clnt, err = newClientFromAlias(targetAlias, targetURLFull) 152 if err != nil { 153 return err.Trace(targetURLFull) 154 } 155 // Recursive mode: Share list of objects 156 go func() { 157 defer close(objectsCh) 158 for content := range clnt.List(ctx, ListOptions{Recursive: isRecursive, ShowDir: DirNone}) { 159 objectsCh <- content 160 } 161 }() 162 } 163 164 // Iterate over all objects to generate share URL 165 for content := range objectsCh { 166 if content.Err != nil { 167 return content.Err.Trace(clnt.GetURL().String()) 168 } 169 // if any incoming directories, we don't need to calculate. 170 if content.Type.IsDir() { 171 continue 172 } 173 objectURL := content.URL.String() 174 objectVersionID := content.VersionID 175 newClnt, err := newClientFromAlias(targetAlias, objectURL) 176 if err != nil { 177 return err.Trace(objectURL) 178 } 179 180 // Generate share URL. 181 shareURL, err := newClnt.ShareDownload(ctx, objectVersionID, expiry) 182 if err != nil { 183 // add objectURL and expiry as part of the trace arguments. 184 return err.Trace(objectURL, "expiry="+expiry.String()) 185 } 186 187 // Make new entries to shareDB. 188 contentType := "" // Not useful for download shares. 189 shareDB.Set(objectURL, shareURL, expiry, contentType) 190 printMsg(shareMessage{ 191 ObjectURL: objectURL, 192 ShareURL: shareURL, 193 TimeLeft: expiry, 194 ContentType: contentType, 195 }) 196 } 197 198 // Save downloads and return. 199 return shareDB.Save(shareDownloadsFile) 200 } 201 202 // main for share download. 203 func mainShareDownload(cliCtx *cli.Context) error { 204 ctx, cancelShareDownload := context.WithCancel(globalContext) 205 defer cancelShareDownload() 206 207 // Parse encryption keys per command. 208 encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx) 209 fatalIf(err, "Unable to parse encryption keys.") 210 211 // check input arguments. 212 checkShareDownloadSyntax(ctx, cliCtx, encKeyDB) 213 214 // Initialize share config folder. 215 initShareConfig() 216 217 // Additional command speific theme customization. 218 shareSetColor() 219 220 // Set command flags from context. 221 isRecursive := cliCtx.Bool("recursive") 222 versionID := cliCtx.String("version-id") 223 expiry := shareDefaultExpiry 224 if cliCtx.String("expire") != "" { 225 var e error 226 expiry, e = time.ParseDuration(cliCtx.String("expire")) 227 fatalIf(probe.NewError(e), "Unable to parse expire=`"+cliCtx.String("expire")+"`.") 228 } 229 230 for _, targetURL := range cliCtx.Args() { 231 err := doShareDownloadURL(ctx, targetURL, versionID, isRecursive, expiry) 232 if err != nil { 233 switch err.ToGoError().(type) { 234 case APINotImplemented: 235 fatalIf(err.Trace(), "Unable to share a non S3 url `"+targetURL+"`.") 236 default: 237 fatalIf(err.Trace(targetURL), "Unable to share target `"+targetURL+"`.") 238 } 239 } 240 } 241 return nil 242 }