mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-01 13:47:51 +00:00
* filer: keep the sentinel when CreateEntry reports an update failure CreateEntry flattened the error UpdateEntry wraps, so errors.Is stopped matching and ErrExistingIsDirectory and ErrExistingIsFile never reached the S3 mapper, which answered a retryable 500 instead. * s3: let a key that is a prefix of other keys be an object S3 keys are flat, so "a/b" and "a/b/c" are independent objects that coexist in either write order. The filer stores a key as a path, so one of them has to live on the directory the other is nested under. Writing the nested key first refused the prefix key outright. Writing it second promoted the file to a directory, which kept its data but lost the key: an empty object left nothing to recognise it by and disappeared, and one with data listed under a trailing slash it never had. Mark the directory that carries such a key, and write the object onto it when the path is already a directory. The mark makes an empty prefix object visible to listings and readable by GET and HEAD, keeps the empty folder cleaner off it, and lists it under the key it was written with. Deleting the key strips the mark back off along with the data. * filer: keep a TTL off a directory that stands for an object An expired entry is deleted a row at a time, so expiring a directory removes it and leaves everything under it unreachable. Promoting a file to a directory carried its TTL across, and a promoted file is exactly the one that has keys nested under it. Drop the TTL on promotion, and leave one an older build wrote alone. The lifecycle worker still expires the object, through the delete that leaves the directory behind. * s3: delete the null version of a key other keys are nested under The routed delete cannot remove an entry that other keys live under, and answered a retryable 500 rather than falling back to the lock path the unversioned delete already falls back to. That path then looked the entry up under the bucket with the whole key as its name, so the demote wrote it back one directory too high and failed as not found. Fall back on any non-precondition error, and split the key before deleting it. Trailing-slash directory markers with children reach the same delete. * filer: keep the sentinel when MkFile and Mkdir report a create failure Same flattening one layer out: every mkFile caller lost the sentinel, so a CopyObject onto a key that other keys are nested under answered a retryable 500 where a PutObject of the same key answers 409. * s3: copy and rename a key that other keys are nested under Such a key is stored on the directory those keys live in, and copy and rename both refused it: the source lookup maps every directory entry to NoSuchKey, so a key a plain GET serves could not be copied or moved, and the destination side refused it as a directory conflict. The source is read through a view of the entry as the object it names. The destination is written the way a PutObject of that key writes it. A rename at either end copies the object's own data across and strips it off the source key rather than going through AtomicRenameEntry, which moves a directory by moving everything under it - the nested keys are not part of what is being renamed.
547 lines
20 KiB
Go
547 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, deleteHandled = s3a.routedDeleteNullVersion(owner, bucket, object, worm, bypass)
|
|
} else {
|
|
deleteCode, deleteHandled = s3a.routedDeleteSpecificVersion(owner, bucket, object, versionId, worm, bypass), 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 AuthorizeObjectDelete 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.AuthorizeObjectDelete(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)
|
|
|
|
}
|