s3api: fix ListObjectsV2 NextContinuationToken duplication for nested prefix (#8294)
* s3api: fix duplicate ListObjectsV2 continuation token for nested prefix * s3api: include prefix in common-prefix continuation token
This commit is contained in:
@@ -351,25 +351,8 @@ func (s3a *S3ApiServer) listFilerEntries(bucket string, originalPrefix string, m
|
||||
}
|
||||
|
||||
// Adjust nextMarker for CommonPrefixes to include trailing slash (AWS S3 compliance)
|
||||
if cursor.isTruncated && lastEntryWasCommonPrefix && lastCommonPrefixName != "" {
|
||||
// For CommonPrefixes, NextMarker should include the trailing slash
|
||||
if requestDir != "" {
|
||||
if prefix != "" {
|
||||
nextMarker = requestDir + "/" + prefix + "/" + lastCommonPrefixName + "/"
|
||||
} else {
|
||||
nextMarker = requestDir + "/" + lastCommonPrefixName + "/"
|
||||
}
|
||||
} else {
|
||||
nextMarker = lastCommonPrefixName + "/"
|
||||
}
|
||||
} else if cursor.isTruncated {
|
||||
if requestDir != "" {
|
||||
if prefix != "" {
|
||||
nextMarker = requestDir + "/" + prefix + "/" + nextMarker
|
||||
} else {
|
||||
nextMarker = requestDir + "/" + nextMarker
|
||||
}
|
||||
}
|
||||
if cursor.isTruncated {
|
||||
nextMarker = buildTruncatedNextMarker(requestDir, prefix, nextMarker, lastEntryWasCommonPrefix, lastCommonPrefixName)
|
||||
}
|
||||
|
||||
if cursor.isTruncated {
|
||||
@@ -480,6 +463,28 @@ func toParentAndDescendants(dirAndName string) (dir, name string) {
|
||||
return
|
||||
}
|
||||
|
||||
func buildTruncatedNextMarker(requestDir, prefix, nextMarker string, lastEntryWasCommonPrefix bool, lastCommonPrefixName string) string {
|
||||
if lastEntryWasCommonPrefix && lastCommonPrefixName != "" {
|
||||
// For CommonPrefixes, NextMarker should include the trailing slash
|
||||
if requestDir != "" {
|
||||
if prefix != "" {
|
||||
return requestDir + "/" + prefix + "/" + lastCommonPrefixName + "/"
|
||||
}
|
||||
return requestDir + "/" + lastCommonPrefixName + "/"
|
||||
}
|
||||
if prefix != "" {
|
||||
return prefix + "/" + lastCommonPrefixName + "/"
|
||||
}
|
||||
return lastCommonPrefixName + "/"
|
||||
}
|
||||
|
||||
if requestDir != "" {
|
||||
return requestDir + "/" + nextMarker
|
||||
}
|
||||
|
||||
return nextMarker
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) doListFilerEntries(client filer_pb.SeaweedFilerClient, dir, prefix string, cursor *ListingCursor, marker, delimiter string, inclusiveStartFrom bool, bucket string, eachEntryFn func(dir string, entry *filer_pb.Entry)) (nextMarker string, err error) {
|
||||
// invariants
|
||||
// prefix and marker should be under dir, marker may contain "/"
|
||||
|
||||
@@ -148,6 +148,26 @@ func Test_normalizePrefixMarker(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTruncatedNextMarker(t *testing.T) {
|
||||
t.Run("does not duplicate prefix segment in next continuation token", func(t *testing.T) {
|
||||
prefix := "export_2026-02-10_17-00-23"
|
||||
nextMarker := "export_2026-02-10_17-00-23/4156000e.jpg"
|
||||
|
||||
actual := buildTruncatedNextMarker("xemu", prefix, nextMarker, false, "")
|
||||
assert.Equal(t, "xemu/export_2026-02-10_17-00-23/4156000e.jpg", actual)
|
||||
})
|
||||
|
||||
t.Run("keeps common prefix marker trailing slash", func(t *testing.T) {
|
||||
actual := buildTruncatedNextMarker("xemu", "export_2026-02-10_17-00-23", "", true, "nested")
|
||||
assert.Equal(t, "xemu/export_2026-02-10_17-00-23/nested/", actual)
|
||||
})
|
||||
|
||||
t.Run("includes prefix for common prefix marker when request dir is empty", func(t *testing.T) {
|
||||
actual := buildTruncatedNextMarker("", "foo", "", true, "bar")
|
||||
assert.Equal(t, "foo/bar/", actual)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAllowUnorderedParameterValidation(t *testing.T) {
|
||||
// Test getListObjectsV1Args with allow-unordered parameter
|
||||
t.Run("getListObjectsV1Args with allow-unordered", func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user