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  }