diff --git a/weed/s3api/s3api_object_handlers_list.go b/weed/s3api/s3api_object_handlers_list.go index 0aecc8655..0f9b5a93a 100644 --- a/weed/s3api/s3api_object_handlers_list.go +++ b/weed/s3api/s3api_object_handlers_list.go @@ -257,7 +257,7 @@ func (s3a *S3ApiServer) listFilerEntries(bucket string, originalPrefix string, m cursor.maxKeys-- lastEntryWasCommonPrefix = false // https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html - } else if delimiter == "/" { // A response can contain CommonPrefixes only if you specify a delimiter. + } else if delimiter != "" { // A response can contain CommonPrefixes only if you specify a delimiter. // Use raw dir and entry.Name (not encoded) to ensure consistent handling // Encoding will be applied after sorting if encodingTypeUrl is set commonPrefixes = append(commonPrefixes, PrefixEntry{ diff --git a/weed/s3api/s3api_object_handlers_list_directory_test.go b/weed/s3api/s3api_object_handlers_list_directory_test.go new file mode 100644 index 000000000..8b3c26b8b --- /dev/null +++ b/weed/s3api/s3api_object_handlers_list_directory_test.go @@ -0,0 +1,63 @@ +package s3api + +import ( + "testing" + + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" + "github.com/stretchr/testify/assert" +) + +// TestDirectoryListedAsCommonPrefix tests that regular directories (without MIME) +// are correctly listed as CommonPrefixes when delimiter="/" is used, not as objects. +// Note: The fix applies to any non-empty delimiter ('/', '_', ':'), but this test focuses on '/'. +func TestDirectoryListedAsCommonPrefix(t *testing.T) { + s3a := &S3ApiServer{ + option: &S3ApiServerOption{ + BucketsPath: "/buckets", + }, + } + + // Regular directory (no MIME) - matching actual user metadata + regularDir := &filer_pb.Entry{ + Name: "f2f1237f-0e69-4e0b-8f01-d4fa299787e1.vhd", + IsDirectory: true, + Attributes: &filer_pb.FuseAttributes{ + Mime: "", // Empty MIME - IsDirectoryKeyObject() returns false + }, + } + + client := &testFilerClient{ + entriesByDir: map[string][]*filer_pb.Entry{ + "/buckets/xoa-bucket/xo-vm-backups/data": {regularDir}, + }, + } + + cursor := &ListingCursor{maxKeys: 1000} + var seenDirs []string + var seenFiles []string + + _, err := s3a.doListFilerEntries( + client, + "/buckets/xoa-bucket/xo-vm-backups/data", + "", // prefix + cursor, + "", // marker + "/", // delimiter="/" - should yield directory for CommonPrefix processing + false, + "xoa-bucket", + func(dir string, entry *filer_pb.Entry) { + if entry.IsDirectory { + seenDirs = append(seenDirs, entry.Name) + } else { + seenFiles = append(seenFiles, entry.Name) + } + }, + ) + + assert.NoError(t, err) + + // The directory should be passed to the callback for delimiter="/" + assert.Contains(t, seenDirs, "f2f1237f-0e69-4e0b-8f01-d4fa299787e1.vhd", + "Directory should be passed to callback for CommonPrefix processing with delimiter=/") + assert.Empty(t, seenFiles, "No files should be seen, only the directory") +}