github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/cp-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  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"path/filepath"
    26  	"strings"
    27  
    28  	"github.com/fatih/color"
    29  	"github.com/minio/cli"
    30  	json "github.com/minio/colorjson"
    31  	"github.com/minio/mc/pkg/probe"
    32  	"github.com/minio/pkg/v2/console"
    33  )
    34  
    35  // cp command flags.
    36  var (
    37  	cpFlags = []cli.Flag{
    38  		cli.StringFlag{
    39  			Name:  "rewind",
    40  			Usage: "roll back object(s) to current version at specified time",
    41  		},
    42  		cli.StringFlag{
    43  			Name:  "version-id, vid",
    44  			Usage: "select an object version to copy",
    45  		},
    46  		cli.BoolFlag{
    47  			Name:  "recursive, r",
    48  			Usage: "copy recursively",
    49  		},
    50  		cli.StringFlag{
    51  			Name:  "older-than",
    52  			Usage: "copy objects older than value in duration string (e.g. 7d10h31s)",
    53  		},
    54  		cli.StringFlag{
    55  			Name:  "newer-than",
    56  			Usage: "copy objects newer than value in duration string (e.g. 7d10h31s)",
    57  		},
    58  		cli.StringFlag{
    59  			Name:  "storage-class, sc",
    60  			Usage: "set storage class for new object(s) on target",
    61  		},
    62  		cli.StringFlag{
    63  			Name:  "attr",
    64  			Usage: "add custom metadata for the object",
    65  		},
    66  		cli.BoolFlag{
    67  			Name:  "preserve, a",
    68  			Usage: "preserve filesystem attributes (mode, ownership, timestamps)",
    69  		},
    70  		cli.BoolFlag{
    71  			Name:  "disable-multipart",
    72  			Usage: "disable multipart upload feature",
    73  		},
    74  		cli.BoolFlag{
    75  			Name:  "md5",
    76  			Usage: "force all upload(s) to calculate md5sum checksum",
    77  		},
    78  		cli.StringFlag{
    79  			Name:  "tags",
    80  			Usage: "apply one or more tags to the uploaded objects",
    81  		},
    82  		cli.StringFlag{
    83  			Name:  rmFlag,
    84  			Usage: "retention mode to be applied on the object (governance, compliance)",
    85  		},
    86  		cli.StringFlag{
    87  			Name:  rdFlag,
    88  			Usage: "retention duration for the object in d days or y years",
    89  		},
    90  		cli.StringFlag{
    91  			Name:  lhFlag,
    92  			Usage: "apply legal hold to the copied object (on, off)",
    93  		},
    94  		cli.BoolFlag{
    95  			Name:  "zip",
    96  			Usage: "Extract from remote zip file (MinIO server source only)",
    97  		},
    98  	}
    99  )
   100  
   101  var (
   102  	rmFlag = "retention-mode"
   103  	rdFlag = "retention-duration"
   104  	lhFlag = "legal-hold"
   105  )
   106  
   107  // ErrInvalidMetadata reflects invalid metadata format
   108  var ErrInvalidMetadata = errors.New("specified metadata should be of form key1=value1;key2=value2;... and so on")
   109  
   110  // Copy command.
   111  var cpCmd = cli.Command{
   112  	Name:         "cp",
   113  	Usage:        "copy objects",
   114  	Action:       mainCopy,
   115  	OnUsageError: onUsageError,
   116  	Before:       setGlobalsFromContext,
   117  	Flags:        append(append(cpFlags, encFlags...), globalFlags...),
   118  	CustomHelpTemplate: `NAME:
   119    {{.HelpName}} - {{.Usage}}
   120  
   121  USAGE:
   122    {{.HelpName}} [FLAGS] SOURCE [SOURCE...] TARGET
   123  
   124  FLAGS:
   125    {{range .VisibleFlags}}{{.}}
   126    {{end}}
   127  
   128  ENVIRONMENT VARIABLES:
   129    MC_ENC_KMS: KMS encryption key in the form of (alias/prefix=key).
   130    MC_ENC_S3: S3 encryption key in the form of (alias/prefix=key).
   131  
   132  EXAMPLES:
   133    01. Copy a list of objects from local file system to Amazon S3 cloud storage.
   134        {{.Prompt}} {{.HelpName}} Music/*.ogg s3/jukebox/
   135  
   136    02. Copy a folder recursively from MinIO cloud storage to Amazon S3 cloud storage.
   137        {{.Prompt}} {{.HelpName}} --recursive play/mybucket/myfolder/ s3/mybucket/
   138  
   139    03. Copy multiple local folders recursively to MinIO cloud storage.
   140        {{.Prompt}} {{.HelpName}} --recursive backup/2014/ backup/2015/ play/archive/
   141  
   142    04. Copy a bucket recursively from aliased Amazon S3 cloud storage to local filesystem on Windows.
   143        {{.Prompt}} {{.HelpName}} --recursive s3\documents\2014\ C:\Backups\2014
   144  
   145    05. Copy files older than 7 days and 10 hours from MinIO cloud storage to Amazon S3 cloud storage.
   146        {{.Prompt}} {{.HelpName}} --older-than 7d10h play/mybucket/myfolder/ s3/mybucket/
   147  
   148    06. Copy files newer than 7 days and 10 hours from MinIO cloud storage to a local path.
   149        {{.Prompt}} {{.HelpName}} --newer-than 7d10h play/mybucket/myfolder/ ~/latest/
   150  
   151    07. Copy an object with name containing unicode characters to Amazon S3 cloud storage.
   152        {{.Prompt}} {{.HelpName}} 本語 s3/andoria/
   153  
   154    08. Copy a local folder with space separated characters to Amazon S3 cloud storage.
   155        {{.Prompt}} {{.HelpName}} --recursive 'workdir/documents/May 2014/' s3/miniocloud
   156  
   157    09. Copy a folder with encrypted objects recursively from Amazon S3 to MinIO cloud storage using s3 encryption.
   158        {{.Prompt}} {{.HelpName}} --recursive --enc-s3 "s3/documents/=my-aws-key" --enc-s3 "myminio/documents/=my-minio-key" s3/documents/ myminio/documents/
   159  
   160    10. Copy a folder with encrypted objects recursively from Amazon S3 to MinIO cloud storage.
   161        {{.Prompt}} {{.HelpName}} --recursive --enc-c "s3/documents/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" --enc-c "myminio/documents/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5BBB" s3/documents/ myminio/documents/
   162  
   163    11. Copy a list of objects from local file system to MinIO cloud storage with specified metadata, separated by ";"
   164        {{.Prompt}} {{.HelpName}} --attr "key1=value1;key2=value2" Music/*.mp4 play/mybucket/
   165  
   166    12. Copy a folder recursively from MinIO cloud storage to Amazon S3 cloud storage with Cache-Control and custom metadata, separated by ";".
   167        {{.Prompt}} {{.HelpName}} --attr "Cache-Control=max-age=90000,min-fresh=9000;key1=value1;key2=value2" --recursive play/mybucket/myfolder/ s3/mybucket/
   168  
   169    13. Copy a text file to an object storage and assign REDUCED_REDUNDANCY storage-class to the uploaded object.
   170        {{.Prompt}} {{.HelpName}} --storage-class REDUCED_REDUNDANCY myobject.txt play/mybucket
   171  
   172    14. Copy a text file to an object storage and preserve the file system attribute as metadata.
   173        {{.Prompt}} {{.HelpName}} -a myobject.txt play/mybucket
   174  
   175    15. Copy a text file to an object storage with object lock mode set to 'GOVERNANCE' with retention duration 1 day.
   176        {{.Prompt}} {{.HelpName}} --retention-mode governance --retention-duration 1d locked.txt play/locked-bucket/
   177  
   178    16. Copy a text file to an object storage with legal-hold enabled.
   179        {{.Prompt}} {{.HelpName}} --legal-hold on locked.txt play/locked-bucket/
   180  
   181    17. Copy a text file to an object storage and disable multipart upload feature.
   182        {{.Prompt}} {{.HelpName}} --disable-multipart myobject.txt play/mybucket
   183  
   184    18. Roll back 10 days in the past to copy the content of 'mybucket'
   185        {{.Prompt}} {{.HelpName}} --rewind 10d -r play/mybucket/ /tmp/dest/
   186  
   187    19. Set tags to the uploaded objects
   188        {{.Prompt}} {{.HelpName}} -r --tags "category=prod&type=backup" ./data/ play/another-bucket/
   189  
   190  `,
   191  }
   192  
   193  // copyMessage container for file copy messages
   194  type copyMessage struct {
   195  	Status     string `json:"status"`
   196  	Source     string `json:"source"`
   197  	Target     string `json:"target"`
   198  	Size       int64  `json:"size"`
   199  	TotalCount int64  `json:"totalCount"`
   200  	TotalSize  int64  `json:"totalSize"`
   201  }
   202  
   203  // String colorized copy message
   204  func (c copyMessage) String() string {
   205  	return console.Colorize("Copy", fmt.Sprintf("`%s` -> `%s`", c.Source, c.Target))
   206  }
   207  
   208  // JSON jsonified copy message
   209  func (c copyMessage) JSON() string {
   210  	c.Status = "success"
   211  	copyMessageBytes, e := json.MarshalIndent(c, "", " ")
   212  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   213  
   214  	return string(copyMessageBytes)
   215  }
   216  
   217  // Progress - an interface which describes current amount
   218  // of data written.
   219  type Progress interface {
   220  	Get() int64
   221  	SetTotal(int64)
   222  }
   223  
   224  // ProgressReader can be used to update the progress of
   225  // an on-going transfer progress.
   226  type ProgressReader interface {
   227  	io.Reader
   228  	Progress
   229  }
   230  
   231  // doCopy - Copy a single file from source to destination
   232  func doCopy(ctx context.Context, copyOpts doCopyOpts) URLs {
   233  	if copyOpts.cpURLs.Error != nil {
   234  		copyOpts.cpURLs.Error = copyOpts.cpURLs.Error.Trace()
   235  		return copyOpts.cpURLs
   236  	}
   237  
   238  	sourceAlias := copyOpts.cpURLs.SourceAlias
   239  	sourceURL := copyOpts.cpURLs.SourceContent.URL
   240  	targetAlias := copyOpts.cpURLs.TargetAlias
   241  	targetURL := copyOpts.cpURLs.TargetContent.URL
   242  	length := copyOpts.cpURLs.SourceContent.Size
   243  	sourcePath := filepath.ToSlash(filepath.Join(sourceAlias, sourceURL.Path))
   244  
   245  	if progressReader, ok := copyOpts.pg.(*progressBar); ok {
   246  		progressReader.SetCaption(copyOpts.cpURLs.SourceContent.URL.String() + ":")
   247  	} else {
   248  		targetPath := filepath.ToSlash(filepath.Join(targetAlias, targetURL.Path))
   249  		printMsg(copyMessage{
   250  			Source:     sourcePath,
   251  			Target:     targetPath,
   252  			Size:       length,
   253  			TotalCount: copyOpts.cpURLs.TotalCount,
   254  			TotalSize:  copyOpts.cpURLs.TotalSize,
   255  		})
   256  	}
   257  
   258  	urls := uploadSourceToTargetURL(ctx, uploadSourceToTargetURLOpts{
   259  		urls:                copyOpts.cpURLs,
   260  		progress:            copyOpts.pg,
   261  		encKeyDB:            copyOpts.encryptionKeys,
   262  		preserve:            copyOpts.preserve,
   263  		isZip:               copyOpts.isZip,
   264  		multipartSize:       copyOpts.multipartSize,
   265  		multipartThreads:    copyOpts.multipartThreads,
   266  		updateProgressTotal: copyOpts.updateProgressTotal,
   267  	})
   268  	if copyOpts.isMvCmd && urls.Error == nil {
   269  		rmManager.add(ctx, sourceAlias, sourceURL.String())
   270  	}
   271  
   272  	return urls
   273  }
   274  
   275  // doCopyFake - Perform a fake copy to update the progress bar appropriately.
   276  func doCopyFake(cpURLs URLs, pg Progress) URLs {
   277  	if progressReader, ok := pg.(*progressBar); ok {
   278  		progressReader.ProgressBar.Add64(cpURLs.SourceContent.Size)
   279  	}
   280  
   281  	return cpURLs
   282  }
   283  
   284  func printCopyURLsError(cpURLs *URLs) {
   285  	// Print in new line and adjust to top so that we
   286  	// don't print over the ongoing scan bar
   287  	if !globalQuiet && !globalJSON {
   288  		console.Eraseline()
   289  	}
   290  
   291  	if strings.Contains(cpURLs.Error.ToGoError().Error(),
   292  		" is a folder.") {
   293  		errorIf(cpURLs.Error.Trace(),
   294  			"Folder cannot be copied. Please use `...` suffix.")
   295  	} else {
   296  		errorIf(cpURLs.Error.Trace(),
   297  			"Unable to prepare URL for copying.")
   298  	}
   299  }
   300  
   301  func doCopySession(ctx context.Context, cancelCopy context.CancelFunc, cli *cli.Context, encryptionKeys map[string][]prefixSSEPair, isMvCmd bool) error {
   302  	var isCopied func(string) bool
   303  	var totalObjects, totalBytes int64
   304  
   305  	cpURLsCh := make(chan URLs, 10000)
   306  	errSeen := false
   307  
   308  	// Store a progress bar or an accounter
   309  	var pg ProgressReader
   310  
   311  	// Enable progress bar reader only during default mode.
   312  	if !globalQuiet && !globalJSON { // set up progress bar
   313  		pg = newProgressBar(totalBytes)
   314  	} else {
   315  		pg = newAccounter(totalBytes)
   316  	}
   317  
   318  	sourceURLs := cli.Args()[:len(cli.Args())-1]
   319  	targetURL := cli.Args()[len(cli.Args())-1] // Last one is target
   320  
   321  	// Check if the target path has object locking enabled
   322  	withLock, _ := isBucketLockEnabled(ctx, targetURL)
   323  
   324  	isRecursive := cli.Bool("recursive")
   325  	olderThan := cli.String("older-than")
   326  	newerThan := cli.String("newer-than")
   327  	rewind := cli.String("rewind")
   328  	versionID := cli.String("version-id")
   329  
   330  	go func() {
   331  		totalBytes := int64(0)
   332  		opts := prepareCopyURLsOpts{
   333  			sourceURLs:  sourceURLs,
   334  			targetURL:   targetURL,
   335  			isRecursive: isRecursive,
   336  			encKeyDB:    encryptionKeys,
   337  			olderThan:   olderThan,
   338  			newerThan:   newerThan,
   339  			timeRef:     parseRewindFlag(rewind),
   340  			versionID:   versionID,
   341  			isZip:       cli.Bool("zip"),
   342  		}
   343  
   344  		for cpURLs := range prepareCopyURLs(ctx, opts) {
   345  			if cpURLs.Error != nil {
   346  				errSeen = true
   347  				printCopyURLsError(&cpURLs)
   348  				break
   349  			}
   350  
   351  			totalBytes += cpURLs.SourceContent.Size
   352  			pg.SetTotal(totalBytes)
   353  			totalObjects++
   354  			cpURLsCh <- cpURLs
   355  		}
   356  		close(cpURLsCh)
   357  	}()
   358  
   359  	quitCh := make(chan struct{})
   360  	statusCh := make(chan URLs)
   361  	parallel := newParallelManager(statusCh)
   362  
   363  	go func() {
   364  		gracefulStop := func() {
   365  			parallel.stopAndWait()
   366  			close(statusCh)
   367  		}
   368  
   369  		for {
   370  			select {
   371  			case <-quitCh:
   372  				gracefulStop()
   373  				return
   374  			case cpURLs, ok := <-cpURLsCh:
   375  				if !ok {
   376  					gracefulStop()
   377  					return
   378  				}
   379  
   380  				// Save total count.
   381  				cpURLs.TotalCount = totalObjects
   382  
   383  				// Save totalSize.
   384  				cpURLs.TotalSize = totalBytes
   385  
   386  				// Initialize target metadata.
   387  				cpURLs.TargetContent.Metadata = make(map[string]string)
   388  
   389  				// Initialize target user metadata.
   390  				cpURLs.TargetContent.UserMetadata = make(map[string]string)
   391  
   392  				// Check and handle storage class if passed in command line args
   393  				if storageClass := cli.String("storage-class"); storageClass != "" {
   394  					cpURLs.TargetContent.StorageClass = storageClass
   395  				}
   396  
   397  				if rm := cli.String(rmFlag); rm != "" {
   398  					cpURLs.TargetContent.RetentionMode = rm
   399  					cpURLs.TargetContent.RetentionEnabled = true
   400  				}
   401  				if rd := cli.String(rdFlag); rd != "" {
   402  					cpURLs.TargetContent.RetentionDuration = rd
   403  				}
   404  				if lh := cli.String(lhFlag); lh != "" {
   405  					cpURLs.TargetContent.LegalHold = strings.ToUpper(lh)
   406  					cpURLs.TargetContent.LegalHoldEnabled = true
   407  				}
   408  
   409  				if tags := cli.String("tags"); tags != "" {
   410  					cpURLs.TargetContent.Metadata["X-Amz-Tagging"] = tags
   411  				}
   412  
   413  				preserve := cli.Bool("preserve")
   414  				isZip := cli.Bool("zip")
   415  				if cli.String("attr") != "" {
   416  					userMetaMap, _ := getMetaDataEntry(cli.String("attr"))
   417  					for metadataKey, metaDataVal := range userMetaMap {
   418  						cpURLs.TargetContent.UserMetadata[metadataKey] = metaDataVal
   419  					}
   420  				}
   421  
   422  				cpURLs.MD5 = cli.Bool("md5") || withLock
   423  				cpURLs.DisableMultipart = cli.Bool("disable-multipart")
   424  
   425  				// Verify if previously copied, notify progress bar.
   426  				if isCopied != nil && isCopied(cpURLs.SourceContent.URL.String()) {
   427  					parallel.queueTask(func() URLs {
   428  						return doCopyFake(cpURLs, pg)
   429  					}, 0)
   430  				} else {
   431  					// Print the copy resume summary once in start
   432  					parallel.queueTask(func() URLs {
   433  						return doCopy(ctx, doCopyOpts{
   434  							cpURLs:         cpURLs,
   435  							pg:             pg,
   436  							encryptionKeys: encryptionKeys,
   437  							isMvCmd:        isMvCmd,
   438  							preserve:       preserve,
   439  							isZip:          isZip,
   440  						})
   441  					}, cpURLs.SourceContent.Size)
   442  				}
   443  			}
   444  		}
   445  	}()
   446  
   447  	var retErr error
   448  	cpAllFilesErr := true
   449  
   450  loop:
   451  	for {
   452  		select {
   453  		case <-globalContext.Done():
   454  			close(quitCh)
   455  			cancelCopy()
   456  			// Receive interrupt notification.
   457  			if !globalQuiet && !globalJSON {
   458  				console.Eraseline()
   459  			}
   460  			break loop
   461  		case cpURLs, ok := <-statusCh:
   462  			// Status channel is closed, we should return.
   463  			if !ok {
   464  				break loop
   465  			}
   466  			if cpURLs.Error == nil {
   467  				cpAllFilesErr = false
   468  			} else {
   469  
   470  				// Set exit status for any copy error
   471  				retErr = exitStatus(globalErrorExitStatus)
   472  
   473  				// Print in new line and adjust to top so that we
   474  				// don't print over the ongoing progress bar.
   475  				if !globalQuiet && !globalJSON {
   476  					console.Eraseline()
   477  				}
   478  				errorIf(cpURLs.Error.Trace(cpURLs.SourceContent.URL.String()),
   479  					fmt.Sprintf("Failed to copy `%s`.", cpURLs.SourceContent.URL.String()))
   480  				if isErrIgnored(cpURLs.Error) {
   481  					cpAllFilesErr = false
   482  					continue loop
   483  				}
   484  
   485  				errSeen = true
   486  				if progressReader, pgok := pg.(*progressBar); pgok {
   487  					if progressReader.ProgressBar.Get() > 0 {
   488  						writeContSize := (int)(cpURLs.SourceContent.Size)
   489  						totalPGSize := (int)(progressReader.ProgressBar.Total)
   490  						written := (int)(progressReader.ProgressBar.Get())
   491  						if totalPGSize > writeContSize && written > writeContSize {
   492  							progressReader.ProgressBar.Set((written - writeContSize))
   493  							progressReader.ProgressBar.Update()
   494  						}
   495  					}
   496  				}
   497  
   498  			}
   499  		}
   500  	}
   501  
   502  	if progressReader, ok := pg.(*progressBar); ok {
   503  		if errSeen || (cpAllFilesErr && totalObjects > 0) {
   504  			// We only erase a line if we are displaying a progress bar
   505  			if !globalQuiet && !globalJSON {
   506  				console.Eraseline()
   507  			}
   508  		} else if progressReader.ProgressBar.Get() > 0 {
   509  			progressReader.Finish()
   510  		}
   511  	} else {
   512  		if accntReader, ok := pg.(*accounter); ok {
   513  			if errSeen || (cpAllFilesErr && totalObjects > 0) {
   514  				// We only erase a line if we are displaying a progress bar
   515  				if !globalQuiet && !globalJSON {
   516  					console.Eraseline()
   517  				}
   518  			} else {
   519  				printMsg(accntReader.Stat())
   520  			}
   521  		}
   522  	}
   523  
   524  	// Source has error
   525  	if errSeen && totalObjects == 0 && retErr == nil {
   526  		retErr = exitStatus(globalErrorExitStatus)
   527  	}
   528  
   529  	return retErr
   530  }
   531  
   532  // mainCopy is the entry point for cp command.
   533  func mainCopy(cliCtx *cli.Context) error {
   534  	ctx, cancelCopy := context.WithCancel(globalContext)
   535  	defer cancelCopy()
   536  
   537  	checkCopySyntax(cliCtx)
   538  	console.SetColor("Copy", color.New(color.FgGreen, color.Bold))
   539  
   540  	var err *probe.Error
   541  
   542  	// Parse encryption keys per command.
   543  	encryptionKeyMap, err := validateAndCreateEncryptionKeys(cliCtx)
   544  	if err != nil {
   545  		err.Trace(cliCtx.Args()...)
   546  	}
   547  	fatalIf(err, "SSE Error")
   548  
   549  	return doCopySession(ctx, cancelCopy, cliCtx, encryptionKeyMap, false)
   550  }
   551  
   552  type doCopyOpts struct {
   553  	cpURLs                   URLs
   554  	pg                       ProgressReader
   555  	encryptionKeys           map[string][]prefixSSEPair
   556  	isMvCmd, preserve, isZip bool
   557  	updateProgressTotal      bool
   558  	multipartSize            string
   559  	multipartThreads         string
   560  }