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  }