s3: count stored versions as bucket contents on delete (#11775)

* s3: count stored versions as bucket contents

A <key>.versions directory holding version entries is user data; skipping
it let bucketHasUserObjects report an empty bucket while the recursive
delete dropped every stored version.

* test: rename stale reserved-folders case in bucket delete test

.versions directories are now descended into, so the case only holds
because the folder is empty.
This commit is contained in:
Chris Lu authored and GitHub committed 2026-10-17 15:25:56 -07:00
1 parent 44bb697495
commit 602ddb31f2
2 files changed
+22 -7

No files matched your search

+15 -2
View File
@@ -152,7 +152,7 @@ func TestBucketHasUserObjects_EmptyDirectories(t *testing.T) {
wantHasUser: true,
},
{
name: "bucket with only reserved folders (.uploads, *.versions)",
name: "empty *.versions folder and .uploads do not count",
entriesByDir: map[string][]*filer_pb.Entry{
"/buckets/b": {
{Name: s3_constants.MultipartUploadsFolder, IsDirectory: true},
@@ -162,7 +162,7 @@ func TestBucketHasUserObjects_EmptyDirectories(t *testing.T) {
wantHasUser: false,
},
{
name: "nested versions-named directories are internal",
name: "nested versions directory with stored versions counts",
entriesByDir: map[string][]*filer_pb.Entry{
"/buckets/b": {
{Name: "logs", IsDirectory: true},
@@ -174,6 +174,19 @@ func TestBucketHasUserObjects_EmptyDirectories(t *testing.T) {
{Name: "v1", IsDirectory: false},
},
},
wantHasUser: true,
},
{
name: "nested versions directory left empty does not count",
entriesByDir: map[string][]*filer_pb.Entry{
"/buckets/b": {
{Name: "logs", IsDirectory: true},
},
"/buckets/b/logs": {
{Name: "foo" + s3_constants.VersionsFolder, IsDirectory: true},
},
"/buckets/b/logs/foo" + s3_constants.VersionsFolder: {},
},
wantHasUser: false,
},
{
+7 -5
View File
@@ -521,8 +521,9 @@ func (s3a *S3ApiServer) DeleteBucketHandler(w http.ResponseWriter, r *http.Reque
}
// bucketHasUserObjects checks whether a bucket contains any user objects.
// Empty directories left behind by deleted objects and internal folders
// (.uploads, *.versions) do not count as user objects.
// Empty directories left behind by deleted objects and the .uploads staging
// folder do not count as user objects; stored versions inside a <key>.versions
// directory do.
func (s3a *S3ApiServer) bucketHasUserObjects(bucket string) (bool, error) {
return s3a.dirHasUserObjects(s3a.option.BucketsPath + "/" + bucket)
}
@@ -556,9 +557,10 @@ func (s3a *S3ApiServer) dirHasUserObjects(root string) (bool, error) {
if entry.IsDirectoryKeyObject() {
return true, nil
}
// Internal folders are skipped by object listing at every level,
// so a directory with a reserved name never counts.
if isReservedDirectoryName(entry.Name) {
// Multipart staging folders are invisible to object listing at
// every level, so they never count. A <key>.versions directory
// is descended into since it may still hold stored versions.
if entry.Name == s3_constants.MultipartUploadsFolder {
continue
}
dirs = append(dirs, dir+"/"+entry.Name)