mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-21 14:46:58 +00:00
s3: stop listing prefixes whose objects are all delete-marked (#10419)
Deleting the only object under a prefix in a versioned bucket writes a delete marker and keeps the version history, so the filer directory survives with nothing a current-version listing would return. A delimited ListObjects kept reporting that path in CommonPrefixes, because the prefixes come from the directory tree rather than from the keys, while a listing scoped inside the prefix correctly came back empty. Probe a directory before reporting it: one that holds entries but no key the listing returns is neither a CommonPrefix nor a path the trailing-slash probe answers for. Empty directories keep the meaning they have today, and the probe only runs for buckets with versioning configured, the only ones that can reach this state.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestDeletedPrefixLeavesNoCommonPrefix covers the versioned bucket case where the only
|
||||
// object under a prefix is deleted. The delete marker and the version history stay, so
|
||||
// the directory survives in the filer, but a current-version listing has no key under
|
||||
// that path and AWS reports no CommonPrefix for it.
|
||||
func TestDeletedPrefixLeavesNoCommonPrefix(t *testing.T) {
|
||||
client := getS3Client(t)
|
||||
bucketName := getNewBucketName()
|
||||
|
||||
createBucket(t, client, bucketName)
|
||||
defer deleteBucket(t, client, bucketName)
|
||||
enableVersioning(t, client, bucketName)
|
||||
|
||||
putObject(t, client, bucketName, "backup/20260101/manifest", "first backup")
|
||||
putObject(t, client, bucketName, "backup/20260102/manifest", "second backup")
|
||||
|
||||
listPrefixes := func(prefix string) []string {
|
||||
t.Helper()
|
||||
input := &s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(bucketName),
|
||||
Delimiter: aws.String("/"),
|
||||
}
|
||||
if prefix != "" {
|
||||
input.Prefix = aws.String(prefix)
|
||||
}
|
||||
resp, err := client.ListObjectsV2(context.TODO(), input)
|
||||
require.NoError(t, err)
|
||||
prefixes := make([]string, 0, len(resp.CommonPrefixes))
|
||||
for _, p := range resp.CommonPrefixes {
|
||||
prefixes = append(prefixes, *p.Prefix)
|
||||
}
|
||||
return prefixes
|
||||
}
|
||||
listKeys := func(prefix string) []string {
|
||||
t.Helper()
|
||||
input := &s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(bucketName),
|
||||
Delimiter: aws.String("/"),
|
||||
}
|
||||
if prefix != "" {
|
||||
input.Prefix = aws.String(prefix)
|
||||
}
|
||||
resp, err := client.ListObjectsV2(context.TODO(), input)
|
||||
require.NoError(t, err)
|
||||
keys := make([]string, 0, len(resp.Contents))
|
||||
for _, c := range resp.Contents {
|
||||
keys = append(keys, *c.Key)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
deleteKey := func(key string) {
|
||||
t.Helper()
|
||||
_, err := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, []string{"backup/20260101/", "backup/20260102/"}, listPrefixes("backup/"))
|
||||
|
||||
deleteKey("backup/20260101/manifest")
|
||||
assert.Equal(t, []string{"backup/20260102/"}, listPrefixes("backup/"),
|
||||
"the delete-marked date must drop out of CommonPrefixes")
|
||||
assert.Equal(t, []string{"backup/"}, listPrefixes(""),
|
||||
"the parent still holds a live object so it keeps its prefix")
|
||||
|
||||
deleteKey("backup/20260102/manifest")
|
||||
assert.Empty(t, listPrefixes("backup/"), "no date is left under backup/")
|
||||
assert.Empty(t, listKeys("backup/"), "and backup/ itself is not a key")
|
||||
assert.Empty(t, listPrefixes(""), "the bucket lists as empty once every object is delete-marked")
|
||||
|
||||
// The version history is untouched: both objects still have their version and their
|
||||
// delete marker, and removing a delete marker brings the prefix back.
|
||||
versions, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, versions.Versions, 2)
|
||||
assert.Len(t, versions.DeleteMarkers, 2)
|
||||
|
||||
for _, marker := range versions.DeleteMarkers {
|
||||
if *marker.Key != "backup/20260102/manifest" {
|
||||
continue
|
||||
}
|
||||
_, err := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: marker.Key,
|
||||
VersionId: marker.VersionId,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, []string{"backup/20260102/"}, listPrefixes("backup/"),
|
||||
"removing the delete marker restores the prefix")
|
||||
}
|
||||
Reference in New Issue
Block a user