Skip to content

Commit

Permalink
Merge pull request #806 from versity/ben/common_prefixes
Browse files Browse the repository at this point in the history
fix: list objects trim common prefixes that match marker prefix
  • Loading branch information
benmcclelland authored Sep 18, 2024
2 parents 221440e + d9d3a16 commit cf067b5
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 3 deletions.
16 changes: 14 additions & 2 deletions backend/azure/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -567,14 +567,19 @@ Pager:
isTruncated = true
break Pager
}

marker := getString(input.Marker)
pfx := strings.TrimSuffix(*v.Name, getString(input.Delimiter))
if marker != "" && strings.HasPrefix(marker, pfx) {
continue
}

cPrefixes = append(cPrefixes, types.CommonPrefix{
Prefix: v.Name,
})
}
}

// TODO: generate common prefixes when appropriate

return s3response.ListObjectsResult{
Contents: objects,
Marker: input.Marker,
Expand Down Expand Up @@ -644,6 +649,13 @@ Pager:
isTruncated = true
break Pager
}

marker := getString(input.ContinuationToken)
pfx := strings.TrimSuffix(*v.Name, getString(input.Delimiter))
if marker != "" && strings.HasPrefix(marker, pfx) {
continue
}

cPrefixes = append(cPrefixes, types.CommonPrefix{
Prefix: v.Name,
})
Expand Down
9 changes: 8 additions & 1 deletion backend/walk.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,19 @@ func Walk(ctx context.Context, fileSystem fs.FS, prefix, delimiter, marker strin

// Common prefixes are a set, so should not have duplicates.
// These are abstractly a "directory", so need to include the
// delimiter at the end.
// delimiter at the end when we add to the map.
cprefNoDelim := prefix + before
cpref := prefix + before + delimiter
if cpref == marker {
pastMarker = true
return nil
}

if marker != "" && strings.HasPrefix(marker, cprefNoDelim) {
// skip common prefixes that are before the marker
return nil
}

cpmap[cpref] = struct{}{}
if (len(objects) + len(cpmap)) == int(max) {
newMarker = cpref
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/group-tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ func TestListObjects(s *S3Conf) {
ListObjects_non_existing_bucket(s)
ListObjects_with_prefix(s)
ListObjects_truncated(s)
ListObjects_paginated(s)
ListObjects_invalid_max_keys(s)
ListObjects_max_keys_0(s)
ListObjects_delimiter(s)
Expand Down Expand Up @@ -612,6 +613,7 @@ func GetIntTests() IntTests {
"ListObjects_non_existing_bucket": ListObjects_non_existing_bucket,
"ListObjects_with_prefix": ListObjects_with_prefix,
"ListObjects_truncated": ListObjects_truncated,
"ListObjects_paginated": ListObjects_paginated,
"ListObjects_invalid_max_keys": ListObjects_invalid_max_keys,
"ListObjects_max_keys_0": ListObjects_max_keys_0,
"ListObjects_delimiter": ListObjects_delimiter,
Expand Down
27 changes: 27 additions & 0 deletions tests/integration/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -3590,6 +3590,33 @@ func ListObjects_with_prefix(s *S3Conf) error {
})
}

func ListObjects_paginated(s *S3Conf) error {
testName := "ListObjects_paginated"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
_, err := putObjects(s3client, []string{"dir1/subdir/file.txt", "dir1/subdir.ext", "dir1/subdir1.ext", "dir1/subdir2.ext"}, bucket)
if err != nil {
return err
}

objs, prefixes, err := listObjects(s3client, bucket, "dir1/", "/", 2)
if err != nil {
return err
}

expected := []string{"dir1/subdir.ext", "dir1/subdir1.ext", "dir1/subdir2.ext"}
if !hasObjNames(objs, expected) {
return fmt.Errorf("expected objects %v, instead got %v", expected, objStrings(objs))
}

expectedPrefix := []string{"dir1/subdir/"}
if !hasPrefixName(prefixes, expectedPrefix) {
return fmt.Errorf("expected prefixes %v, instead got %v", expectedPrefix, pfxStrings(prefixes))
}

return nil
})
}

func ListObjects_truncated(s *S3Conf) error {
testName := "ListObjects_truncated"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
Expand Down
86 changes: 86 additions & 0 deletions tests/integration/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,76 @@ func putObjects(client *s3.Client, objs []string, bucket string) ([]types.Object
return contents, nil
}

func listObjects(client *s3.Client, bucket, prefix, delimiter string, maxKeys int32) ([]types.Object, []types.CommonPrefix, error) {
var contents []types.Object
var commonPrefixes []types.CommonPrefix

var continuationToken *string

for {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
Bucket: &bucket,
ContinuationToken: continuationToken,
Prefix: &prefix,
Delimiter: &delimiter,
MaxKeys: &maxKeys,
})
cancel()
if err != nil {
return nil, nil, err
}
contents = append(contents, res.Contents...)
commonPrefixes = append(commonPrefixes, res.CommonPrefixes...)
continuationToken = res.NextContinuationToken

if !*res.IsTruncated {
break
}
}

return contents, commonPrefixes, nil
}

func hasObjNames(objs []types.Object, names []string) bool {
if len(objs) != len(names) {
return false
}

for _, obj := range objs {
if contains(names, *obj.Key) {
continue
}
return false
}

return true
}

func hasPrefixName(prefixes []types.CommonPrefix, names []string) bool {
if len(prefixes) != len(names) {
return false
}

for _, prefix := range prefixes {
if contains(names, *prefix.Prefix) {
continue
}
return false
}

return true
}

func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}

func putObjectWithData(lgth int64, input *s3.PutObjectInput, client *s3.Client) (csum [32]byte, data []byte, err error) {
data = make([]byte, lgth)
rand.Read(data)
Expand Down Expand Up @@ -774,3 +844,19 @@ func checkWORMProtection(client *s3.Client, bucket, object string) error {

return nil
}

func objStrings(objs []types.Object) []string {
objStrs := make([]string, len(objs))
for i, obj := range objs {
objStrs[i] = *obj.Key
}
return objStrs
}

func pfxStrings(pfxs []types.CommonPrefix) []string {
pfxStrs := make([]string, len(pfxs))
for i, pfx := range pfxs {
pfxStrs[i] = *pfx.Prefix
}
return pfxStrs
}

0 comments on commit cf067b5

Please sign in to comment.