aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api/s3api_object_handlers_delete.go
diff options
context:
space:
mode:
authorChris Lu <chrislusf@users.noreply.github.com>2025-07-19 21:43:34 -0700
committerGitHub <noreply@github.com>2025-07-19 21:43:34 -0700
commit12f50d37fa52444a43ad6ff4cc3d156db4035528 (patch)
treef2ea4466b899e18672530238dc7b35b91115e963 /weed/s3api/s3api_object_handlers_delete.go
parent0e4d803896fc9a48a77d0d1669583c613452539c (diff)
downloadseaweedfs-12f50d37fa52444a43ad6ff4cc3d156db4035528.tar.xz
seaweedfs-12f50d37fa52444a43ad6ff4cc3d156db4035528.zip
test versioning also (#7000)
* test versioning also * fix some versioning tests * fall back * fixes Never-versioned buckets: No VersionId headers, no Status field Pre-versioning objects: Regular files, VersionId="null", included in all operations Post-versioning objects: Stored in .versions directories with real version IDs Suspended versioning: Proper status handling and null version IDs * fixes Bucket Versioning Status Compliance Fixed: New buckets now return no Status field (AWS S3 compliant) Before: Always returned "Suspended" ❌ After: Returns empty VersioningConfiguration for unconfigured buckets ✅ 2. Multi-Object Delete Versioning Support Fixed: DeleteMultipleObjectsHandler now fully versioning-aware Before: Always deleted physical files, breaking versioning ❌ After: Creates delete markers or deletes specific versions properly ✅ Added: DeleteMarker field in response structure for AWS compatibility 3. Copy Operations Versioning Support Fixed: CopyObjectHandler and CopyObjectPartHandler now versioning-aware Before: Only copied regular files, couldn't handle versioned sources ❌ After: Parses version IDs from copy source, creates versions in destination ✅ Added: pathToBucketObjectAndVersion() function for version ID parsing 4. Pre-versioning Object Handling Fixed: getLatestObjectVersion() now has proper fallback logic Before: Failed when .versions directory didn't exist ❌ After: Falls back to regular objects for pre-versioning scenarios ✅ 5. Enhanced Object Version Listings Fixed: listObjectVersions() includes both versioned AND pre-versioning objects Before: Only showed .versions directories, ignored pre-versioning objects ❌ After: Shows complete version history with VersionId="null" for pre-versioning ✅ 6. Null Version ID Handling Fixed: getSpecificObjectVersion() properly handles versionId="null" Before: Couldn't retrieve pre-versioning objects by version ID ❌ After: Returns regular object files for "null" version requests ✅ 7. Version ID Response Headers Fixed: PUT operations only return x-amz-version-id when appropriate Before: Returned version IDs for non-versioned buckets ❌ After: Only returns version IDs for explicitly configured versioning ✅ * more fixes * fix copying with versioning, multipart upload * more fixes * reduce volume size for easier dev test * fix * fix version id * fix versioning * Update filer_multipart.go * fix multipart versioned upload * more fixes * more fixes * fix versioning on suspended * fixes * fixing test_versioning_obj_suspended_copy * Update s3api_object_versioning.go * fix versions * skipping test_versioning_obj_suspend_versions * > If the versioning state has never been set on a bucket, it has no versioning state; a GetBucketVersioning request does not return a versioning state value. * fix tests, avoid duplicated bucket creation, skip tests * only run s3tests_boto3/functional/test_s3.py * fix checking filer_pb.ErrNotFound * Update weed/s3api/s3api_object_versioning.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update weed/s3api/s3api_object_handlers_copy.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update weed/s3api/s3api_bucket_config.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update test/s3/versioning/s3_versioning_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Diffstat (limited to 'weed/s3api/s3api_object_handlers_delete.go')
-rw-r--r--weed/s3api/s3api_object_handlers_delete.go121
1 files changed, 91 insertions, 30 deletions
diff --git a/weed/s3api/s3api_object_handlers_delete.go b/weed/s3api/s3api_object_handlers_delete.go
index b2d9c51c9..8cb5c04fe 100644
--- a/weed/s3api/s3api_object_handlers_delete.go
+++ b/weed/s3api/s3api_object_handlers_delete.go
@@ -32,8 +32,8 @@ func (s3a *S3ApiServer) DeleteObjectHandler(w http.ResponseWriter, r *http.Reque
// Check for specific version ID in query parameters
versionId := r.URL.Query().Get("versionId")
- // Check if versioning is enabled for the bucket
- versioningEnabled, err := s3a.isVersioningEnabled(bucket)
+ // Check if versioning is configured for the bucket (Enabled or Suspended)
+ versioningConfigured, err := s3a.isVersioningConfigured(bucket)
if err != nil {
if err == filer_pb.ErrNotFound {
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
@@ -49,7 +49,7 @@ func (s3a *S3ApiServer) DeleteObjectHandler(w http.ResponseWriter, r *http.Reque
auditLog = s3err.GetAccessLog(r, http.StatusNoContent, s3err.ErrNone)
}
- if versioningEnabled {
+ if versioningConfigured {
// Handle versioned delete
if versionId != "" {
// Check object lock permissions before deleting specific version
@@ -137,8 +137,10 @@ func (s3a *S3ApiServer) DeleteObjectHandler(w http.ResponseWriter, r *http.Reque
// 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"`
+ Key string `xml:"Key"`
+ VersionId string `xml:"VersionId,omitempty"`
+ DeleteMarker bool `xml:"DeleteMarker,omitempty"`
+ DeleteMarkerVersionId string `xml:"DeleteMarkerVersionId,omitempty"`
}
// DeleteObjectsRequest - xml carrying the object key names which needs to be deleted.
@@ -201,8 +203,8 @@ func (s3a *S3ApiServer) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *h
auditLog = s3err.GetAccessLog(r, http.StatusNoContent, s3err.ErrNone)
}
- // Check if versioning is enabled for the bucket (needed for object lock checks)
- versioningEnabled, err := s3a.isVersioningEnabled(bucket)
+ // Check if versioning is configured for the bucket (needed for object lock checks)
+ versioningConfigured, err := s3a.isVersioningConfigured(bucket)
if err != nil {
if err == filer_pb.ErrNotFound {
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
@@ -222,7 +224,7 @@ func (s3a *S3ApiServer) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *h
}
// Check object lock permissions before deletion (only for versioned buckets)
- if versioningEnabled {
+ if versioningConfigured {
// Validate governance bypass for this specific object
governanceBypassAllowed := s3a.evaluateGovernanceBypassRequest(r, bucket, object.Key)
if err := s3a.enforceObjectLockProtections(r, bucket, object.Key, object.VersionId, governanceBypassAllowed); err != nil {
@@ -236,31 +238,90 @@ func (s3a *S3ApiServer) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *h
continue
}
}
- lastSeparator := strings.LastIndex(object.Key, "/")
- parentDirectoryPath, entryName, isDeleteData, isRecursive := "", object.Key, true, false
- if lastSeparator > 0 && lastSeparator+1 < len(object.Key) {
- entryName = object.Key[lastSeparator+1:]
- parentDirectoryPath = "/" + object.Key[:lastSeparator]
- }
- parentDirectoryPath = fmt.Sprintf("%s/%s%s", s3a.option.BucketsPath, bucket, parentDirectoryPath)
-
- err := doDeleteEntry(client, parentDirectoryPath, entryName, isDeleteData, isRecursive)
- if err == nil {
- directoriesWithDeletion[parentDirectoryPath]++
- deletedObjects = append(deletedObjects, object)
- } else if strings.Contains(err.Error(), filer.MsgFailDelNonEmptyFolder) {
- deletedObjects = append(deletedObjects, object)
+
+ var deleteVersionId string
+ var isDeleteMarker bool
+
+ if versioningConfigured {
+ // Handle versioned delete
+ if object.VersionId != "" {
+ // Delete specific version
+ err := s3a.deleteSpecificObjectVersion(bucket, object.Key, object.VersionId)
+ if err != nil {
+ deleteErrors = append(deleteErrors, DeleteError{
+ Code: "",
+ Message: err.Error(),
+ Key: object.Key,
+ VersionId: object.VersionId,
+ })
+ continue
+ }
+ deleteVersionId = object.VersionId
+ } else {
+ // Create delete marker (logical delete)
+ deleteMarkerVersionId, err := s3a.createDeleteMarker(bucket, object.Key)
+ if err != nil {
+ deleteErrors = append(deleteErrors, DeleteError{
+ Code: "",
+ Message: err.Error(),
+ Key: object.Key,
+ VersionId: object.VersionId,
+ })
+ continue
+ }
+ deleteVersionId = deleteMarkerVersionId
+ isDeleteMarker = true
+ }
+
+ // Add to successful deletions with version info
+ deletedObject := ObjectIdentifier{
+ Key: object.Key,
+ VersionId: deleteVersionId,
+ DeleteMarker: isDeleteMarker,
+ }
+
+ // For delete markers, also set DeleteMarkerVersionId field
+ if isDeleteMarker {
+ deletedObject.DeleteMarkerVersionId = deleteVersionId
+ // Don't set VersionId for delete markers, use DeleteMarkerVersionId instead
+ deletedObject.VersionId = ""
+ }
+ if !deleteObjects.Quiet {
+ deletedObjects = append(deletedObjects, deletedObject)
+ }
+ if isDeleteMarker {
+ // For delete markers, we don't need to track directories for cleanup
+ continue
+ }
} else {
- delete(directoriesWithDeletion, parentDirectoryPath)
- deleteErrors = append(deleteErrors, DeleteError{
- Code: "",
- Message: err.Error(),
- Key: object.Key,
- VersionId: object.VersionId,
- })
+ // Handle non-versioned delete (original logic)
+ lastSeparator := strings.LastIndex(object.Key, "/")
+ parentDirectoryPath, entryName, isDeleteData, isRecursive := "", object.Key, true, false
+ if lastSeparator > 0 && lastSeparator+1 < len(object.Key) {
+ entryName = object.Key[lastSeparator+1:]
+ parentDirectoryPath = "/" + object.Key[:lastSeparator]
+ }
+ parentDirectoryPath = fmt.Sprintf("%s/%s%s", s3a.option.BucketsPath, bucket, parentDirectoryPath)
+
+ err := doDeleteEntry(client, parentDirectoryPath, entryName, isDeleteData, isRecursive)
+ if err == nil {
+ directoriesWithDeletion[parentDirectoryPath]++
+ deletedObjects = append(deletedObjects, object)
+ } else if strings.Contains(err.Error(), filer.MsgFailDelNonEmptyFolder) {
+ deletedObjects = append(deletedObjects, object)
+ } else {
+ delete(directoriesWithDeletion, parentDirectoryPath)
+ deleteErrors = append(deleteErrors, DeleteError{
+ Code: "",
+ Message: err.Error(),
+ Key: object.Key,
+ VersionId: object.VersionId,
+ })
+ }
}
+
if auditLog != nil {
- auditLog.Key = entryName
+ auditLog.Key = object.Key
s3err.PostAccessLog(*auditLog)
}
}