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 }