mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-17 12:46:59 +00:00
* s3: delete a directory marker instead of versioning it The key "dir/" is stored as the filer directory itself, so a delete marker cannot stand in for it without hiding the children underneath, and its history has to sit inside the directory it describes, where listings keep meeting it. Delete it the way an unversioned bucket already does: remove the directory when nothing is left under it, demote it to a plain directory when children remain, and drop a history an older build recorded for it. * s3: stop resolving directory markers through a version history Nothing records one for them any more, so the lookups that read it are dead weight - and the one in the listing was a filer round trip per directory marker returned, which for a bucket that keeps a marker per directory is the whole listing cost. A listing reads what a directory stands for straight off the entry it already has; a unit test pins that N markers cost one ListEntries rather than N+1. The guard that keeps a history left inside a directory by an older build from surfacing as a key named after it stays. * s3: do not let deleting "dir/" destroy the object at "dir" Writing under an existing object turns that object's entry into a directory while it keeps its data, so the keys "m2" and "m2/" end up sharing one entry. Stripping the entry to delete "m2/" therefore wiped the object at "m2" - a different key, and in a versioned bucket one no delete marker records. Leave a directory holding uploaded data alone; "m2/" does not name it. * s3: make the directory-marker delete fail closed and take the write lock The guard that spares a promoted file only fired when the entry read succeeded, so a transient filer error fell through to the delete and could destroy the object at "dir" anyway. Fail the request instead, take the object write lock so the entry cannot change between the check and the delete, and report a stale history that cannot be removed rather than leaving it to keep naming the key in ListObjectVersions. * s3: check If-Match inside the directory-marker delete lock The lock belongs to the caller: taking it inside the delete nested it under the batch handler's own lock, and since every lock from a gateway shares one owner the inner release would have freed it while the outer caller still assumed it held it. Both callers now own the lock, the single-object path re-checks If-Match inside it the way the other delete paths do, and a batch delete of a trailing-slash key in an unversioned bucket goes through the same marker path instead of the raw delete. A history lookup that fails now fails the delete.
548 lines
20 KiB
Go
548 lines
20 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,
|
|
}
|
|
}
|
|
|
|
func validateDeleteObjectIdentifier(object ObjectIdentifier) s3err.ErrorCode {
|
|
if !s3_constants.IsValidObjectKey(object.Key) || !isValidVersionID(object.VersionId) {
|
|
return s3err.ErrInvalidRequest
|
|
}
|
|
return s3err.ErrNone
|
|
}
|
|
|
|
// 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) {
|
|
if !isValidVersionID(versionId) {
|
|
return nil, errInvalidVersionID
|
|
}
|
|
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
|
|
|
|
// The key "dir/" is the filer directory itself, which a delete marker cannot stand
|
|
// in for without hiding the children underneath it. It is not a versioned object,
|
|
// so it is deleted the way an unversioned bucket deletes it.
|
|
if versionId == "" && strings.HasSuffix(object, "/") {
|
|
return result, s3a.deleteDirectoryMarker(bucket, object)
|
|
}
|
|
|
|
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 {
|
|
if !s3_constants.IsValidBucketName(bucket) || !s3_constants.IsValidObjectKey(object) {
|
|
return errors.New("invalid bucket or object path")
|
|
}
|
|
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
|
|
|
|
// A trailing-slash key is a directory marker in every bucket, versioned or not, and
|
|
// is deleted the same way: the raw delete below cannot handle a directory that still
|
|
// has children, and versioning has nothing to add to a key that is not an object.
|
|
deleteHandled := false
|
|
if versionId == "" && strings.HasSuffix(object, "/") {
|
|
deleteCode, deleteHandled = 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 {
|
|
return s3a.deleteDirectoryMarker(bucket, object)
|
|
}), true
|
|
}
|
|
|
|
// Fast path: route the delete to the owner filer under its per-path lock;
|
|
// routedObjectOwner excludes versioned/object-lock buckets.
|
|
if !deleteHandled && !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 validationCode := validateDeleteObjectIdentifier(object); validationCode != s3err.ErrNone {
|
|
deleteErrors = append(deleteErrors, deleteErrorFromCode(validationCode, object.Key, object.VersionId))
|
|
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 strings.HasSuffix(object.Key, "/") {
|
|
return s3a.deleteDirectoryMarker(bucket, object.Key)
|
|
}
|
|
|
|
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)
|
|
|
|
}
|