diff --git a/weed/s3api/filer_util.go b/weed/s3api/filer_util.go index 10afab106..398751f79 100644 --- a/weed/s3api/filer_util.go +++ b/weed/s3api/filer_util.go @@ -78,6 +78,43 @@ func doDeleteEntry(client filer_pb.SeaweedFilerClient, parentDirectoryPath strin return nil } +// removeDirectoryKeyObjectMarker clears the MIME type and S3-specific metadata from a +// directory entry so it no longer appears as an object in S3 listings (IsDirectoryKeyObject() +// returns false). This handles the case where a directory marker (key ending with "/") is +// deleted on a non-empty directory — matching AWS S3 behavior where deleting the marker +// only removes that object, not the child objects under the prefix. +func removeDirectoryKeyObjectMarker(client filer_pb.SeaweedFilerClient, parentDir, entryName string) error { + ctx := context.Background() + resp, err := filer_pb.LookupEntry(ctx, client, &filer_pb.LookupDirectoryEntryRequest{ + Directory: parentDir, + Name: entryName, + }) + if err != nil { + if errors.Is(err, filer_pb.ErrNotFound) { + return nil + } + return err + } + entry := resp.Entry + if entry == nil || !entry.IsDirectory || entry.Attributes == nil || entry.Attributes.Mime == "" { + return nil // Not a directory key object, nothing to remove + } + + // Clear marker metadata so IsDirectoryKeyObject() returns false + entry.Attributes.Mime = "" + entry.Attributes.Md5 = nil + entry.Content = nil + if entry.Extended != nil { + delete(entry.Extended, s3_constants.ExtETagKey) + delete(entry.Extended, s3_constants.ExtAmzOwnerKey) + } + + return filer_pb.UpdateEntry(ctx, client, &filer_pb.UpdateEntryRequest{ + Directory: parentDir, + Entry: entry, + }) +} + func (s3a *S3ApiServer) exists(parentDirectoryPath string, entryName string, isDirectory bool) (exists bool, err error) { return filer_pb.Exists(context.Background(), s3a, parentDirectoryPath, entryName, isDirectory) diff --git a/weed/s3api/s3api_object_handlers_delete.go b/weed/s3api/s3api_object_handlers_delete.go index ea51f61e0..fe21cf1fa 100644 --- a/weed/s3api/s3api_object_handlers_delete.go +++ b/weed/s3api/s3api_object_handlers_delete.go @@ -130,7 +130,15 @@ func (s3a *S3ApiServer) DeleteObjectHandler(w http.ResponseWriter, r *http.Reque dir, name := target.DirAndName() err := s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { - return doDeleteEntry(client, dir, name, true, false) + deleteErr := doDeleteEntry(client, dir, name, true, false) + if deleteErr != nil && strings.HasSuffix(object, "/") && + strings.Contains(deleteErr.Error(), filer.MsgFailDelNonEmptyFolder) { + // Deleting a directory key object (S3 key ending with "/") on a non-empty + // directory. Strip the marker metadata so it no longer appears as an S3 object, + // matching AWS S3 behavior where only the marker is removed, not child objects. + return removeDirectoryKeyObjectMarker(client, dir, name) + } + return deleteErr // Note: Empty folder cleanup is now handled asynchronously by EmptyFolderCleaner // which listens to metadata events and uses consistent hashing for coordination }) @@ -349,6 +357,11 @@ func (s3a *S3ApiServer) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *h if err == nil { deletedObjects = append(deletedObjects, object) } else if strings.Contains(err.Error(), filer.MsgFailDelNonEmptyFolder) { + if strings.HasSuffix(object.Key, "/") { + if markerErr := removeDirectoryKeyObjectMarker(client, parentDirectoryPath, entryName); markerErr != nil { + glog.Warningf("DeleteMultipleObjectsHandler: failed to remove directory marker %s/%s: %v", parentDirectoryPath, entryName, markerErr) + } + } deletedObjects = append(deletedObjects, object) } else { deleteErrors = append(deleteErrors, DeleteError{