/* * MinIO Client (C) 2019 MinIO, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cmd import ( "encoding/json" "fmt" "path/filepath" "strings" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/console" "github.com/minio/mc/pkg/probe" ) var ( rbFlags = []cli.Flag{ cli.BoolFlag{ Name: "force", Usage: "allow a recursive remove operation", }, cli.BoolFlag{ Name: "dangerous", Usage: "allow site-wide removal of objects", }, } ) // remove a bucket. var rbCmd = cli.Command{ Name: "rb", Usage: "remove a bucket", Action: mainRemoveBucket, Before: setGlobalsFromContext, Flags: append(rbFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET [TARGET...] {{if .VisibleFlags}} FLAGS: {{range .VisibleFlags}}{{.}} {{end}}{{end}} EXAMPLES: 1. Remove an empty bucket on Amazon S3 cloud storage $ {{.HelpName}} s3/mybucket 2. Remove a directory hierarchy. $ {{.HelpName}} /tmp/this/new/dir1 3. Remove bucket 'jazz-songs' and all its contents $ {{.HelpName}} --force s3/jazz-songs 4. Remove all buckets and objects recursively from S3 host $ {{.HelpName}} --force --dangerous s3 `, } // removeBucketMessage is container for delete bucket success and failure messages. type removeBucketMessage struct { Status string `json:"status"` Bucket string `json:"bucket"` } // String colorized delete bucket message. func (s removeBucketMessage) String() string { return console.Colorize("RemoveBucket", fmt.Sprintf("Removed `%s` successfully.", s.Bucket)) } // JSON jsonified remove bucket message. func (s removeBucketMessage) JSON() string { removeBucketJSONBytes, e := json.Marshal(s) fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(removeBucketJSONBytes) } // Validate command line arguments. func checkRbSyntax(ctx *cli.Context) { if !ctx.Args().Present() { exitCode := 1 cli.ShowCommandHelpAndExit(ctx, "rb", exitCode) } // Set command flags from context. isForce := ctx.Bool("force") isDangerous := ctx.Bool("dangerous") for _, url := range ctx.Args() { if isNamespaceRemoval(url) { if isForce && isDangerous { continue } fatalIf(errDummy().Trace(), "This operation results in **site-wide** removal of buckets. If you are really sure, retry this command with ‘--force’ and ‘--dangerous’ flags.") } } } // deletes a bucket and all its contents func deleteBucket(url string) *probe.Error { targetAlias, targetURL, _ := mustExpandAlias(url) clnt, pErr := newClientFromAlias(targetAlias, targetURL) if pErr != nil { return pErr } var isIncomplete bool isRemoveBucket := true contentCh := make(chan *clientContent) errorCh := clnt.Remove(isIncomplete, isRemoveBucket, contentCh) for content := range clnt.List(true, false, DirLast) { if content.Err != nil { switch content.Err.ToGoError().(type) { case PathInsufficientPermission: errorIf(content.Err.Trace(url), "Failed to remove `"+url+"`.") // Ignore Permission error. continue } close(contentCh) return content.Err } urlString := content.URL.Path sent := false for !sent { select { case contentCh <- content: sent = true case pErr := <-errorCh: switch pErr.ToGoError().(type) { case PathInsufficientPermission: errorIf(pErr.Trace(urlString), "Failed to remove `"+urlString+"`.") // Ignore Permission error. continue } close(contentCh) return pErr } } // list internally mimics recursive directory listing of object prefixes for s3 similar to FS. // The rmMessage needs to be printed only for actual buckets being deleted and not objects. tgt := strings.TrimPrefix(urlString, string(filepath.Separator)) if !strings.Contains(tgt, string(filepath.Separator)) && tgt != targetAlias { printMsg(removeBucketMessage{ Bucket: targetAlias + urlString, Status: "success", }) } } // Remove the given url since the user will always want to remove it. alias, _ := url2Alias(targetURL) if alias != "" { contentCh <- &clientContent{URL: *newClientURL(targetURL)} } // Finish removing and print all the remaining errors close(contentCh) for pErr := range errorCh { switch pErr.ToGoError().(type) { case PathInsufficientPermission: errorIf(pErr.Trace(url), "Failed to remove `"+url+"`.") // Ignore Permission error. continue } return pErr } return nil } // isNamespaceRemoval returns true if alias // is not qualified by bucket func isNamespaceRemoval(url string) bool { // clean path for aliases like s3/. //Note: UNC path using / works properly in go 1.9.2 even though it breaks the UNC specification. url = filepath.ToSlash(filepath.Clean(url)) // namespace removal applies only for non FS. So filter out if passed url represents a directory if !isAliasURLDir(url, nil) { _, path := url2Alias(url) return (path == "") } return false } // mainRemoveBucket is entry point for rb command. func mainRemoveBucket(ctx *cli.Context) error { // check 'rb' cli arguments. checkRbSyntax(ctx) isForce := ctx.Bool("force") // Additional command specific theme customization. console.SetColor("RemoveBucket", color.New(color.FgGreen, color.Bold)) var cErr error for _, targetURL := range ctx.Args() { // Instantiate client for URL. clnt, err := newClient(targetURL) if err != nil { errorIf(err.Trace(targetURL), "Invalid target `"+targetURL+"`.") cErr = exitStatus(globalErrorExitStatus) continue } _, err = clnt.Stat(false, false, nil) if err != nil { switch err.ToGoError().(type) { case BucketNameEmpty: default: errorIf(err.Trace(targetURL), "Unable to validate target `"+targetURL+"`.") cErr = exitStatus(globalErrorExitStatus) continue } } isEmpty := true for range clnt.List(true, false, DirNone) { isEmpty = false break } // For all recursive operations make sure to check for 'force' flag. if !isForce && !isEmpty { fatalIf(errDummy().Trace(), "`"+targetURL+"` is not empty. Retry this command with ‘--force’ flag if you want to remove `"+targetURL+"` and all its contents") } e := deleteBucket(targetURL) fatalIf(e.Trace(targetURL), "Failed to remove `"+targetURL+"`.") if !isNamespaceRemoval(targetURL) { printMsg(removeBucketMessage{ Bucket: targetURL, Status: "success", }) } } return cErr }