s3: fix delete of directory marker on non-empty directory (#8731)

When deleting an S3 key ending with "/" (a directory marker) where the
directory has children, strip the marker metadata instead of returning
InternalError. This matches AWS S3 behavior where deleting a directory
marker only removes that object, not child objects under the prefix.
This commit is contained in:
Copilot
2026-03-23 10:38:10 -07:00
parent 3a765df2ff
commit a03ec37bdb
2 changed files with 51 additions and 1 deletions
+37
View File
@@ -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)
+14 -1
View File
@@ -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{