mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-06-13 23:36:45 +03:00
d321f9efb4
A suspended-versioning DELETE was recorded with createDeleteMarker, which mints a fresh real version id each time, so repeated suspended deletes piled up delete markers instead of overwriting a single null marker as S3 specifies. Record the suspended delete as a 'null' marker with a fixed file name (v_null) and point the latest-version pointer at it explicitly; putSuspendedVersioningObject's existing null-version cleanup removes it on the next suspended PUT, so the object undeletes cleanly and at most one null marker exists. Enabled-versioning deletes are unchanged (still distinct historical markers). Update TestSuspendedVersioningDeleteBehavior to the AWS-correct counts: one null marker after a suspended delete, and the null marker plus one real marker after a re-enabled delete.
509 lines
19 KiB
Go
509 lines
19 KiB
Go
package s3api
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
stats_collect "github.com/seaweedfs/seaweedfs/weed/stats"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
const (
|
|
deleteMultipleObjectsLimit = 1000
|
|
)
|
|
|
|
type deleteMutationResult struct {
|
|
versionId string
|
|
deleteMarker bool
|
|
}
|
|
|
|
func deleteErrorFromCode(code s3err.ErrorCode, key, versionId string) DeleteError {
|
|
apiErr := s3err.GetAPIError(code)
|
|
return DeleteError{
|
|
Code: apiErr.Code,
|
|
Message: apiErr.Description,
|
|
Key: key,
|
|
VersionId: versionId,
|
|
}
|
|
}
|
|
|
|
// isMissingDeleteConditionTarget normalizes missing-target detection for conditional deletes.
|
|
// Prefer errors.Is(err, filer_pb.ErrNotFound) and errors.Is(err, ErrDeleteMarker); keep the
|
|
// string-based fallback only as a defensive bridge for filer paths that still return plain text.
|
|
func isMissingDeleteConditionTarget(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
if errors.Is(err, filer_pb.ErrNotFound) || errors.Is(err, ErrDeleteMarker) {
|
|
return true
|
|
}
|
|
|
|
lowerErr := strings.ToLower(err.Error())
|
|
return strings.Contains(lowerErr, "not found")
|
|
}
|
|
|
|
func (s3a *S3ApiServer) resolveDeleteConditionalEntry(bucket, object, versionId, versioningState string) (*filer_pb.Entry, error) {
|
|
normalizedObject := s3_constants.NormalizeObjectKey(object)
|
|
bucketDir := s3a.bucketDir(bucket)
|
|
|
|
if versionId != "" {
|
|
if versionId == "null" {
|
|
return s3a.getEntry(bucketDir, normalizedObject)
|
|
}
|
|
return s3a.getEntry(s3a.getVersionedObjectDir(bucket, normalizedObject), s3a.getVersionFileName(versionId))
|
|
}
|
|
|
|
switch versioningState {
|
|
case s3_constants.VersioningEnabled:
|
|
entry, err := s3a.getLatestObjectVersion(bucket, normalizedObject)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return normalizeConditionalTargetEntry(entry), nil
|
|
default:
|
|
entry, err := s3a.resolveObjectEntry(bucket, normalizedObject)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return normalizeConditionalTargetEntry(entry), nil
|
|
}
|
|
}
|
|
|
|
func (s3a *S3ApiServer) validateDeleteIfMatch(entry *filer_pb.Entry, ifMatch string, missingCode s3err.ErrorCode) s3err.ErrorCode {
|
|
if ifMatch == "" {
|
|
return s3err.ErrNone
|
|
}
|
|
if entry == nil {
|
|
return missingCode
|
|
}
|
|
if ifMatch == "*" {
|
|
return s3err.ErrNone
|
|
}
|
|
if !s3a.etagMatches(ifMatch, s3a.getObjectETag(entry)) {
|
|
return s3err.ErrPreconditionFailed
|
|
}
|
|
return s3err.ErrNone
|
|
}
|
|
|
|
func (s3a *S3ApiServer) checkDeleteIfMatch(bucket, object, versionId, versioningState, ifMatch string, missingCode s3err.ErrorCode) s3err.ErrorCode {
|
|
if ifMatch == "" {
|
|
return s3err.ErrNone
|
|
}
|
|
|
|
entry, err := s3a.resolveDeleteConditionalEntry(bucket, object, versionId, versioningState)
|
|
if err != nil {
|
|
if isMissingDeleteConditionTarget(err) {
|
|
return missingCode
|
|
}
|
|
glog.Errorf("checkDeleteIfMatch: failed to resolve %s/%s (versionId=%s): %v", bucket, object, versionId, err)
|
|
return s3err.ErrInternalError
|
|
}
|
|
|
|
return s3a.validateDeleteIfMatch(entry, ifMatch, missingCode)
|
|
}
|
|
|
|
func (s3a *S3ApiServer) deleteVersionedObject(r *http.Request, bucket, object, versionId, versioningState string) (deleteMutationResult, s3err.ErrorCode) {
|
|
var result deleteMutationResult
|
|
|
|
switch {
|
|
case versionId != "":
|
|
versionEntry, versionLookupErr := s3a.getSpecificObjectVersion(bucket, object, versionId)
|
|
if versionLookupErr == nil && versionEntry != nil && versionEntry.Extended != nil {
|
|
if deleteMarker, ok := versionEntry.Extended[s3_constants.ExtDeleteMarkerKey]; ok && string(deleteMarker) == "true" {
|
|
result.deleteMarker = true
|
|
}
|
|
}
|
|
governanceBypassAllowed := s3a.evaluateGovernanceBypassRequest(r, bucket, object)
|
|
if err := s3a.enforceObjectLockProtections(r, bucket, object, versionId, governanceBypassAllowed); err != nil {
|
|
glog.V(2).Infof("deleteVersionedObject: object lock check failed for %s/%s version %s: %v", bucket, object, versionId, err)
|
|
return result, s3err.ErrAccessDenied
|
|
}
|
|
if err := s3a.deleteSpecificObjectVersion(r.Context(), bucket, object, versionId, false); err != nil {
|
|
glog.Errorf("deleteVersionedObject: failed to delete specific version %s for %s/%s: %v", versionId, bucket, object, err)
|
|
return result, s3err.ErrInternalError
|
|
}
|
|
result.versionId = versionId
|
|
return result, s3err.ErrNone
|
|
|
|
case versioningState == s3_constants.VersioningEnabled:
|
|
deleteMarkerVersionId, err := s3a.createDeleteMarker(bucket, object)
|
|
if err != nil {
|
|
glog.Errorf("deleteVersionedObject: failed to create delete marker for %s/%s: %v", bucket, object, err)
|
|
return result, s3err.ErrInternalError
|
|
}
|
|
result.versionId = deleteMarkerVersionId
|
|
result.deleteMarker = true
|
|
return result, s3err.ErrNone
|
|
|
|
case versioningState == s3_constants.VersioningSuspended:
|
|
governanceBypassAllowed := s3a.evaluateGovernanceBypassRequest(r, bucket, object)
|
|
if err := s3a.enforceObjectLockProtections(r, bucket, object, "null", governanceBypassAllowed); err != nil {
|
|
glog.V(2).Infof("deleteVersionedObject: object lock check failed for %s/%s null version: %v", bucket, object, err)
|
|
return result, s3err.ErrAccessDenied
|
|
}
|
|
if err := s3a.deleteSpecificObjectVersion(r.Context(), bucket, object, "null", false); err != nil {
|
|
glog.Errorf("deleteVersionedObject: failed to delete null version for %s/%s: %v", bucket, object, err)
|
|
return result, s3err.ErrInternalError
|
|
}
|
|
// Suspended versioning overwrites the null version with a single null delete
|
|
// marker (S3 spec), so the marker replaces any prior one instead of piling up.
|
|
if err := s3a.createNullDeleteMarker(bucket, object); err != nil {
|
|
glog.Errorf("deleteVersionedObject: failed to create null delete marker for suspended versioning %s/%s: %v", bucket, object, err)
|
|
return result, s3err.ErrInternalError
|
|
}
|
|
result.versionId = "null"
|
|
result.deleteMarker = true
|
|
return result, s3err.ErrNone
|
|
}
|
|
|
|
glog.Errorf("deleteVersionedObject: unsupported versioning state %q for %s/%s", versioningState, bucket, object)
|
|
return result, s3err.ErrInternalError
|
|
}
|
|
|
|
// deleteUnversionedObjectWithClient removes the bare object entry. When
|
|
// metadataOnly is true the filer skips per-chunk DeleteFile RPCs and
|
|
// relies on the volume's natural TTL to reclaim chunks; pass true only
|
|
// when the entry's Attributes.TtlSec > 0 so the volume is guaranteed to
|
|
// drop the chunks on its own.
|
|
func (s3a *S3ApiServer) deleteUnversionedObjectWithClient(client filer_pb.SeaweedFilerClient, bucket, object string, metadataOnly bool) error {
|
|
target := util.NewFullPath(s3a.bucketDir(bucket), object)
|
|
dir, name := target.DirAndName()
|
|
return deleteObjectEntry(client, dir, name, !metadataOnly, false)
|
|
}
|
|
|
|
func (s3a *S3ApiServer) DeleteObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
bucket, object := s3_constants.GetBucketAndObject(r)
|
|
glog.Infof("DeleteObjectHandler %s %s", bucket, object)
|
|
if err := s3a.validateTableBucketObjectPath(bucket, object); err != nil {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
|
return
|
|
}
|
|
|
|
// Check for specific version ID in query parameters
|
|
versionId := r.URL.Query().Get("versionId")
|
|
|
|
// Get detailed versioning state for proper handling of suspended vs enabled versioning
|
|
versioningState, err := s3a.getVersioningState(bucket)
|
|
if err != nil {
|
|
if err == filer_pb.ErrNotFound {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
|
return
|
|
}
|
|
glog.Errorf("Error checking versioning status for bucket %s: %v", bucket, err)
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
return
|
|
}
|
|
|
|
versioningConfigured := (versioningState != "")
|
|
|
|
var auditLog *s3err.AccessLog
|
|
if s3err.Logger != nil {
|
|
auditLog = s3err.GetAccessLog(r, http.StatusNoContent, s3err.ErrNone)
|
|
}
|
|
|
|
if ifMatchResult := s3a.checkDeleteIfMatch(bucket, object, versionId, versioningState, r.Header.Get(s3_constants.IfMatch), s3err.ErrPreconditionFailed); ifMatchResult != s3err.ErrNone {
|
|
s3err.WriteErrorResponse(w, r, ifMatchResult)
|
|
return
|
|
}
|
|
|
|
var deleteResult deleteMutationResult
|
|
var deleteCode s3err.ErrorCode
|
|
|
|
// Fast path: route the delete to the owner filer under its per-path lock;
|
|
// routedObjectOwner excludes versioned/object-lock buckets.
|
|
deleteHandled := false
|
|
if !versioningConfigured {
|
|
if cond, condOk := buildDeleteCondition(r); condOk {
|
|
if owner, ownerOk := s3a.routedObjectOwner(bucket, object); ownerOk {
|
|
resp, err := s3a.routedDelete(owner, bucket, object, cond)
|
|
switch {
|
|
case err != nil:
|
|
glog.Warningf("DeleteObjectHandler: routed delete to %s failed for %s/%s, falling back to lock: %v", owner, bucket, object, err)
|
|
case resp.ErrorCode == filer_pb.FilerError_PRECONDITION_FAILED:
|
|
deleteCode, deleteHandled = s3err.ErrPreconditionFailed, true
|
|
case resp.Error != "":
|
|
// Non-precondition error: fall back (the lock path handles cases
|
|
// the raw delete cannot, e.g. a non-empty directory marker).
|
|
glog.Warningf("DeleteObjectHandler: routed delete to %s returned %q for %s/%s, falling back to lock", owner, resp.Error, bucket, object)
|
|
default:
|
|
deleteCode, deleteHandled = s3err.ErrNone, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Versioned/suspended delete with no specific version: route off the lock when
|
|
// the bucket has an owner. createDeleteMarker routes its own pointer flip; a
|
|
// delete marker never removes a locked version, so object-lock buckets route
|
|
// here too. The If-Match precondition was already checked above.
|
|
if !deleteHandled && versioningConfigured && versionId == "" {
|
|
if owner := s3a.routableWriteOwner(bucket, object); owner != "" {
|
|
deleteResult, deleteCode = s3a.deleteVersionedObject(r, bucket, object, versionId, versioningState)
|
|
deleteHandled = true
|
|
}
|
|
}
|
|
// Specific-version delete: route off the lock. A real version recomputes the
|
|
// .versions pointer excluding it and deletes the version file; the null
|
|
// version is the regular object entry, deleted directly. Object-lock buckets
|
|
// gate the delete on the version's WORM guards, evaluated on the owner — for
|
|
// governance bypass the retention guard is scoped to COMPLIANCE so the filer
|
|
// allows a governance-mode delete while still denying compliance and legal
|
|
// hold, without the gateway reading the version.
|
|
if !deleteHandled && versionId != "" {
|
|
worm, lockErr := s3a.isObjectLockEnabled(bucket)
|
|
bypass := worm && s3a.evaluateGovernanceBypassRequest(r, bucket, object)
|
|
if lockErr == nil {
|
|
if owner := s3a.objectWriteOwner(bucket, object); owner != "" {
|
|
deleteResult.versionId = versionId
|
|
if ve, vErr := s3a.getSpecificObjectVersion(bucket, object, versionId); vErr == nil && ve != nil && ve.Extended != nil {
|
|
if dm, ok := ve.Extended[s3_constants.ExtDeleteMarkerKey]; ok && string(dm) == "true" {
|
|
deleteResult.deleteMarker = true
|
|
}
|
|
}
|
|
if versionId == "null" {
|
|
deleteCode = s3a.routedDeleteNullVersion(owner, bucket, object, worm, bypass)
|
|
} else {
|
|
deleteCode = s3a.routedDeleteSpecificVersion(owner, bucket, object, versionId, worm, bypass)
|
|
}
|
|
deleteHandled = true
|
|
}
|
|
}
|
|
}
|
|
if !deleteHandled {
|
|
deleteCode = s3a.withObjectWriteLock(bucket, object, func() s3err.ErrorCode {
|
|
return s3a.checkDeleteIfMatch(bucket, object, versionId, versioningState, r.Header.Get(s3_constants.IfMatch), s3err.ErrPreconditionFailed)
|
|
}, func() s3err.ErrorCode {
|
|
if versioningConfigured {
|
|
result, errCode := s3a.deleteVersionedObject(r, bucket, object, versionId, versioningState)
|
|
if errCode != s3err.ErrNone {
|
|
return errCode
|
|
}
|
|
deleteResult = result
|
|
return s3err.ErrNone
|
|
}
|
|
|
|
governanceBypassAllowed := s3a.evaluateGovernanceBypassRequest(r, bucket, object)
|
|
if err := s3a.enforceObjectLockProtections(r, bucket, object, "", governanceBypassAllowed); err != nil {
|
|
glog.V(2).Infof("DeleteObjectHandler: object lock check failed for %s/%s: %v", bucket, object, err)
|
|
return s3err.ErrAccessDenied
|
|
}
|
|
|
|
if err := s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
return s3a.deleteUnversionedObjectWithClient(client, bucket, object, false)
|
|
}); err != nil {
|
|
glog.Errorf("DeleteObjectHandler: failed to delete %s/%s: %v", bucket, object, err)
|
|
return s3err.ErrInternalError
|
|
}
|
|
|
|
return s3err.ErrNone
|
|
})
|
|
}
|
|
if deleteCode != s3err.ErrNone {
|
|
s3err.WriteErrorResponse(w, r, deleteCode)
|
|
return
|
|
}
|
|
|
|
if deleteResult.versionId != "" {
|
|
w.Header().Set("x-amz-version-id", deleteResult.versionId)
|
|
}
|
|
if deleteResult.deleteMarker {
|
|
w.Header().Set("x-amz-delete-marker", "true")
|
|
}
|
|
|
|
if auditLog != nil {
|
|
auditLog.Key = strings.TrimPrefix(object, "/")
|
|
s3err.PostAccessLog(*auditLog)
|
|
s3err.MarkAuditLogged(r)
|
|
}
|
|
|
|
stats_collect.RecordBucketActiveTime(bucket)
|
|
stats_collect.S3DeletedObjectsCounter.WithLabelValues(bucket).Inc()
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// ObjectIdentifier represents an object to be deleted with its key name and optional version ID.
|
|
type ObjectIdentifier struct {
|
|
Key string `xml:"Key"`
|
|
VersionId string `xml:"VersionId,omitempty"`
|
|
ETag string `xml:"ETag,omitempty"`
|
|
DeleteMarker bool `xml:"DeleteMarker,omitempty"`
|
|
DeleteMarkerVersionId string `xml:"DeleteMarkerVersionId,omitempty"`
|
|
}
|
|
|
|
// DeleteObjectsRequest - xml carrying the object key names which needs to be deleted.
|
|
type DeleteObjectsRequest struct {
|
|
// Element to enable quiet mode for the request
|
|
Quiet bool
|
|
// List of objects to be deleted
|
|
Objects []ObjectIdentifier `xml:"Object"`
|
|
}
|
|
|
|
// DeleteError structure.
|
|
type DeleteError struct {
|
|
Code string `xml:"Code"`
|
|
Message string `xml:"Message"`
|
|
Key string `xml:"Key"`
|
|
VersionId string `xml:"VersionId,omitempty"`
|
|
}
|
|
|
|
// DeleteObjectsResponse container for multiple object deletes.
|
|
type DeleteObjectsResponse struct {
|
|
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ DeleteResult" json:"-"`
|
|
|
|
// Collection of all deleted objects
|
|
DeletedObjects []ObjectIdentifier `xml:"Deleted,omitempty"`
|
|
|
|
// Collection of errors deleting certain objects.
|
|
Errors []DeleteError `xml:"Error,omitempty"`
|
|
}
|
|
|
|
// DeleteMultipleObjectsHandler - Delete multiple objects
|
|
func (s3a *S3ApiServer) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
bucket, _ := s3_constants.GetBucketAndObject(r)
|
|
glog.V(3).Infof("DeleteMultipleObjectsHandler %s", bucket)
|
|
|
|
deleteXMLBytes, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
return
|
|
}
|
|
|
|
deleteObjects := &DeleteObjectsRequest{}
|
|
if err := xml.Unmarshal(deleteXMLBytes, deleteObjects); err != nil {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrMalformedXML)
|
|
return
|
|
}
|
|
|
|
if len(deleteObjects.Objects) > deleteMultipleObjectsLimit {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidMaxDeleteObjects)
|
|
return
|
|
}
|
|
|
|
var deletedObjects []ObjectIdentifier
|
|
var deleteErrors []DeleteError
|
|
var auditLog *s3err.AccessLog
|
|
|
|
if s3err.Logger != nil {
|
|
auditLog = s3err.GetAccessLog(r, http.StatusNoContent, s3err.ErrNone)
|
|
}
|
|
|
|
// Get detailed versioning state for proper handling of suspended vs enabled versioning
|
|
versioningState, err := s3a.getVersioningState(bucket)
|
|
if err != nil {
|
|
if err == filer_pb.ErrNotFound {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
|
return
|
|
}
|
|
glog.Errorf("Error checking versioning status for bucket %s: %v", bucket, err)
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
return
|
|
}
|
|
|
|
versioningConfigured := (versioningState != "")
|
|
deletedCount := 0
|
|
|
|
// Per-key authorization: keys arrive in the body, so the route Auth middleware
|
|
// only authenticated. Authorize each key via AuthorizeBatchDeleteKey below.
|
|
var identity *Identity
|
|
if id := s3_constants.GetIdentityFromContext(r); id != nil {
|
|
identity, _ = id.(*Identity)
|
|
}
|
|
|
|
err = s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
// delete file entries
|
|
for _, object := range deleteObjects.Objects {
|
|
if object.Key == "" {
|
|
continue
|
|
}
|
|
if err := s3a.validateTableBucketObjectPath(bucket, object.Key); err != nil {
|
|
deleteErrors = append(deleteErrors, deleteErrorFromCode(s3err.ErrAccessDenied, object.Key, object.VersionId))
|
|
continue
|
|
}
|
|
if authErr := s3a.iam.AuthorizeBatchDeleteKey(r, identity, bucket, object.Key, object.VersionId); authErr != s3err.ErrNone {
|
|
deleteErrors = append(deleteErrors, deleteErrorFromCode(authErr, object.Key, object.VersionId))
|
|
continue
|
|
}
|
|
|
|
var deleteResult deleteMutationResult
|
|
deleteCode := s3a.withObjectWriteLock(bucket, object.Key, func() s3err.ErrorCode {
|
|
return s3a.checkDeleteIfMatch(bucket, object.Key, object.VersionId, versioningState, object.ETag, s3err.ErrNoSuchKey)
|
|
}, func() s3err.ErrorCode {
|
|
if versioningConfigured {
|
|
result, errCode := s3a.deleteVersionedObject(r, bucket, object.Key, object.VersionId, versioningState)
|
|
if errCode != s3err.ErrNone {
|
|
return errCode
|
|
}
|
|
deleteResult = result
|
|
return s3err.ErrNone
|
|
}
|
|
|
|
governanceBypassAllowed := s3a.evaluateGovernanceBypassRequest(r, bucket, object.Key)
|
|
if err := s3a.enforceObjectLockProtections(r, bucket, object.Key, "", governanceBypassAllowed); err != nil {
|
|
glog.V(2).Infof("DeleteMultipleObjectsHandler: object lock check failed for %s/%s: %v", bucket, object.Key, err)
|
|
return s3err.ErrAccessDenied
|
|
}
|
|
|
|
if err := s3a.deleteUnversionedObjectWithClient(client, bucket, object.Key, false); err != nil {
|
|
glog.Errorf("DeleteMultipleObjectsHandler: failed to delete %s/%s: %v", bucket, object.Key, err)
|
|
return s3err.ErrInternalError
|
|
}
|
|
|
|
return s3err.ErrNone
|
|
})
|
|
if deleteCode != s3err.ErrNone {
|
|
deleteErrors = append(deleteErrors, deleteErrorFromCode(deleteCode, object.Key, object.VersionId))
|
|
continue
|
|
}
|
|
|
|
deletedCount++
|
|
if !deleteObjects.Quiet {
|
|
deletedObject := ObjectIdentifier{
|
|
Key: object.Key,
|
|
VersionId: deleteResult.versionId,
|
|
}
|
|
if deleteResult.deleteMarker {
|
|
deletedObject.DeleteMarker = true
|
|
deletedObject.DeleteMarkerVersionId = deleteResult.versionId
|
|
deletedObject.VersionId = ""
|
|
}
|
|
deletedObjects = append(deletedObjects, deletedObject)
|
|
}
|
|
|
|
if auditLog != nil {
|
|
auditLog.Key = object.Key
|
|
s3err.PostAccessLog(*auditLog)
|
|
s3err.MarkAuditLogged(r)
|
|
}
|
|
}
|
|
|
|
// Note: Empty folder cleanup is now handled asynchronously by EmptyFolderCleaner
|
|
// which listens to metadata events and uses consistent hashing for coordination
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
glog.Errorf("DeleteMultipleObjectsHandler: failed to initialize filer client for bucket %s: %v", bucket, err)
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
return
|
|
}
|
|
|
|
deleteResp := DeleteObjectsResponse{}
|
|
if !deleteObjects.Quiet {
|
|
deleteResp.DeletedObjects = deletedObjects
|
|
}
|
|
deleteResp.Errors = deleteErrors
|
|
stats_collect.RecordBucketActiveTime(bucket)
|
|
stats_collect.S3DeletedObjectsCounter.WithLabelValues(bucket).Add(float64(deletedCount))
|
|
|
|
writeSuccessResponseXML(w, r, deleteResp)
|
|
|
|
}
|