github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/rm-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 "bufio" 22 "context" 23 "fmt" 24 "net/http" 25 "os" 26 "path" 27 "path/filepath" 28 "strings" 29 "time" 30 31 "github.com/fatih/color" 32 "github.com/minio/cli" 33 json "github.com/minio/colorjson" 34 "github.com/minio/mc/pkg/probe" 35 "github.com/minio/minio-go/v7" 36 "github.com/minio/pkg/v2/console" 37 ) 38 39 // rm specific flags. 40 var ( 41 rmFlags = []cli.Flag{ 42 cli.BoolFlag{ 43 Name: "versions", 44 Usage: "remove object(s) and all its versions", 45 }, 46 cli.BoolFlag{ 47 Name: "recursive, r", 48 Usage: "remove recursively", 49 }, 50 cli.BoolFlag{ 51 Name: "force", 52 Usage: "allow a recursive remove operation", 53 }, 54 cli.BoolFlag{ 55 Name: "dangerous", 56 Usage: "allow site-wide removal of objects", 57 }, 58 cli.StringFlag{ 59 Name: "rewind", 60 Usage: "roll back object(s) to current version at specified time", 61 }, 62 cli.StringFlag{ 63 Name: "version-id, vid", 64 Usage: "delete a specific version of an object", 65 }, 66 cli.BoolFlag{ 67 Name: "incomplete, I", 68 Usage: "remove incomplete uploads", 69 }, 70 cli.BoolFlag{ 71 Name: "dry-run", 72 Usage: "perform a fake remove operation", 73 }, 74 cli.BoolFlag{ 75 Name: "fake", 76 Usage: "perform a fake remove operation", 77 Hidden: true, // deprecated 2022 78 }, 79 cli.BoolFlag{ 80 Name: "stdin", 81 Usage: "read object names from STDIN", 82 }, 83 cli.StringFlag{ 84 Name: "older-than", 85 Usage: "remove objects older than value in duration string (e.g. 7d10h31s)", 86 }, 87 cli.StringFlag{ 88 Name: "newer-than", 89 Usage: "remove objects newer than value in duration string (e.g. 7d10h31s)", 90 }, 91 cli.BoolFlag{ 92 Name: "bypass", 93 Usage: "bypass governance", 94 }, 95 cli.BoolFlag{ 96 Name: "non-current", 97 Usage: "remove object(s) versions that are non-current", 98 }, 99 cli.BoolFlag{ 100 Name: "purge", 101 Usage: "attempt a prefix purge, requires confirmation please use with caution - only works with '--force'", 102 Hidden: true, 103 }, 104 } 105 ) 106 107 // remove a file or folder. 108 var rmCmd = cli.Command{ 109 Name: "rm", 110 Usage: "remove object(s)", 111 Action: mainRm, 112 OnUsageError: onUsageError, 113 Before: setGlobalsFromContext, 114 Flags: append(rmFlags, globalFlags...), 115 CustomHelpTemplate: `NAME: 116 {{.HelpName}} - {{.Usage}} 117 118 USAGE: 119 {{.HelpName}} [FLAGS] TARGET [TARGET ...] 120 121 FLAGS: 122 {{range .VisibleFlags}}{{.}} 123 {{end}} 124 125 EXAMPLES: 126 01. Remove a file. 127 {{.Prompt}} {{.HelpName}} 1999/old-backup.tgz 128 129 02. Perform a fake remove operation. 130 {{.Prompt}} {{.HelpName}} --dry-run 1999/old-backup.tgz 131 132 03. Remove all objects recursively from bucket 'jazz-songs' matching the prefix 'louis'. 133 {{.Prompt}} {{.HelpName}} --recursive --force s3/jazz-songs/louis/ 134 135 04. Remove all objects older than '90' days recursively from bucket 'jazz-songs' matching the prefix 'louis'. 136 {{.Prompt}} {{.HelpName}} --recursive --force --older-than 90d s3/jazz-songs/louis/ 137 138 05. Remove all objects newer than 7 days and 10 hours recursively from bucket 'pop-songs' 139 {{.Prompt}} {{.HelpName}} --recursive --force --newer-than 7d10h s3/pop-songs/ 140 141 06. Remove all objects read from STDIN. 142 {{.Prompt}} {{.HelpName}} --force --stdin 143 144 07. Remove all objects recursively from Amazon S3 cloud storage. 145 {{.Prompt}} {{.HelpName}} --recursive --force --dangerous s3 146 147 08. Remove all objects older than '90' days recursively under all buckets. 148 {{.Prompt}} {{.HelpName}} --recursive --dangerous --force --older-than 90d s3 149 150 09. Drop all incomplete uploads on the bucket 'jazz-songs'. 151 {{.Prompt}} {{.HelpName}} --incomplete --recursive --force s3/jazz-songs/ 152 153 10. Bypass object retention in governance mode and delete the object. 154 {{.Prompt}} {{.HelpName}} --bypass s3/pop-songs/ 155 156 11. Remove a particular version ID. 157 {{.Prompt}} {{.HelpName}} s3/docs/money.xls --version-id "f20f3792-4bd4-4288-8d3c-b9d05b3b62f6" 158 159 12. Remove all object versions older than one year. 160 {{.Prompt}} {{.HelpName}} s3/docs/ --recursive --versions --rewind 365d 161 162 14. Perform a fake removal of object(s) versions that are non-current and older than 10 days. If top-level version is a delete 163 marker, this will also be deleted when --non-current flag is specified. 164 {{.Prompt}} {{.HelpName}} s3/docs/ --recursive --force --versions --non-current --older-than 10d --dry-run 165 `, 166 } 167 168 // Structured message depending on the type of console. 169 type rmMessage struct { 170 Status string `json:"status"` 171 Key string `json:"key"` 172 DeleteMarker bool `json:"deleteMarker"` 173 VersionID string `json:"versionID"` 174 ModTime *time.Time `json:"modTime"` 175 DryRun bool `json:"dryRun"` 176 } 177 178 // Colorized message for console printing. 179 func (r rmMessage) String() string { 180 msg := "Removed " 181 if r.DryRun { 182 msg = "DRYRUN: Removing " 183 } 184 185 if r.DeleteMarker { 186 msg = "Created delete marker " 187 } 188 189 msg += console.Colorize("Removed", fmt.Sprintf("`%s`", r.Key)) 190 if r.VersionID != "" { 191 msg += fmt.Sprintf(" (versionId=%s)", r.VersionID) 192 if r.ModTime != nil { 193 msg += fmt.Sprintf(" (modTime=%s)", r.ModTime.Format(printDate)) 194 } 195 } 196 msg += "." 197 return msg 198 } 199 200 // JSON'ified message for scripting. 201 func (r rmMessage) JSON() string { 202 r.Status = "success" 203 msgBytes, e := json.MarshalIndent(r, "", " ") 204 fatalIf(probe.NewError(e), "Unable to marshal into JSON.") 205 return string(msgBytes) 206 } 207 208 // Validate command line arguments. 209 func checkRmSyntax(ctx context.Context, cliCtx *cli.Context) { 210 // Set command flags from context. 211 isForce := cliCtx.Bool("force") 212 isRecursive := cliCtx.Bool("recursive") 213 isStdin := cliCtx.Bool("stdin") 214 isDangerous := cliCtx.Bool("dangerous") 215 isVersions := cliCtx.Bool("versions") 216 isNoncurrentVersion := cliCtx.Bool("non-current") 217 isForceDel := cliCtx.Bool("purge") 218 versionID := cliCtx.String("version-id") 219 rewind := cliCtx.String("rewind") 220 isNamespaceRemoval := false 221 222 if versionID != "" && (isRecursive || isVersions || rewind != "") { 223 fatalIf(errDummy().Trace(), 224 "You cannot specify --version-id with any of --versions, --rewind and --recursive flags.") 225 } 226 227 if isNoncurrentVersion && !(isVersions && isRecursive) { 228 fatalIf(errDummy().Trace(), 229 "You cannot specify --non-current without --versions --recursive, please use --non-current --versions --recursive.") 230 } 231 232 if isForceDel && !isForce { 233 fatalIf(errDummy().Trace(), 234 "You cannot specify --purge without --force.") 235 } 236 237 if isForceDel && isRecursive { 238 fatalIf(errDummy().Trace(), 239 "You cannot specify --purge with --recursive.") 240 } 241 242 if isForceDel && (isNoncurrentVersion || isVersions || cliCtx.IsSet("older-than") || cliCtx.IsSet("newer-than") || versionID != "") { 243 fatalIf(errDummy().Trace(), 244 "You cannot specify --purge flag with any flag(s) other than --force.") 245 } 246 247 if !isForceDel { 248 for _, url := range cliCtx.Args() { 249 // clean path for aliases like s3/. 250 // Note: UNC path using / works properly in go 1.9.2 even though it breaks the UNC specification. 251 url = filepath.ToSlash(filepath.Clean(url)) 252 // namespace removal applies only for non FS. So filter out if passed url represents a directory 253 dir, _ := isAliasURLDir(ctx, url, nil, time.Time{}, false) 254 if dir { 255 _, path := url2Alias(url) 256 isNamespaceRemoval = (path == "") 257 break 258 } 259 if dir && isRecursive && !isForce { 260 fatalIf(errDummy().Trace(), 261 "Removal requires --force flag. This operation is *IRREVERSIBLE*. Please review carefully before performing this *DANGEROUS* operation.") 262 } 263 if dir && !isRecursive { 264 fatalIf(errDummy().Trace(), 265 "Removal requires --recursive flag. This operation is *IRREVERSIBLE*. Please review carefully before performing this *DANGEROUS* operation.") 266 } 267 } 268 } 269 270 if !cliCtx.Args().Present() && !isStdin { 271 exitCode := 1 272 showCommandHelpAndExit(cliCtx, exitCode) 273 } 274 275 // For all recursive or versions bulk deletion operations make sure to check for 'force' flag. 276 if (isVersions || isRecursive || isStdin) && !isForce { 277 fatalIf(errDummy().Trace(), 278 "Removal requires --force flag. This operation is *IRREVERSIBLE*. Please review carefully before performing this *DANGEROUS* operation.") 279 } 280 281 if isNamespaceRemoval && !(isDangerous && isForce) { 282 fatalIf(errDummy().Trace(), 283 "This operation results in site-wide removal of objects. If you are really sure, retry this command with ‘--dangerous’ and ‘--force’ flags.") 284 } 285 } 286 287 // Remove a single object or a single version in a versioned bucket 288 func removeSingle(url, versionID string, opts removeOpts) error { 289 ctx, cancel := context.WithCancel(globalContext) 290 defer cancel() 291 292 var ( 293 // A HEAD request can fail with: 294 // - 400 Bad Request when the object SSE-C 295 // - 405 Method Not Allowed when this is a delete marker 296 // In those cases, we still want t remove the target object/version 297 // so we simply ignore them. 298 ignoreStatError bool 299 300 isDir bool 301 modTime time.Time 302 ) 303 304 targetAlias, targetURL, _ := mustExpandAlias(url) 305 if !opts.isForceDel { 306 _, content, pErr := url2Stat(ctx, url2StatOptions{ 307 urlStr: url, 308 versionID: versionID, 309 fileAttr: false, 310 timeRef: time.Time{}, 311 isZip: false, 312 ignoreBucketExistsCheck: false, 313 }) 314 if pErr != nil { 315 switch st := minio.ToErrorResponse(pErr.ToGoError()).StatusCode; st { 316 case http.StatusBadRequest, http.StatusMethodNotAllowed: 317 ignoreStatError = true 318 default: 319 _, ok := pErr.ToGoError().(ObjectMissing) 320 ignoreStatError = (st == http.StatusServiceUnavailable || ok || st == http.StatusNotFound) && (opts.isForce && opts.isForceDel) 321 if !ignoreStatError { 322 errorIf(pErr.Trace(url), "Failed to remove `"+url+"`.") 323 return exitStatus(globalErrorExitStatus) 324 } 325 } 326 } else { 327 isDir = content.Type.IsDir() 328 modTime = content.Time 329 } 330 331 // We should not proceed 332 if ignoreStatError && opts.olderThan != "" || opts.newerThan != "" { 333 errorIf(pErr.Trace(url), "Unable to stat `"+url+"`.") 334 return exitStatus(globalErrorExitStatus) 335 } 336 337 // Skip objects older than older--than parameter if specified 338 if opts.olderThan != "" && isOlder(modTime, opts.olderThan) { 339 return nil 340 } 341 342 // Skip objects older than older--than parameter if specified 343 if opts.newerThan != "" && isNewer(modTime, opts.newerThan) { 344 return nil 345 } 346 347 if opts.isFake { 348 printDryRunMsg(targetAlias, content, opts.withVersions) 349 return nil 350 } 351 } 352 353 clnt, pErr := newClientFromAlias(targetAlias, targetURL) 354 if pErr != nil { 355 errorIf(pErr.Trace(url), "Invalid argument `"+url+"`.") 356 return exitStatus(globalErrorExitStatus) // End of journey. 357 } 358 359 if !strings.HasSuffix(targetURL, string(clnt.GetURL().Separator)) && isDir { 360 targetURL = targetURL + string(clnt.GetURL().Separator) 361 } 362 363 contentCh := make(chan *ClientContent, 1) 364 contentURL := *newClientURL(targetURL) 365 contentCh <- &ClientContent{URL: contentURL, VersionID: versionID} 366 close(contentCh) 367 isRemoveBucket := false 368 resultCh := clnt.Remove(ctx, opts.isIncomplete, isRemoveBucket, opts.isBypass, opts.isForce && opts.isForceDel, contentCh) 369 for result := range resultCh { 370 if result.Err != nil { 371 errorIf(result.Err.Trace(url), "Failed to remove `"+url+"`.") 372 switch result.Err.ToGoError().(type) { 373 case PathInsufficientPermission: 374 // Ignore Permission error. 375 continue 376 } 377 return exitStatus(globalErrorExitStatus) 378 } 379 msg := rmMessage{ 380 Key: path.Join(targetAlias, result.BucketName, result.ObjectName), 381 VersionID: result.ObjectVersionID, 382 } 383 if result.DeleteMarker { 384 msg.DeleteMarker = true 385 msg.VersionID = result.DeleteMarkerVersionID 386 } 387 printMsg(msg) 388 } 389 return nil 390 } 391 392 type removeOpts struct { 393 timeRef time.Time 394 withVersions bool 395 nonCurrentVersion bool 396 isForce bool 397 isRecursive bool 398 isIncomplete bool 399 isFake bool 400 isBypass bool 401 isForceDel bool 402 olderThan string 403 newerThan string 404 } 405 406 func printDryRunMsg(targetAlias string, content *ClientContent, printModTime bool) { 407 if content == nil { 408 return 409 } 410 msg := rmMessage{ 411 Status: "success", 412 DryRun: true, 413 Key: targetAlias + getKey(content), 414 VersionID: content.VersionID, 415 } 416 if printModTime { 417 msg.ModTime = &content.Time 418 } 419 printMsg(msg) 420 } 421 422 // listAndRemove uses listing before removal, it can list recursively or not, with versions or not. 423 // 424 // Use cases: 425 // * Remove objects recursively 426 // * Remove all versions of a single object 427 func listAndRemove(url string, opts removeOpts) error { 428 ctx, cancelRemove := context.WithCancel(globalContext) 429 defer cancelRemove() 430 431 targetAlias, targetURL, _ := mustExpandAlias(url) 432 clnt, pErr := newClientFromAlias(targetAlias, targetURL) 433 if pErr != nil { 434 errorIf(pErr.Trace(url), "Failed to remove `"+url+"` recursively.") 435 return exitStatus(globalErrorExitStatus) // End of journey. 436 } 437 contentCh := make(chan *ClientContent) 438 isRemoveBucket := false 439 440 listOpts := ListOptions{Recursive: opts.isRecursive, Incomplete: opts.isIncomplete, ShowDir: DirLast} 441 if !opts.timeRef.IsZero() { 442 listOpts.WithOlderVersions = opts.withVersions 443 listOpts.WithDeleteMarkers = true 444 listOpts.TimeRef = opts.timeRef 445 } 446 atLeastOneObjectFound := false 447 448 resultCh := clnt.Remove(ctx, opts.isIncomplete, isRemoveBucket, opts.isBypass, false, contentCh) 449 450 var lastPath string 451 var perObjectVersions []*ClientContent 452 for content := range clnt.List(ctx, listOpts) { 453 if content.Err != nil { 454 errorIf(content.Err.Trace(url), "Failed to remove `"+url+"` recursively.") 455 switch content.Err.ToGoError().(type) { 456 case PathInsufficientPermission: 457 // Ignore Permission error. 458 continue 459 } 460 close(contentCh) 461 return exitStatus(globalErrorExitStatus) 462 } 463 464 urlString := content.URL.Path 465 466 // rm command is not supposed to remove buckets, ignore if this is a bucket name 467 if content.URL.Type == objectStorage && strings.LastIndex(urlString, string(content.URL.Separator)) == 0 { 468 continue 469 } 470 471 if !opts.isRecursive { 472 currentObjectURL := targetAlias + getKey(content) 473 standardizedURL := getStandardizedURL(currentObjectURL) 474 if !strings.HasPrefix(url, standardizedURL) { 475 break 476 } 477 } 478 479 if opts.nonCurrentVersion && opts.isRecursive && opts.withVersions { 480 if lastPath != content.URL.Path { 481 lastPath = content.URL.Path 482 for _, content := range perObjectVersions { 483 if content.IsLatest && !content.IsDeleteMarker { 484 continue 485 } 486 if !content.Time.IsZero() { 487 // Skip objects older than --older-than parameter, if specified 488 if opts.olderThan != "" && isOlder(content.Time, opts.olderThan) { 489 continue 490 } 491 492 // Skip objects newer than --newer-than parameter if specified 493 if opts.newerThan != "" && isNewer(content.Time, opts.newerThan) { 494 continue 495 } 496 } else { 497 // Skip prefix levels. 498 continue 499 } 500 501 if opts.isFake { 502 printDryRunMsg(targetAlias, content, true) 503 continue 504 } 505 506 sent := false 507 for !sent { 508 select { 509 case contentCh <- content: 510 sent = true 511 case result := <-resultCh: 512 path := path.Join(targetAlias, result.BucketName, result.ObjectName) 513 if result.Err != nil { 514 errorIf(result.Err.Trace(path), 515 "Failed to remove `"+path+"`.") 516 switch result.Err.ToGoError().(type) { 517 case PathInsufficientPermission: 518 // Ignore Permission error. 519 continue 520 } 521 close(contentCh) 522 return exitStatus(globalErrorExitStatus) 523 } 524 msg := rmMessage{ 525 Key: path, 526 VersionID: result.ObjectVersionID, 527 } 528 if result.DeleteMarker { 529 msg.DeleteMarker = true 530 msg.VersionID = result.DeleteMarkerVersionID 531 } 532 printMsg(msg) 533 } 534 } 535 } 536 perObjectVersions = []*ClientContent{} 537 } 538 atLeastOneObjectFound = true 539 perObjectVersions = append(perObjectVersions, content) 540 continue 541 } 542 543 // This will mark that we found at least one target object 544 // even that it could be ineligible for deletion. So we can 545 // inform the user that he was searching in an empty area 546 atLeastOneObjectFound = true 547 548 if !content.Time.IsZero() { 549 // Skip objects older than --older-than parameter, if specified 550 if opts.olderThan != "" && isOlder(content.Time, opts.olderThan) { 551 continue 552 } 553 554 // Skip objects newer than --newer-than parameter if specified 555 if opts.newerThan != "" && isNewer(content.Time, opts.newerThan) { 556 continue 557 } 558 } else { 559 // Skip prefix levels. 560 continue 561 } 562 563 if !opts.isFake { 564 sent := false 565 for !sent { 566 select { 567 case contentCh <- content: 568 sent = true 569 case result := <-resultCh: 570 path := path.Join(targetAlias, result.BucketName, result.ObjectName) 571 if result.Err != nil { 572 errorIf(result.Err.Trace(path), 573 "Failed to remove `"+path+"`.") 574 switch e := result.Err.ToGoError().(type) { 575 case PathInsufficientPermission: 576 // Ignore Permission error. 577 continue 578 case minio.ErrorResponse: 579 if strings.Contains(e.Message, "Object is WORM protected and cannot be overwritten") { 580 continue 581 } 582 } 583 close(contentCh) 584 return exitStatus(globalErrorExitStatus) 585 } 586 msg := rmMessage{ 587 Key: path, 588 VersionID: result.ObjectVersionID, 589 } 590 if result.DeleteMarker { 591 msg.DeleteMarker = true 592 msg.VersionID = result.DeleteMarkerVersionID 593 } 594 printMsg(msg) 595 } 596 } 597 } else { 598 printDryRunMsg(targetAlias, content, opts.withVersions) 599 } 600 } 601 602 if opts.nonCurrentVersion && opts.isRecursive && opts.withVersions { 603 for _, content := range perObjectVersions { 604 if content.IsLatest && !content.IsDeleteMarker { 605 continue 606 } 607 if !content.Time.IsZero() { 608 // Skip objects older than --older-than parameter, if specified 609 if opts.olderThan != "" && isOlder(content.Time, opts.olderThan) { 610 continue 611 } 612 613 // Skip objects newer than --newer-than parameter if specified 614 if opts.newerThan != "" && isNewer(content.Time, opts.newerThan) { 615 continue 616 } 617 } else { 618 // Skip prefix levels. 619 continue 620 } 621 622 if opts.isFake { 623 printDryRunMsg(targetAlias, content, true) 624 continue 625 } 626 627 sent := false 628 for !sent { 629 select { 630 case contentCh <- content: 631 sent = true 632 case result := <-resultCh: 633 path := path.Join(targetAlias, result.BucketName, result.ObjectName) 634 if result.Err != nil { 635 errorIf(result.Err.Trace(path), 636 "Failed to remove `"+path+"`.") 637 switch result.Err.ToGoError().(type) { 638 case PathInsufficientPermission: 639 // Ignore Permission error. 640 continue 641 } 642 close(contentCh) 643 return exitStatus(globalErrorExitStatus) 644 } 645 msg := rmMessage{ 646 Key: path, 647 VersionID: result.ObjectVersionID, 648 } 649 if result.DeleteMarker { 650 msg.DeleteMarker = true 651 msg.VersionID = result.DeleteMarkerVersionID 652 } 653 printMsg(msg) 654 } 655 } 656 } 657 } 658 659 close(contentCh) 660 if opts.isFake { 661 return nil 662 } 663 for result := range resultCh { 664 path := path.Join(targetAlias, result.BucketName, result.ObjectName) 665 if result.Err != nil { 666 errorIf(result.Err.Trace(path), "Failed to remove `"+path+"` recursively.") 667 switch result.Err.ToGoError().(type) { 668 case PathInsufficientPermission: 669 // Ignore Permission error. 670 continue 671 } 672 return exitStatus(globalErrorExitStatus) 673 } 674 msg := rmMessage{ 675 Key: path, 676 VersionID: result.ObjectVersionID, 677 } 678 if result.DeleteMarker { 679 msg.DeleteMarker = true 680 msg.VersionID = result.DeleteMarkerVersionID 681 } 682 printMsg(msg) 683 } 684 685 if !atLeastOneObjectFound { 686 if opts.isForce { 687 // Do not throw an exit code with --force check unix `rm -f` 688 // behavior and do not print an error as well. 689 return nil 690 } 691 errorIf(errDummy().Trace(url), "No object/version found to be removed in `"+url+"`.") 692 return exitStatus(globalErrorExitStatus) 693 } 694 695 return nil 696 } 697 698 // main for rm command. 699 func mainRm(cliCtx *cli.Context) error { 700 ctx, cancelRm := context.WithCancel(globalContext) 701 defer cancelRm() 702 703 checkRmSyntax(ctx, cliCtx) 704 705 isIncomplete := cliCtx.Bool("incomplete") 706 isRecursive := cliCtx.Bool("recursive") 707 isFake := cliCtx.Bool("dry-run") || cliCtx.Bool("fake") 708 isStdin := cliCtx.Bool("stdin") 709 isBypass := cliCtx.Bool("bypass") 710 olderThan := cliCtx.String("older-than") 711 newerThan := cliCtx.String("newer-than") 712 isForce := cliCtx.Bool("force") 713 isForceDel := cliCtx.Bool("purge") 714 withNoncurrentVersion := cliCtx.Bool("non-current") 715 withVersions := cliCtx.Bool("versions") 716 versionID := cliCtx.String("version-id") 717 rewind := parseRewindFlag(cliCtx.String("rewind")) 718 719 if withVersions && rewind.IsZero() { 720 rewind = time.Now().UTC() 721 } 722 723 // Set color. 724 console.SetColor("Removed", color.New(color.FgGreen, color.Bold)) 725 726 var rerr error 727 var e error 728 // Support multiple targets. 729 for _, url := range cliCtx.Args() { 730 if isRecursive || withVersions { 731 e = listAndRemove(url, removeOpts{ 732 timeRef: rewind, 733 withVersions: withVersions, 734 nonCurrentVersion: withNoncurrentVersion, 735 isForce: isForce, 736 isRecursive: isRecursive, 737 isIncomplete: isIncomplete, 738 isFake: isFake, 739 isBypass: isBypass, 740 olderThan: olderThan, 741 newerThan: newerThan, 742 }) 743 } else { 744 e = removeSingle(url, versionID, removeOpts{ 745 isIncomplete: isIncomplete, 746 isFake: isFake, 747 isForce: isForce, 748 isForceDel: isForceDel, 749 isBypass: isBypass, 750 olderThan: olderThan, 751 newerThan: newerThan, 752 }) 753 } 754 if rerr == nil { 755 rerr = e 756 } 757 } 758 759 if !isStdin { 760 return rerr 761 } 762 763 scanner := bufio.NewScanner(os.Stdin) 764 for scanner.Scan() { 765 url := scanner.Text() 766 if isRecursive || withVersions { 767 e = listAndRemove(url, removeOpts{ 768 timeRef: rewind, 769 withVersions: withVersions, 770 nonCurrentVersion: withNoncurrentVersion, 771 isForce: isForce, 772 isRecursive: isRecursive, 773 isIncomplete: isIncomplete, 774 isFake: isFake, 775 isBypass: isBypass, 776 olderThan: olderThan, 777 newerThan: newerThan, 778 }) 779 } else { 780 e = removeSingle(url, versionID, removeOpts{ 781 isIncomplete: isIncomplete, 782 isFake: isFake, 783 isForce: isForce, 784 isForceDel: isForceDel, 785 isBypass: isBypass, 786 olderThan: olderThan, 787 newerThan: newerThan, 788 }) 789 } 790 if rerr == nil { 791 rerr = e 792 } 793 } 794 795 return rerr 796 }