fix(storage): strip Root prefix from S3 List() returned paths (#1413)

This commit is contained in:
Vlad Mocanu
2026-04-08 22:01:34 +03:00
committed by GitHub
parent 2b94535ade
commit 626adbf14c
2 changed files with 52 additions and 1 deletions

View File

@@ -138,7 +138,7 @@ func (s *s3Storage) List(ctx context.Context, path string) ([]ObjectInfo, error)
continue continue
} }
objects = append(objects, ObjectInfo{ objects = append(objects, ObjectInfo{
Path: aws.ToString(obj.Key), Path: s.pathFromKey(aws.ToString(obj.Key)),
Size: aws.ToInt64(obj.Size), Size: aws.ToInt64(obj.Size),
ModTime: aws.ToTime(obj.LastModified), ModTime: aws.ToTime(obj.LastModified),
}) })
@@ -147,6 +147,13 @@ func (s *s3Storage) List(ctx context.Context, path string) ([]ObjectInfo, error)
return objects, nil return objects, nil
} }
func (s *s3Storage) pathFromKey(key string) string {
if s.prefix == "" {
return key
}
return strings.TrimPrefix(key, s.prefix+"/")
}
func (s *s3Storage) Walk(ctx context.Context, root string, fn func(ObjectInfo) error) error { func (s *s3Storage) Walk(ctx context.Context, root string, fn func(ObjectInfo) error) error {
objects, err := s.List(ctx, root) objects, err := s.List(ctx, root)
if err != nil { if err != nil {

View File

@@ -35,6 +35,50 @@ func TestS3Helpers(t *testing.T) {
} }
}) })
t.Run("pathFromKey strips prefix to honor relative-path contract", func(t *testing.T) {
tests := []struct {
name string
prefix string
key string
expected string
}{
{name: "no prefix returns key unchanged", prefix: "", key: "images/logo.png", expected: "images/logo.png"},
{name: "no prefix empty key", prefix: "", key: "", expected: ""},
{name: "prefix matches and is stripped", prefix: "data/uploads", key: "data/uploads/application-images/logo.svg", expected: "application-images/logo.svg"},
{name: "single-segment prefix stripped", prefix: "root", key: "root/foo/bar.txt", expected: "foo/bar.txt"},
{name: "prefix equal to key without trailing slash is unchanged", prefix: "root", key: "root", expected: "root"},
{name: "key without expected prefix returned unchanged", prefix: "data/uploads", key: "other/path.txt", expected: "other/path.txt"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
s := &s3Storage{
bucket: "bucket",
prefix: tc.prefix,
}
assert.Equal(t, tc.expected, s.pathFromKey(tc.key))
})
}
})
t.Run("pathFromKey is the inverse of buildObjectKey for clean paths", func(t *testing.T) {
paths := []string{
"images/logo.png",
"application-images/logo.svg",
"oidc-client-images/abc.png",
"deeply/nested/file.bin",
}
prefixes := []string{"", "root", "data/uploads"}
for _, prefix := range prefixes {
for _, p := range paths {
s := &s3Storage{bucket: "bucket", prefix: prefix}
assert.Equal(t, p, s.pathFromKey(s.buildObjectKey(p)),
"round-trip failed for prefix=%q path=%q", prefix, p)
}
}
})
t.Run("isS3NotFound detects expected errors", func(t *testing.T) { t.Run("isS3NotFound detects expected errors", func(t *testing.T) {
assert.True(t, isS3NotFound(&smithy.GenericAPIError{Code: "NoSuchKey"})) assert.True(t, isS3NotFound(&smithy.GenericAPIError{Code: "NoSuchKey"}))
assert.True(t, isS3NotFound(&smithy.GenericAPIError{Code: "NotFound"})) assert.True(t, isS3NotFound(&smithy.GenericAPIError{Code: "NotFound"}))