api-remove.go (15928B)
1 /* 2 * MinIO Go Library for Amazon S3 Compatible Cloud Storage 3 * Copyright 2015-2020 MinIO, Inc. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package minio 19 20 import ( 21 "bytes" 22 "context" 23 "encoding/xml" 24 "io" 25 "net/http" 26 "net/url" 27 "time" 28 29 "github.com/minio/minio-go/v7/pkg/s3utils" 30 ) 31 32 //revive:disable 33 34 // Deprecated: BucketOptions will be renamed to RemoveBucketOptions in future versions. 35 type BucketOptions = RemoveBucketOptions 36 37 //revive:enable 38 39 // RemoveBucketOptions special headers to purge buckets, only 40 // useful when endpoint is MinIO 41 type RemoveBucketOptions struct { 42 ForceDelete bool 43 } 44 45 // RemoveBucketWithOptions deletes the bucket name. 46 // 47 // All objects (including all object versions and delete markers) 48 // in the bucket will be deleted forcibly if bucket options set 49 // ForceDelete to 'true'. 50 func (c *Client) RemoveBucketWithOptions(ctx context.Context, bucketName string, opts RemoveBucketOptions) error { 51 // Input validation. 52 if err := s3utils.CheckValidBucketName(bucketName); err != nil { 53 return err 54 } 55 56 // Build headers. 57 headers := make(http.Header) 58 if opts.ForceDelete { 59 headers.Set(minIOForceDelete, "true") 60 } 61 62 // Execute DELETE on bucket. 63 resp, err := c.executeMethod(ctx, http.MethodDelete, requestMetadata{ 64 bucketName: bucketName, 65 contentSHA256Hex: emptySHA256Hex, 66 customHeader: headers, 67 }) 68 defer closeResponse(resp) 69 if err != nil { 70 return err 71 } 72 if resp != nil { 73 if resp.StatusCode != http.StatusNoContent { 74 return httpRespToErrorResponse(resp, bucketName, "") 75 } 76 } 77 78 // Remove the location from cache on a successful delete. 79 c.bucketLocCache.Delete(bucketName) 80 return nil 81 } 82 83 // RemoveBucket deletes the bucket name. 84 // 85 // All objects (including all object versions and delete markers). 86 // in the bucket must be deleted before successfully attempting this request. 87 func (c *Client) RemoveBucket(ctx context.Context, bucketName string) error { 88 // Input validation. 89 if err := s3utils.CheckValidBucketName(bucketName); err != nil { 90 return err 91 } 92 // Execute DELETE on bucket. 93 resp, err := c.executeMethod(ctx, http.MethodDelete, requestMetadata{ 94 bucketName: bucketName, 95 contentSHA256Hex: emptySHA256Hex, 96 }) 97 defer closeResponse(resp) 98 if err != nil { 99 return err 100 } 101 if resp != nil { 102 if resp.StatusCode != http.StatusNoContent { 103 return httpRespToErrorResponse(resp, bucketName, "") 104 } 105 } 106 107 // Remove the location from cache on a successful delete. 108 c.bucketLocCache.Delete(bucketName) 109 110 return nil 111 } 112 113 // AdvancedRemoveOptions intended for internal use by replication 114 type AdvancedRemoveOptions struct { 115 ReplicationDeleteMarker bool 116 ReplicationStatus ReplicationStatus 117 ReplicationMTime time.Time 118 ReplicationRequest bool 119 } 120 121 // RemoveObjectOptions represents options specified by user for RemoveObject call 122 type RemoveObjectOptions struct { 123 ForceDelete bool 124 GovernanceBypass bool 125 VersionID string 126 Internal AdvancedRemoveOptions 127 } 128 129 // RemoveObject removes an object from a bucket. 130 func (c *Client) RemoveObject(ctx context.Context, bucketName, objectName string, opts RemoveObjectOptions) error { 131 // Input validation. 132 if err := s3utils.CheckValidBucketName(bucketName); err != nil { 133 return err 134 } 135 if err := s3utils.CheckValidObjectName(objectName); err != nil { 136 return err 137 } 138 139 res := c.removeObject(ctx, bucketName, objectName, opts) 140 return res.Err 141 } 142 143 func (c *Client) removeObject(ctx context.Context, bucketName, objectName string, opts RemoveObjectOptions) RemoveObjectResult { 144 // Get resources properly escaped and lined up before 145 // using them in http request. 146 urlValues := make(url.Values) 147 148 if opts.VersionID != "" { 149 urlValues.Set("versionId", opts.VersionID) 150 } 151 152 // Build headers. 153 headers := make(http.Header) 154 155 if opts.GovernanceBypass { 156 // Set the bypass goverenance retention header 157 headers.Set(amzBypassGovernance, "true") 158 } 159 if opts.Internal.ReplicationDeleteMarker { 160 headers.Set(minIOBucketReplicationDeleteMarker, "true") 161 } 162 if !opts.Internal.ReplicationMTime.IsZero() { 163 headers.Set(minIOBucketSourceMTime, opts.Internal.ReplicationMTime.Format(time.RFC3339Nano)) 164 } 165 if !opts.Internal.ReplicationStatus.Empty() { 166 headers.Set(amzBucketReplicationStatus, string(opts.Internal.ReplicationStatus)) 167 } 168 if opts.Internal.ReplicationRequest { 169 headers.Set(minIOBucketReplicationRequest, "true") 170 } 171 if opts.ForceDelete { 172 headers.Set(minIOForceDelete, "true") 173 } 174 // Execute DELETE on objectName. 175 resp, err := c.executeMethod(ctx, http.MethodDelete, requestMetadata{ 176 bucketName: bucketName, 177 objectName: objectName, 178 contentSHA256Hex: emptySHA256Hex, 179 queryValues: urlValues, 180 customHeader: headers, 181 }) 182 defer closeResponse(resp) 183 if err != nil { 184 return RemoveObjectResult{Err: err} 185 } 186 if resp != nil { 187 // if some unexpected error happened and max retry is reached, we want to let client know 188 if resp.StatusCode != http.StatusNoContent { 189 err := httpRespToErrorResponse(resp, bucketName, objectName) 190 return RemoveObjectResult{Err: err} 191 } 192 } 193 194 // DeleteObject always responds with http '204' even for 195 // objects which do not exist. So no need to handle them 196 // specifically. 197 return RemoveObjectResult{ 198 ObjectName: objectName, 199 ObjectVersionID: opts.VersionID, 200 DeleteMarker: resp.Header.Get("x-amz-delete-marker") == "true", 201 DeleteMarkerVersionID: resp.Header.Get("x-amz-version-id"), 202 } 203 } 204 205 // RemoveObjectError - container of Multi Delete S3 API error 206 type RemoveObjectError struct { 207 ObjectName string 208 VersionID string 209 Err error 210 } 211 212 // RemoveObjectResult - container of Multi Delete S3 API result 213 type RemoveObjectResult struct { 214 ObjectName string 215 ObjectVersionID string 216 217 DeleteMarker bool 218 DeleteMarkerVersionID string 219 220 Err error 221 } 222 223 // generateRemoveMultiObjects - generate the XML request for remove multi objects request 224 func generateRemoveMultiObjectsRequest(objects []ObjectInfo) []byte { 225 delObjects := []deleteObject{} 226 for _, obj := range objects { 227 delObjects = append(delObjects, deleteObject{ 228 Key: obj.Key, 229 VersionID: obj.VersionID, 230 }) 231 } 232 xmlBytes, _ := xml.Marshal(deleteMultiObjects{Objects: delObjects, Quiet: false}) 233 return xmlBytes 234 } 235 236 // processRemoveMultiObjectsResponse - parse the remove multi objects web service 237 // and return the success/failure result status for each object 238 func processRemoveMultiObjectsResponse(body io.Reader, resultCh chan<- RemoveObjectResult) { 239 // Parse multi delete XML response 240 rmResult := &deleteMultiObjectsResult{} 241 err := xmlDecoder(body, rmResult) 242 if err != nil { 243 resultCh <- RemoveObjectResult{ObjectName: "", Err: err} 244 return 245 } 246 247 // Fill deletion that returned success 248 for _, obj := range rmResult.DeletedObjects { 249 resultCh <- RemoveObjectResult{ 250 ObjectName: obj.Key, 251 // Only filled with versioned buckets 252 ObjectVersionID: obj.VersionID, 253 DeleteMarker: obj.DeleteMarker, 254 DeleteMarkerVersionID: obj.DeleteMarkerVersionID, 255 } 256 } 257 258 // Fill deletion that returned an error. 259 for _, obj := range rmResult.UnDeletedObjects { 260 // Version does not exist is not an error ignore and continue. 261 switch obj.Code { 262 case "InvalidArgument", "NoSuchVersion": 263 continue 264 } 265 resultCh <- RemoveObjectResult{ 266 ObjectName: obj.Key, 267 ObjectVersionID: obj.VersionID, 268 Err: ErrorResponse{ 269 Code: obj.Code, 270 Message: obj.Message, 271 }, 272 } 273 } 274 } 275 276 // RemoveObjectsOptions represents options specified by user for RemoveObjects call 277 type RemoveObjectsOptions struct { 278 GovernanceBypass bool 279 } 280 281 // RemoveObjects removes multiple objects from a bucket while 282 // it is possible to specify objects versions which are received from 283 // objectsCh. Remove failures are sent back via error channel. 284 func (c *Client) RemoveObjects(ctx context.Context, bucketName string, objectsCh <-chan ObjectInfo, opts RemoveObjectsOptions) <-chan RemoveObjectError { 285 errorCh := make(chan RemoveObjectError, 1) 286 287 // Validate if bucket name is valid. 288 if err := s3utils.CheckValidBucketName(bucketName); err != nil { 289 defer close(errorCh) 290 errorCh <- RemoveObjectError{ 291 Err: err, 292 } 293 return errorCh 294 } 295 // Validate objects channel to be properly allocated. 296 if objectsCh == nil { 297 defer close(errorCh) 298 errorCh <- RemoveObjectError{ 299 Err: errInvalidArgument("Objects channel cannot be nil"), 300 } 301 return errorCh 302 } 303 304 resultCh := make(chan RemoveObjectResult, 1) 305 go c.removeObjects(ctx, bucketName, objectsCh, resultCh, opts) 306 go func() { 307 defer close(errorCh) 308 for res := range resultCh { 309 // Send only errors to the error channel 310 if res.Err == nil { 311 continue 312 } 313 errorCh <- RemoveObjectError{ 314 ObjectName: res.ObjectName, 315 VersionID: res.ObjectVersionID, 316 Err: res.Err, 317 } 318 } 319 }() 320 321 return errorCh 322 } 323 324 // RemoveObjectsWithResult removes multiple objects from a bucket while 325 // it is possible to specify objects versions which are received from 326 // objectsCh. Remove results, successes and failures are sent back via 327 // RemoveObjectResult channel 328 func (c *Client) RemoveObjectsWithResult(ctx context.Context, bucketName string, objectsCh <-chan ObjectInfo, opts RemoveObjectsOptions) <-chan RemoveObjectResult { 329 resultCh := make(chan RemoveObjectResult, 1) 330 331 // Validate if bucket name is valid. 332 if err := s3utils.CheckValidBucketName(bucketName); err != nil { 333 defer close(resultCh) 334 resultCh <- RemoveObjectResult{ 335 Err: err, 336 } 337 return resultCh 338 } 339 // Validate objects channel to be properly allocated. 340 if objectsCh == nil { 341 defer close(resultCh) 342 resultCh <- RemoveObjectResult{ 343 Err: errInvalidArgument("Objects channel cannot be nil"), 344 } 345 return resultCh 346 } 347 348 go c.removeObjects(ctx, bucketName, objectsCh, resultCh, opts) 349 return resultCh 350 } 351 352 // Return true if the character is within the allowed characters in an XML 1.0 document 353 // The list of allowed characters can be found here: https://www.w3.org/TR/xml/#charsets 354 func validXMLChar(r rune) (ok bool) { 355 return r == 0x09 || 356 r == 0x0A || 357 r == 0x0D || 358 r >= 0x20 && r <= 0xD7FF || 359 r >= 0xE000 && r <= 0xFFFD || 360 r >= 0x10000 && r <= 0x10FFFF 361 } 362 363 func hasInvalidXMLChar(str string) bool { 364 for _, s := range str { 365 if !validXMLChar(s) { 366 return true 367 } 368 } 369 return false 370 } 371 372 // Generate and call MultiDelete S3 requests based on entries received from objectsCh 373 func (c *Client) removeObjects(ctx context.Context, bucketName string, objectsCh <-chan ObjectInfo, resultCh chan<- RemoveObjectResult, opts RemoveObjectsOptions) { 374 maxEntries := 1000 375 finish := false 376 urlValues := make(url.Values) 377 urlValues.Set("delete", "") 378 379 // Close result channel when Multi delete finishes. 380 defer close(resultCh) 381 382 // Loop over entries by 1000 and call MultiDelete requests 383 for { 384 if finish { 385 break 386 } 387 count := 0 388 var batch []ObjectInfo 389 390 // Try to gather 1000 entries 391 for object := range objectsCh { 392 if hasInvalidXMLChar(object.Key) { 393 // Use single DELETE so the object name will be in the request URL instead of the multi-delete XML document. 394 removeResult := c.removeObject(ctx, bucketName, object.Key, RemoveObjectOptions{ 395 VersionID: object.VersionID, 396 GovernanceBypass: opts.GovernanceBypass, 397 }) 398 if err := removeResult.Err; err != nil { 399 // Version does not exist is not an error ignore and continue. 400 switch ToErrorResponse(err).Code { 401 case "InvalidArgument", "NoSuchVersion": 402 continue 403 } 404 resultCh <- removeResult 405 } 406 407 resultCh <- removeResult 408 continue 409 } 410 411 batch = append(batch, object) 412 if count++; count >= maxEntries { 413 break 414 } 415 } 416 if count == 0 { 417 // Multi Objects Delete API doesn't accept empty object list, quit immediately 418 break 419 } 420 if count < maxEntries { 421 // We didn't have 1000 entries, so this is the last batch 422 finish = true 423 } 424 425 // Build headers. 426 headers := make(http.Header) 427 if opts.GovernanceBypass { 428 // Set the bypass goverenance retention header 429 headers.Set(amzBypassGovernance, "true") 430 } 431 432 // Generate remove multi objects XML request 433 removeBytes := generateRemoveMultiObjectsRequest(batch) 434 // Execute GET on bucket to list objects. 435 resp, err := c.executeMethod(ctx, http.MethodPost, requestMetadata{ 436 bucketName: bucketName, 437 queryValues: urlValues, 438 contentBody: bytes.NewReader(removeBytes), 439 contentLength: int64(len(removeBytes)), 440 contentMD5Base64: sumMD5Base64(removeBytes), 441 contentSHA256Hex: sum256Hex(removeBytes), 442 customHeader: headers, 443 }) 444 if resp != nil { 445 if resp.StatusCode != http.StatusOK { 446 e := httpRespToErrorResponse(resp, bucketName, "") 447 resultCh <- RemoveObjectResult{ObjectName: "", Err: e} 448 } 449 } 450 if err != nil { 451 for _, b := range batch { 452 resultCh <- RemoveObjectResult{ 453 ObjectName: b.Key, 454 ObjectVersionID: b.VersionID, 455 Err: err, 456 } 457 } 458 continue 459 } 460 461 // Process multiobjects remove xml response 462 processRemoveMultiObjectsResponse(resp.Body, resultCh) 463 464 closeResponse(resp) 465 } 466 } 467 468 // RemoveIncompleteUpload aborts an partially uploaded object. 469 func (c *Client) RemoveIncompleteUpload(ctx context.Context, bucketName, objectName string) error { 470 // Input validation. 471 if err := s3utils.CheckValidBucketName(bucketName); err != nil { 472 return err 473 } 474 if err := s3utils.CheckValidObjectName(objectName); err != nil { 475 return err 476 } 477 // Find multipart upload ids of the object to be aborted. 478 uploadIDs, err := c.findUploadIDs(ctx, bucketName, objectName) 479 if err != nil { 480 return err 481 } 482 483 for _, uploadID := range uploadIDs { 484 // abort incomplete multipart upload, based on the upload id passed. 485 err := c.abortMultipartUpload(ctx, bucketName, objectName, uploadID) 486 if err != nil { 487 return err 488 } 489 } 490 491 return nil 492 } 493 494 // abortMultipartUpload aborts a multipart upload for the given 495 // uploadID, all previously uploaded parts are deleted. 496 func (c *Client) abortMultipartUpload(ctx context.Context, bucketName, objectName, uploadID string) error { 497 // Input validation. 498 if err := s3utils.CheckValidBucketName(bucketName); err != nil { 499 return err 500 } 501 if err := s3utils.CheckValidObjectName(objectName); err != nil { 502 return err 503 } 504 505 // Initialize url queries. 506 urlValues := make(url.Values) 507 urlValues.Set("uploadId", uploadID) 508 509 // Execute DELETE on multipart upload. 510 resp, err := c.executeMethod(ctx, http.MethodDelete, requestMetadata{ 511 bucketName: bucketName, 512 objectName: objectName, 513 queryValues: urlValues, 514 contentSHA256Hex: emptySHA256Hex, 515 }) 516 defer closeResponse(resp) 517 if err != nil { 518 return err 519 } 520 if resp != nil { 521 if resp.StatusCode != http.StatusNoContent { 522 // Abort has no response body, handle it for any errors. 523 var errorResponse ErrorResponse 524 switch resp.StatusCode { 525 case http.StatusNotFound: 526 // This is needed specifically for abort and it cannot 527 // be converged into default case. 528 errorResponse = ErrorResponse{ 529 Code: "NoSuchUpload", 530 Message: "The specified multipart upload does not exist.", 531 BucketName: bucketName, 532 Key: objectName, 533 RequestID: resp.Header.Get("x-amz-request-id"), 534 HostID: resp.Header.Get("x-amz-id-2"), 535 Region: resp.Header.Get("x-amz-bucket-region"), 536 } 537 default: 538 return httpRespToErrorResponse(resp, bucketName, objectName) 539 } 540 return errorResponse 541 } 542 } 543 return nil 544 }