github.com/minio/mc@v0.0.0-20240507152021-646712d5e5fb/cmd/rb-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  	"fmt"
    23  	"path"
    24  	"path/filepath"
    25  	"strings"
    26  
    27  	"github.com/fatih/color"
    28  	"github.com/minio/cli"
    29  	json "github.com/minio/colorjson"
    30  	"github.com/minio/mc/pkg/probe"
    31  	"github.com/minio/minio-go/v7"
    32  	"github.com/minio/pkg/v2/console"
    33  )
    34  
    35  var rbFlags = []cli.Flag{
    36  	cli.BoolFlag{
    37  		Name:  "force",
    38  		Usage: "force a recursive remove operation on all object versions",
    39  	},
    40  	cli.BoolFlag{
    41  		Name:  "dangerous",
    42  		Usage: "allow site-wide removal of objects",
    43  	},
    44  }
    45  
    46  // remove a bucket.
    47  var rbCmd = cli.Command{
    48  	Name:         "rb",
    49  	Usage:        "remove a bucket",
    50  	Action:       mainRemoveBucket,
    51  	OnUsageError: onUsageError,
    52  	Before:       setGlobalsFromContext,
    53  	Flags:        append(rbFlags, globalFlags...),
    54  	CustomHelpTemplate: `NAME:
    55    {{.HelpName}} - {{.Usage}}
    56  
    57  USAGE:
    58    {{.HelpName}} [FLAGS] TARGET [TARGET...]
    59  {{if .VisibleFlags}}
    60  FLAGS:
    61    {{range .VisibleFlags}}{{.}}
    62    {{end}}{{end}}
    63  EXAMPLES:
    64    1. Remove an empty bucket on Amazon S3 cloud storage
    65       {{.Prompt}} {{.HelpName}} s3/mybucket
    66  
    67    2. Remove a directory hierarchy.
    68       {{.Prompt}} {{.HelpName}} /tmp/this/new/dir1
    69  
    70    3. Remove bucket 'jazz-songs' and all its contents
    71       {{.Prompt}} {{.HelpName}} --force s3/jazz-songs
    72  
    73    4. Remove all buckets and objects recursively from S3 host
    74       {{.Prompt}} {{.HelpName}} --force --dangerous s3
    75  `,
    76  }
    77  
    78  // removeBucketMessage is container for delete bucket success and failure messages.
    79  type removeBucketMessage struct {
    80  	Status string `json:"status"`
    81  	Bucket string `json:"bucket"`
    82  }
    83  
    84  // String colorized delete bucket message.
    85  func (s removeBucketMessage) String() string {
    86  	return console.Colorize("RemoveBucket", fmt.Sprintf("Removed `%s` successfully.", s.Bucket))
    87  }
    88  
    89  // JSON jsonified remove bucket message.
    90  func (s removeBucketMessage) JSON() string {
    91  	removeBucketJSONBytes, e := json.Marshal(s)
    92  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
    93  
    94  	return string(removeBucketJSONBytes)
    95  }
    96  
    97  // Validate command line arguments.
    98  func checkRbSyntax(cliCtx *cli.Context) {
    99  	if !cliCtx.Args().Present() {
   100  		exitCode := 1
   101  		showCommandHelpAndExit(cliCtx, exitCode)
   102  	}
   103  	// Set command flags from context.
   104  	isForce := cliCtx.Bool("force")
   105  	isDangerous := cliCtx.Bool("dangerous")
   106  
   107  	for _, url := range cliCtx.Args() {
   108  		if isS3NamespaceRemoval(url) {
   109  			if isForce && isDangerous {
   110  				continue
   111  			}
   112  			fatalIf(errDummy().Trace(),
   113  				"This operation results in **site-wide** removal of buckets. If you are really sure, retry this command with ‘--force’ and ‘--dangerous’ flags.")
   114  		}
   115  	}
   116  }
   117  
   118  // Return a list of aliased urls of buckets under the passed url
   119  func listBucketsURLs(ctx context.Context, url string) ([]string, *probe.Error) {
   120  	var buckets []string
   121  
   122  	targetAlias, targetURL, _ := mustExpandAlias(url)
   123  	clnt, err := newClientFromAlias(targetAlias, targetURL)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	opts := ListOptions{
   129  		ShowDir: DirLast,
   130  	}
   131  
   132  	for content := range clnt.List(ctx, opts) {
   133  		if content.Err != nil {
   134  			errorIf(content.Err, "")
   135  			continue
   136  		}
   137  
   138  		select {
   139  		case <-ctx.Done():
   140  			return nil, probe.NewError(ctx.Err())
   141  		default:
   142  		}
   143  
   144  		bucketName := strings.TrimPrefix(content.URL.Path, clnt.GetURL().Path)
   145  		bucketPath := path.Join(url, bucketName)
   146  		buckets = append(buckets, bucketPath)
   147  	}
   148  
   149  	return buckets, nil
   150  }
   151  
   152  // Delete a bucket and all its objects and versions will be removed as well.
   153  func deleteBucket(ctx context.Context, url string, isForce bool) *probe.Error {
   154  	targetAlias, targetURL, _ := mustExpandAlias(url)
   155  	clnt, pErr := newClientFromAlias(targetAlias, targetURL)
   156  	if pErr != nil {
   157  		return pErr
   158  	}
   159  	contentCh := make(chan *ClientContent)
   160  	resultCh := clnt.Remove(ctx, false, false, false, false, contentCh)
   161  
   162  	go func() {
   163  		defer close(contentCh)
   164  		opts := ListOptions{
   165  			Recursive:         true,
   166  			WithOlderVersions: true,
   167  			WithDeleteMarkers: true,
   168  			ShowDir:           DirLast,
   169  		}
   170  
   171  		for content := range clnt.List(ctx, opts) {
   172  			if content.Err != nil {
   173  				contentCh <- content
   174  				continue
   175  			}
   176  
   177  			urlString := content.URL.Path
   178  
   179  			select {
   180  			case contentCh <- content:
   181  			case <-ctx.Done():
   182  				return
   183  			}
   184  
   185  			// list internally mimics recursive directory listing of object prefixes for s3 similar to FS.
   186  			// The rmMessage needs to be printed only for actual buckets being deleted and not objects.
   187  			tgt := strings.TrimPrefix(urlString, string(filepath.Separator))
   188  			if !strings.Contains(tgt, string(filepath.Separator)) && tgt != targetAlias {
   189  				printMsg(removeBucketMessage{
   190  					Bucket: targetAlias + urlString, Status: "success",
   191  				})
   192  			}
   193  		}
   194  	}()
   195  
   196  	// Give up on the first error.
   197  	for result := range resultCh {
   198  		if result.Err != nil {
   199  			return result.Err.Trace(url)
   200  		}
   201  	}
   202  	// Return early if prefix delete
   203  	switch c := clnt.(type) {
   204  	case *S3Client:
   205  		_, object := c.url2BucketAndObject()
   206  		if object != "" && isForce {
   207  			return nil
   208  		}
   209  	default:
   210  	}
   211  
   212  	// Remove a bucket without force flag first because force
   213  	// won't work if a bucket has some locking rules, that's
   214  	// why we start with regular bucket removal first.
   215  	err := clnt.RemoveBucket(ctx, false)
   216  	if err != nil {
   217  		if isForce && minio.ToErrorResponse(err.ToGoError()).Code == "BucketNotEmpty" {
   218  			return clnt.RemoveBucket(ctx, true)
   219  		}
   220  	}
   221  
   222  	return err
   223  }
   224  
   225  // isS3NamespaceRemoval returns true if alias
   226  // is not qualified by bucket
   227  func isS3NamespaceRemoval(url string) bool {
   228  	// clean path for aliases like s3/.
   229  	// Note: UNC path using / works properly in go 1.9.2 even though it breaks the UNC specification.
   230  	url = filepath.ToSlash(filepath.Clean(url))
   231  	// namespace removal applies only for non FS. So filter out if passed url represents a directory
   232  	_, path := url2Alias(url)
   233  	return (path == "")
   234  }
   235  
   236  // mainRemoveBucket is entry point for rb command.
   237  func mainRemoveBucket(cliCtx *cli.Context) error {
   238  	ctx, cancelRemoveBucket := context.WithCancel(globalContext)
   239  	defer cancelRemoveBucket()
   240  
   241  	// check 'rb' cli arguments.
   242  	checkRbSyntax(cliCtx)
   243  	isForce := cliCtx.Bool("force")
   244  
   245  	// Additional command specific theme customization.
   246  	console.SetColor("RemoveBucket", color.New(color.FgGreen, color.Bold))
   247  
   248  	var cErr error
   249  	for _, targetURL := range cliCtx.Args() {
   250  		// Instantiate client for URL.
   251  		clnt, err := newClient(targetURL)
   252  		if err != nil {
   253  			errorIf(err.Trace(targetURL), "Invalid target `"+targetURL+"`.")
   254  			cErr = exitStatus(globalErrorExitStatus)
   255  			continue
   256  		}
   257  		_, err = clnt.Stat(ctx, StatOptions{})
   258  		if err != nil {
   259  			switch err.ToGoError().(type) {
   260  			case BucketNameEmpty:
   261  			default:
   262  				errorIf(err.Trace(targetURL), "Unable to validate target `"+targetURL+"`.")
   263  				cErr = exitStatus(globalErrorExitStatus)
   264  				continue
   265  
   266  			}
   267  		}
   268  
   269  		// Check if the bucket contains any object, version or delete marker.
   270  		isEmpty := true
   271  		opts := ListOptions{
   272  			Recursive:         true,
   273  			ShowDir:           DirNone,
   274  			WithOlderVersions: true,
   275  			WithDeleteMarkers: true,
   276  		}
   277  
   278  		listCtx, listCancel := context.WithCancel(ctx)
   279  		for obj := range clnt.List(listCtx, opts) {
   280  			if obj.Err != nil {
   281  				continue
   282  			}
   283  			isEmpty = false
   284  			break
   285  		}
   286  		listCancel()
   287  
   288  		// For all recursive operations make sure to check for 'force' flag.
   289  		if !isForce && !isEmpty {
   290  			fatalIf(errDummy().Trace(), "`"+targetURL+"` is not empty. Retry this command with ‘--force’ flag if you want to remove `"+targetURL+"` and all its contents")
   291  		}
   292  
   293  		var bucketsURL []string
   294  		if isS3NamespaceRemoval(targetURL) {
   295  			bucketsURL, err = listBucketsURLs(ctx, targetURL)
   296  			fatalIf(err.Trace(targetURL), "Failed to remove `"+targetURL+"`.")
   297  		} else {
   298  			bucketsURL = []string{targetURL}
   299  		}
   300  
   301  		for _, bucketURL := range bucketsURL {
   302  			e := deleteBucket(ctx, bucketURL, isForce)
   303  			fatalIf(e.Trace(bucketURL), "Failed to remove `"+bucketURL+"`.")
   304  
   305  			printMsg(removeBucketMessage{
   306  				Bucket: bucketURL, Status: "success",
   307  			})
   308  		}
   309  	}
   310  	return cErr
   311  }