mirror of
https://github.com/oven-sh/bun
synced 2026-02-15 13:22:07 +00:00
Fix S3 trailing slash preservation in object keys
S3 object keys with trailing slashes were being incorrectly stripped when writing to S3. This is problematic because trailing slashes have semantic meaning in S3 (commonly used to represent folders/prefixes). The fix removes the normalization logic that was stripping trailing slashes from S3 paths, while still removing leading slashes as required for proper S3 key formatting. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -311,12 +311,8 @@ pub const S3 = struct {
|
||||
|
||||
pub fn path(this: *@This()) []const u8 {
|
||||
var path_name = bun.URL.parse(this.pathlike.slice()).s3Path();
|
||||
// normalize start and ending
|
||||
if (strings.endsWith(path_name, "/")) {
|
||||
path_name = path_name[0..path_name.len];
|
||||
} else if (strings.endsWith(path_name, "\\")) {
|
||||
path_name = path_name[0 .. path_name.len - 1];
|
||||
}
|
||||
// For S3, we only remove leading slashes but preserve trailing slashes
|
||||
// as they are semantically significant (e.g., for folder representations)
|
||||
if (strings.startsWith(path_name, "/")) {
|
||||
path_name = path_name[1..];
|
||||
} else if (strings.startsWith(path_name, "\\")) {
|
||||
|
||||
@@ -1166,4 +1166,83 @@ describe.skipIf(!optionsFromEnv.accessKeyId)("S3 - CI - List Objects", () => {
|
||||
expect(storedFile.owner).toBeObject();
|
||||
expect(storedFile.owner!.id).toBeString();
|
||||
});
|
||||
|
||||
it("should preserve trailing slashes in S3 keys", async () => {
|
||||
const testKeys = [
|
||||
"test_folder/",
|
||||
"test_folder/subfolder/",
|
||||
"test_file_without_slash",
|
||||
];
|
||||
const uploadedKeys: string[] = [];
|
||||
|
||||
using server = createBunServer(async req => {
|
||||
const url = new URL(req.url);
|
||||
|
||||
// Handle PUT requests (write operations)
|
||||
if (req.method === "PUT") {
|
||||
// Extract the key from the URL path (remove leading slash)
|
||||
const key = url.pathname.substring(1);
|
||||
uploadedKeys.push(key);
|
||||
|
||||
return new Response("", {
|
||||
headers: {
|
||||
ETag: '"test-etag"',
|
||||
},
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
|
||||
// Handle GET requests (list operations)
|
||||
if (req.method === "GET" && url.search.includes("list-type=2")) {
|
||||
// Return the keys exactly as they were uploaded
|
||||
const contents = uploadedKeys.map(key =>
|
||||
`<Contents><Key>${key}</Key><Size>0</Size></Contents>`
|
||||
).join("");
|
||||
|
||||
return new Response(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ListBucketResult>
|
||||
<Name>test-bucket</Name>
|
||||
${contents}
|
||||
<IsTruncated>false</IsTruncated>
|
||||
</ListBucketResult>`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/xml",
|
||||
},
|
||||
status: 200,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return new Response("Not Found", { status: 404 });
|
||||
});
|
||||
|
||||
const client = new S3Client({
|
||||
...options,
|
||||
endpoint: server.url.href,
|
||||
});
|
||||
|
||||
// Write objects with and without trailing slashes
|
||||
for (const key of testKeys) {
|
||||
await client.write(key, new ArrayBuffer(0));
|
||||
}
|
||||
|
||||
// List objects and verify trailing slashes are preserved
|
||||
const listResult = await client.list({});
|
||||
|
||||
expect(listResult.contents).toBeArray();
|
||||
expect(listResult.contents).toHaveLength(3);
|
||||
|
||||
// Verify each key is preserved exactly as written
|
||||
const listedKeys = listResult.contents!.map((item: any) => item.key);
|
||||
expect(listedKeys).toContain("test_folder/");
|
||||
expect(listedKeys).toContain("test_folder/subfolder/");
|
||||
expect(listedKeys).toContain("test_file_without_slash");
|
||||
|
||||
// Specifically verify trailing slashes are preserved
|
||||
expect(listedKeys[0]).toEndWith("/");
|
||||
expect(listedKeys[1]).toEndWith("/");
|
||||
expect(listedKeys[2]).not.toEndWith("/");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user