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 }