@@ -725,39 +725,58 @@ struct InternalBucketController: RouteCollection {
725725 throw Abort ( . internalServerError, reason: " Invalid prefix for deletion " )
726726 }
727727
728- var marker : String ? = nil
728+ // Enumerate every VERSION and delete marker under the prefix, cluster-wide - NOT just
729+ // the current objects. A folder delete is a permanent prune, so on a versioning-enabled
730+ // bucket each stored version must be removed by its real versionId. The previous code
731+ // listed only current objects (a ListObjectsV2-style listing) and then deleted them at
732+ // the non-versioned path (versionId nil) - but a versioned object lives at a
733+ // `.versions/<id>` path, so that delete found nothing, removed nothing, and still
734+ // reported success. That is exactly the "folder delete says Successful but nothing is
735+ // gone" symptom on a versioned bucket. `listAllVersions` reports a non-versioned object
736+ // (matching S3) as versionId "null", which `deleteVersion` maps back to the plain path,
737+ // so this single loop prunes versioned and non-versioned buckets alike.
738+ var keyMarker : String ? = nil
739+ var versionIdMarker : String ? = nil
729740 var failedKeys = 0
730741 repeat {
731- let ( objects, _, isTruncated, nextMarker) = try await ClusterListingService . listObjects (
732- req: req, bucketName: bucketName, prefix: sanitizedPrefix, delimiter: nil ,
733- maxKeys: 1000 , marker: marker)
734- for object in objects {
742+ let ( versions, deleteMarkers, _, isTruncated, nextKeyMarker, nextVersionIdMarker) =
743+ try await ClusterListingService . listAllVersions (
744+ req: req, bucketName: bucketName, prefix: sanitizedPrefix, delimiter: nil ,
745+ keyMarker: keyMarker, versionIdMarker: versionIdMarker, maxKeys: 1000 )
746+ for object in versions + deleteMarkers {
747+ let targetVersionId = object. versionId ?? " null "
735748 // A per-key delete can fail (a responsible peer being unreachable) - unlike
736749 // the local-disk-only deletePrefix this replaced, whose only failure mode was
737750 // a local IO error. Rather than silently reporting the whole folder deleted
738751 // when some objects survived, count the failures and surface them below.
739- let outcome : S3Service . ObjectDeleteOutcome
740752 do {
741- outcome = try await ClusterReplicationService . deleteObjectClusterWide (
742- req: req, bucketName: bucketName, key: object. key, versionId : nil ,
743- versioningStatus: . disabled)
753+ _ = try await ClusterReplicationService . deleteObjectClusterWide (
754+ req: req, bucketName: bucketName, key: object. key,
755+ versionId : targetVersionId , versioningStatus: . disabled)
744756 } catch {
745757 failedKeys += 1
746758 req. logger. warning (
747- " Folder delete: failed to delete ' \( object. key) ' under ' \( sanitizedPrefix) ': \( error) "
759+ " Folder delete: failed to delete ' \( object. key) ' (version \( targetVersionId ) ) under '\( sanitizedPrefix) ': \( error) "
748760 )
749761 continue
750762 }
751- await NotificationService . emit (
752- event: . objectRemovedDelete, bucketName: bucketName, key: object. key,
753- size: nil , etag: nil , versionId: outcome. versionId,
754- requestId: req. id, sourceIP: req. remoteAddress? . ipAddress, app: req. application)
763+ // A delete marker being removed isn't itself an object-removed event; only emit
764+ // for a real object version.
765+ if !object. isDeleteMarker {
766+ await NotificationService . emit (
767+ event: . objectRemovedDelete, bucketName: bucketName, key: object. key,
768+ size: nil , etag: nil , versionId: targetVersionId,
769+ requestId: req. id, sourceIP: req. remoteAddress? . ipAddress, app: req. application)
770+ }
755771 await ReplicationService . enqueueDelete (
756772 app: req. application, bucketName: bucketName, key: object. key,
757- versionId: nil )
773+ versionId: targetVersionId )
758774 }
759- marker = isTruncated ? nextMarker : nil
760- } while marker != nil
775+ // Advance past what was just processed. Marker positions are absolute in the sorted
776+ // key/version space, so deleting items at or before the marker never skips the rest.
777+ keyMarker = isTruncated ? nextKeyMarker : nil
778+ versionIdMarker = isTruncated ? nextVersionIdMarker : nil
779+ } while keyMarker != nil
761780
762781 if failedKeys > 0 {
763782 throw Abort (
0 commit comments