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  }