mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
feat(s3): add Content-Disposition support for S3 uploads (#25363)
### What does this PR do? - Add `contentDisposition` option to S3 file uploads to control the `Content-Disposition` HTTP header - Support passing `contentDisposition` through all S3 upload paths (simple uploads, multipart uploads, and streaming uploads) - Add TypeScript types for the new option Fixes https://github.com/oven-sh/bun/issues/25362 ### How did you verify your code works? Test
This commit is contained in:
18
packages/bun-types/s3.d.ts
vendored
18
packages/bun-types/s3.d.ts
vendored
@@ -281,6 +281,24 @@ declare module "bun" {
|
||||
*/
|
||||
type?: string;
|
||||
|
||||
/**
|
||||
* The Content-Disposition header value.
|
||||
* Controls how the file is presented when downloaded.
|
||||
*
|
||||
* @example
|
||||
* // Setting attachment disposition with filename
|
||||
* const file = s3.file("report.pdf", {
|
||||
* contentDisposition: "attachment; filename=\"quarterly-report.pdf\""
|
||||
* });
|
||||
*
|
||||
* @example
|
||||
* // Setting inline disposition
|
||||
* await s3.write("image.png", imageData, {
|
||||
* contentDisposition: "inline"
|
||||
* });
|
||||
*/
|
||||
contentDisposition?: string | undefined;
|
||||
|
||||
/**
|
||||
* By default, Amazon S3 uses the STANDARD Storage Class to store newly created objects.
|
||||
*
|
||||
|
||||
@@ -968,6 +968,7 @@ fn writeFileWithEmptySourceToDestination(ctx: *jsc.JSGlobalObject, destination_b
|
||||
s3.path(),
|
||||
"",
|
||||
destination_blob.contentTypeOrMimeType(),
|
||||
aws_options.content_disposition,
|
||||
aws_options.acl,
|
||||
proxy_url,
|
||||
aws_options.storage_class,
|
||||
@@ -1116,6 +1117,7 @@ pub fn writeFileWithSourceDestination(ctx: *jsc.JSGlobalObject, source_blob: *Bl
|
||||
aws_options.acl,
|
||||
aws_options.storage_class,
|
||||
destination_blob.contentTypeOrMimeType(),
|
||||
aws_options.content_disposition,
|
||||
proxy_url,
|
||||
null,
|
||||
undefined,
|
||||
@@ -1154,6 +1156,7 @@ pub fn writeFileWithSourceDestination(ctx: *jsc.JSGlobalObject, source_blob: *Bl
|
||||
s3.path(),
|
||||
bytes.slice(),
|
||||
destination_blob.contentTypeOrMimeType(),
|
||||
aws_options.content_disposition,
|
||||
aws_options.acl,
|
||||
proxy_url,
|
||||
aws_options.storage_class,
|
||||
@@ -1183,6 +1186,7 @@ pub fn writeFileWithSourceDestination(ctx: *jsc.JSGlobalObject, source_blob: *Bl
|
||||
aws_options.acl,
|
||||
aws_options.storage_class,
|
||||
destination_blob.contentTypeOrMimeType(),
|
||||
aws_options.content_disposition,
|
||||
proxy_url,
|
||||
null,
|
||||
undefined,
|
||||
@@ -1387,6 +1391,7 @@ pub fn writeFileInternal(globalThis: *jsc.JSGlobalObject, path_or_blob_: *PathOr
|
||||
aws_options.acl,
|
||||
aws_options.storage_class,
|
||||
destination_blob.contentTypeOrMimeType(),
|
||||
aws_options.content_disposition,
|
||||
proxy_url,
|
||||
null,
|
||||
undefined,
|
||||
@@ -1447,6 +1452,7 @@ pub fn writeFileInternal(globalThis: *jsc.JSGlobalObject, path_or_blob_: *PathOr
|
||||
aws_options.acl,
|
||||
aws_options.storage_class,
|
||||
destination_blob.contentTypeOrMimeType(),
|
||||
aws_options.content_disposition,
|
||||
proxy_url,
|
||||
null,
|
||||
undefined,
|
||||
@@ -2402,6 +2408,7 @@ pub fn pipeReadableStreamToBlob(this: *Blob, globalThis: *jsc.JSGlobalObject, re
|
||||
aws_options.acl,
|
||||
aws_options.storage_class,
|
||||
this.contentTypeOrMimeType(),
|
||||
aws_options.content_disposition,
|
||||
proxy_url,
|
||||
null,
|
||||
undefined,
|
||||
@@ -2629,6 +2636,14 @@ pub fn getWriter(
|
||||
}
|
||||
}
|
||||
}
|
||||
var content_disposition_str: ?ZigString.Slice = null;
|
||||
defer if (content_disposition_str) |cd| cd.deinit();
|
||||
if (try options.getTruthy(globalThis, "contentDisposition")) |content_disposition| {
|
||||
if (!content_disposition.isString()) {
|
||||
return globalThis.throwInvalidArgumentType("write", "options.contentDisposition", "string");
|
||||
}
|
||||
content_disposition_str = try content_disposition.toSlice(globalThis, bun.default_allocator);
|
||||
}
|
||||
const credentialsWithOptions = try s3.getCredentialsWithOptions(options, globalThis);
|
||||
return try S3.writableStream(
|
||||
credentialsWithOptions.credentials.dupe(),
|
||||
@@ -2636,6 +2651,7 @@ pub fn getWriter(
|
||||
globalThis,
|
||||
credentialsWithOptions.options,
|
||||
this.contentTypeOrMimeType(),
|
||||
if (content_disposition_str) |cd| cd.slice() else null,
|
||||
proxy_url,
|
||||
credentialsWithOptions.storage_class,
|
||||
);
|
||||
@@ -2647,6 +2663,7 @@ pub fn getWriter(
|
||||
globalThis,
|
||||
.{},
|
||||
this.contentTypeOrMimeType(),
|
||||
null,
|
||||
proxy_url,
|
||||
null,
|
||||
);
|
||||
|
||||
@@ -1301,6 +1301,7 @@ pub fn Bun__fetch_(
|
||||
credentialsWithOptions.acl,
|
||||
credentialsWithOptions.storage_class,
|
||||
if (headers) |h| (h.getContentType()) else null,
|
||||
if (headers) |h| h.getContentDisposition() else null,
|
||||
proxy_url,
|
||||
@ptrCast(&Wrapper.resolve),
|
||||
s3_stream,
|
||||
|
||||
@@ -75,20 +75,12 @@ pub fn deinit(this: *Headers) void {
|
||||
this.entries.deinit(this.allocator);
|
||||
this.buf.clearAndFree(this.allocator);
|
||||
}
|
||||
pub fn getContentType(this: *const Headers) ?[]const u8 {
|
||||
if (this.entries.len == 0 or this.buf.items.len == 0) {
|
||||
return null;
|
||||
}
|
||||
const header_entries = this.entries.slice();
|
||||
const header_names = header_entries.items(.name);
|
||||
const header_values = header_entries.items(.value);
|
||||
|
||||
for (header_names, 0..header_names.len) |name, i| {
|
||||
if (bun.strings.eqlCaseInsensitiveASCII(this.asStr(name), "content-type", true)) {
|
||||
return this.asStr(header_values[i]);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
pub fn getContentDisposition(this: *const Headers) ?[]const u8 {
|
||||
return this.get("content-disposition");
|
||||
}
|
||||
pub fn getContentType(this: *const Headers) ?[]const u8 {
|
||||
return this.get("content-type");
|
||||
}
|
||||
pub fn asStr(this: *const Headers, ptr: api.StringPointer) []const u8 {
|
||||
return if (ptr.offset + ptr.length <= this.buf.items.len)
|
||||
|
||||
@@ -229,6 +229,7 @@ pub fn upload(
|
||||
path: []const u8,
|
||||
content: []const u8,
|
||||
content_type: ?[]const u8,
|
||||
content_disposition: ?[]const u8,
|
||||
acl: ?ACL,
|
||||
proxy_url: ?[]const u8,
|
||||
storage_class: ?StorageClass,
|
||||
@@ -241,6 +242,7 @@ pub fn upload(
|
||||
.proxy_url = proxy_url,
|
||||
.body = content,
|
||||
.content_type = content_type,
|
||||
.content_disposition = content_disposition,
|
||||
.acl = acl,
|
||||
.storage_class = storage_class,
|
||||
}, .{ .upload = callback }, callback_context);
|
||||
@@ -252,6 +254,7 @@ pub fn writableStream(
|
||||
globalThis: *jsc.JSGlobalObject,
|
||||
options: MultiPartUploadOptions,
|
||||
content_type: ?[]const u8,
|
||||
content_disposition: ?[]const u8,
|
||||
proxy: ?[]const u8,
|
||||
storage_class: ?StorageClass,
|
||||
) bun.JSError!jsc.JSValue {
|
||||
@@ -295,6 +298,7 @@ pub fn writableStream(
|
||||
.path = bun.handleOom(bun.default_allocator.dupe(u8, path)),
|
||||
.proxy = if (proxy_url.len > 0) bun.handleOom(bun.default_allocator.dupe(u8, proxy_url)) else "",
|
||||
.content_type = if (content_type) |ct| bun.handleOom(bun.default_allocator.dupe(u8, ct)) else null,
|
||||
.content_disposition = if (content_disposition) |cd| bun.handleOom(bun.default_allocator.dupe(u8, cd)) else null,
|
||||
.storage_class = storage_class,
|
||||
|
||||
.callback = @ptrCast(&Wrapper.callback),
|
||||
@@ -434,6 +438,7 @@ pub fn uploadStream(
|
||||
acl: ?ACL,
|
||||
storage_class: ?StorageClass,
|
||||
content_type: ?[]const u8,
|
||||
content_disposition: ?[]const u8,
|
||||
proxy: ?[]const u8,
|
||||
callback: ?*const fn (S3UploadResult, *anyopaque) void,
|
||||
callback_context: *anyopaque,
|
||||
@@ -470,6 +475,7 @@ pub fn uploadStream(
|
||||
.path = bun.handleOom(bun.default_allocator.dupe(u8, path)),
|
||||
.proxy = if (proxy_url.len > 0) bun.handleOom(bun.default_allocator.dupe(u8, proxy_url)) else "",
|
||||
.content_type = if (content_type) |ct| bun.handleOom(bun.default_allocator.dupe(u8, ct)) else null,
|
||||
.content_disposition = if (content_disposition) |cd| bun.handleOom(bun.default_allocator.dupe(u8, cd)) else null,
|
||||
.callback = @ptrCast(&S3UploadStreamWrapper.resolve),
|
||||
.callback_context = undefined,
|
||||
.globalThis = globalThis,
|
||||
|
||||
@@ -212,6 +212,21 @@ pub const S3Credentials = struct {
|
||||
if (try opts.getOptionalEnum(globalObject, "storageClass", StorageClass)) |storage_class| {
|
||||
new_credentials.storage_class = storage_class;
|
||||
}
|
||||
|
||||
if (try opts.getTruthyComptime(globalObject, "contentDisposition")) |js_value| {
|
||||
if (!js_value.isEmptyOrUndefinedOrNull()) {
|
||||
if (js_value.isString()) {
|
||||
const str = try bun.String.fromJS(js_value, globalObject);
|
||||
defer str.deref();
|
||||
if (str.tag != .Empty and str.tag != .Dead) {
|
||||
new_credentials._contentDispositionSlice = str.toUTF8(bun.default_allocator);
|
||||
new_credentials.content_disposition = new_credentials._contentDispositionSlice.?.slice();
|
||||
}
|
||||
} else {
|
||||
return globalObject.throwInvalidArgumentTypeValue("contentDisposition", "string", js_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new_credentials;
|
||||
@@ -879,17 +894,15 @@ pub const S3Credentials = struct {
|
||||
|
||||
break :brk try std.fmt.allocPrint(bun.default_allocator, "{s}://{s}{s}?{s}", .{ protocol, host, normalizedPath, url_query_string.items });
|
||||
} else {
|
||||
var encoded_content_disposition_buffer: [255]u8 = undefined;
|
||||
const encoded_content_disposition: []const u8 = if (content_disposition) |cd| encodeURIComponent(cd, &encoded_content_disposition_buffer, true) catch return error.ContentTypeIsTooLong else "";
|
||||
const canonical = brk_canonical: {
|
||||
if (content_md5) |content_md5_value| {
|
||||
if (storage_class) |storage_class_value| {
|
||||
if (acl) |acl_value| {
|
||||
if (content_disposition != null) {
|
||||
if (content_disposition) |disposition| {
|
||||
if (session_token) |token| {
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\ncontent-md5:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\nx-amz-storage-class:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, content_md5_value, host, acl_value, aws_content_hash, amz_date, token, storage_class_value, signed_headers, aws_content_hash });
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\ncontent-md5:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\nx-amz-storage-class:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", disposition, content_md5_value, host, acl_value, aws_content_hash, amz_date, token, storage_class_value, signed_headers, aws_content_hash });
|
||||
} else {
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\ncontent-md5:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-storage-class:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, content_md5_value, host, acl_value, aws_content_hash, amz_date, storage_class_value, signed_headers, aws_content_hash });
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\ncontent-md5:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-storage-class:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", disposition, content_md5_value, host, acl_value, aws_content_hash, amz_date, storage_class_value, signed_headers, aws_content_hash });
|
||||
}
|
||||
} else {
|
||||
if (session_token) |token| {
|
||||
@@ -899,11 +912,11 @@ pub const S3Credentials = struct {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (content_disposition != null) {
|
||||
if (content_disposition) |disposition| {
|
||||
if (session_token) |token| {
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\ncontent-md5:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\nx-amz-storage-class:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, content_md5_value, host, aws_content_hash, amz_date, token, storage_class_value, signed_headers, aws_content_hash });
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\ncontent-md5:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\nx-amz-storage-class:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", disposition, content_md5_value, host, aws_content_hash, amz_date, token, storage_class_value, signed_headers, aws_content_hash });
|
||||
} else {
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\ncontent-md5:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-storage-class:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, content_md5_value, host, aws_content_hash, amz_date, storage_class_value, signed_headers, aws_content_hash });
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\ncontent-md5:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-storage-class:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", disposition, content_md5_value, host, aws_content_hash, amz_date, storage_class_value, signed_headers, aws_content_hash });
|
||||
}
|
||||
} else {
|
||||
if (session_token) |token| {
|
||||
@@ -915,11 +928,11 @@ pub const S3Credentials = struct {
|
||||
}
|
||||
} else {
|
||||
if (acl) |acl_value| {
|
||||
if (content_disposition != null) {
|
||||
if (content_disposition) |disposition| {
|
||||
if (session_token) |token| {
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\ncontent-md5:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, content_md5_value, host, acl_value, aws_content_hash, amz_date, token, signed_headers, aws_content_hash });
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\ncontent-md5:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", disposition, content_md5_value, host, acl_value, aws_content_hash, amz_date, token, signed_headers, aws_content_hash });
|
||||
} else {
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\ncontent-md5:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, content_md5_value, host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash });
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\ncontent-md5:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", disposition, content_md5_value, host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash });
|
||||
}
|
||||
} else {
|
||||
if (session_token) |token| {
|
||||
@@ -929,11 +942,11 @@ pub const S3Credentials = struct {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (content_disposition != null) {
|
||||
if (content_disposition) |disposition| {
|
||||
if (session_token) |token| {
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\ncontent-md5:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, content_md5_value, host, aws_content_hash, amz_date, token, signed_headers, aws_content_hash });
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\ncontent-md5:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", disposition, content_md5_value, host, aws_content_hash, amz_date, token, signed_headers, aws_content_hash });
|
||||
} else {
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\ncontent-md5:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, content_md5_value, host, aws_content_hash, amz_date, signed_headers, aws_content_hash });
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\ncontent-md5:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", disposition, content_md5_value, host, aws_content_hash, amz_date, signed_headers, aws_content_hash });
|
||||
}
|
||||
} else {
|
||||
if (session_token) |token| {
|
||||
@@ -957,11 +970,11 @@ pub const S3Credentials = struct {
|
||||
} else {
|
||||
if (storage_class) |storage_class_value| {
|
||||
if (acl) |acl_value| {
|
||||
if (content_disposition != null) {
|
||||
if (content_disposition) |disposition| {
|
||||
if (session_token) |token| {
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\nx-amz-storage-class:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, host, acl_value, aws_content_hash, amz_date, token, storage_class_value, signed_headers, aws_content_hash });
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\nx-amz-storage-class:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", disposition, host, acl_value, aws_content_hash, amz_date, token, storage_class_value, signed_headers, aws_content_hash });
|
||||
} else {
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-storage-class:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, host, acl_value, aws_content_hash, amz_date, storage_class_value, signed_headers, aws_content_hash });
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-storage-class:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", disposition, host, acl_value, aws_content_hash, amz_date, storage_class_value, signed_headers, aws_content_hash });
|
||||
}
|
||||
} else {
|
||||
if (session_token) |token| {
|
||||
@@ -971,11 +984,11 @@ pub const S3Credentials = struct {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (content_disposition != null) {
|
||||
if (content_disposition) |disposition| {
|
||||
if (session_token) |token| {
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\nx-amz-storage-class:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, host, aws_content_hash, amz_date, token, storage_class_value, signed_headers, aws_content_hash });
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\nx-amz-storage-class:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", disposition, host, aws_content_hash, amz_date, token, storage_class_value, signed_headers, aws_content_hash });
|
||||
} else {
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-storage-class:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, host, aws_content_hash, amz_date, storage_class_value, signed_headers, aws_content_hash });
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-storage-class:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", disposition, host, aws_content_hash, amz_date, storage_class_value, signed_headers, aws_content_hash });
|
||||
}
|
||||
} else {
|
||||
if (session_token) |token| {
|
||||
@@ -987,11 +1000,11 @@ pub const S3Credentials = struct {
|
||||
}
|
||||
} else {
|
||||
if (acl) |acl_value| {
|
||||
if (content_disposition != null) {
|
||||
if (content_disposition) |disposition| {
|
||||
if (session_token) |token| {
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, host, acl_value, aws_content_hash, amz_date, token, signed_headers, aws_content_hash });
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", disposition, host, acl_value, aws_content_hash, amz_date, token, signed_headers, aws_content_hash });
|
||||
} else {
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash });
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", disposition, host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash });
|
||||
}
|
||||
} else {
|
||||
if (session_token) |token| {
|
||||
@@ -1001,11 +1014,11 @@ pub const S3Credentials = struct {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (content_disposition != null) {
|
||||
if (content_disposition) |disposition| {
|
||||
if (session_token) |token| {
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, host, aws_content_hash, amz_date, token, signed_headers, aws_content_hash });
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", disposition, host, aws_content_hash, amz_date, token, signed_headers, aws_content_hash });
|
||||
} else {
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, host, aws_content_hash, amz_date, signed_headers, aws_content_hash });
|
||||
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", disposition, host, aws_content_hash, amz_date, signed_headers, aws_content_hash });
|
||||
}
|
||||
} else {
|
||||
if (session_token) |token| {
|
||||
@@ -1079,16 +1092,15 @@ pub const S3Credentials = struct {
|
||||
result._headers[result._headers_len] = .{ .name = "x-amz-security-token", .value = session_token_value };
|
||||
result._headers_len += 1;
|
||||
}
|
||||
if (storage_class) |storage_class_value| {
|
||||
result._headers[result._headers_len] = .{ .name = "x-amz-storage-class", .value = storage_class_value };
|
||||
result._headers_len += 1;
|
||||
}
|
||||
|
||||
if (content_disposition) |cd| {
|
||||
const content_disposition_value = bun.handleOom(bun.default_allocator.dupe(u8, cd));
|
||||
result.content_disposition = content_disposition_value;
|
||||
result._headers[result._headers_len] = .{ .name = "Content-Disposition", .value = content_disposition_value };
|
||||
result._headers_len += 1;
|
||||
}
|
||||
|
||||
if (storage_class) |storage_class_value| {
|
||||
result._headers[result._headers_len] = .{ .name = "x-amz-storage-class", .value = storage_class_value };
|
||||
result._headers[result._headers_len] = .{ .name = "content-disposition", .value = content_disposition_value };
|
||||
result._headers_len += 1;
|
||||
}
|
||||
|
||||
@@ -1108,6 +1120,7 @@ pub const S3CredentialsWithOptions = struct {
|
||||
options: MultiPartUploadOptions = .{},
|
||||
acl: ?ACL = null,
|
||||
storage_class: ?StorageClass = null,
|
||||
content_disposition: ?[]const u8 = null,
|
||||
/// indicates if the credentials have changed
|
||||
changed_credentials: bool = false,
|
||||
/// indicates if the virtual hosted style is used
|
||||
@@ -1118,6 +1131,7 @@ pub const S3CredentialsWithOptions = struct {
|
||||
_endpointSlice: ?jsc.ZigString.Slice = null,
|
||||
_bucketSlice: ?jsc.ZigString.Slice = null,
|
||||
_sessionTokenSlice: ?jsc.ZigString.Slice = null,
|
||||
_contentDispositionSlice: ?jsc.ZigString.Slice = null,
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
if (this._accessKeyIdSlice) |slice| slice.deinit();
|
||||
@@ -1126,6 +1140,7 @@ pub const S3CredentialsWithOptions = struct {
|
||||
if (this._endpointSlice) |slice| slice.deinit();
|
||||
if (this._bucketSlice) |slice| slice.deinit();
|
||||
if (this._sessionTokenSlice) |slice| slice.deinit();
|
||||
if (this._contentDispositionSlice) |slice| slice.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ pub const MultiPartUpload = struct {
|
||||
path: []const u8,
|
||||
proxy: []const u8,
|
||||
content_type: ?[]const u8 = null,
|
||||
content_disposition: ?[]const u8 = null,
|
||||
upload_id: []const u8 = "",
|
||||
uploadid_buffer: bun.MutableString = .{ .allocator = bun.default_allocator, .list = .{} },
|
||||
|
||||
@@ -276,6 +277,11 @@ pub const MultiPartUpload = struct {
|
||||
bun.default_allocator.free(ct);
|
||||
}
|
||||
}
|
||||
if (this.content_disposition) |cd| {
|
||||
if (cd.len > 0) {
|
||||
bun.default_allocator.free(cd);
|
||||
}
|
||||
}
|
||||
this.credentials.deref();
|
||||
this.uploadid_buffer.deinit();
|
||||
for (this.multipart_etags.items) |tag| {
|
||||
@@ -301,6 +307,7 @@ pub const MultiPartUpload = struct {
|
||||
.proxy_url = this.proxyUrl(),
|
||||
.body = this.buffered.slice(),
|
||||
.content_type = this.content_type,
|
||||
.content_disposition = this.content_disposition,
|
||||
.acl = this.acl,
|
||||
.storage_class = this.storage_class,
|
||||
}, .{ .upload = @ptrCast(&singleSendUploadResponse) }, this);
|
||||
@@ -589,6 +596,7 @@ pub const MultiPartUpload = struct {
|
||||
.body = "",
|
||||
.search_params = "?uploads=",
|
||||
.content_type = this.content_type,
|
||||
.content_disposition = this.content_disposition,
|
||||
.acl = this.acl,
|
||||
.storage_class = this.storage_class,
|
||||
}, .{ .download = @ptrCast(&startMultiPartRequestResult) }, this);
|
||||
@@ -665,6 +673,7 @@ pub const MultiPartUpload = struct {
|
||||
.proxy_url = this.proxyUrl(),
|
||||
.body = this.buffered.slice(),
|
||||
.content_type = this.content_type,
|
||||
.content_disposition = this.content_disposition,
|
||||
.acl = this.acl,
|
||||
.storage_class = this.storage_class,
|
||||
}, .{ .upload = @ptrCast(&singleSendUploadResponse) }, this) catch {}; // TODO: properly propagate exception upwards
|
||||
|
||||
@@ -408,6 +408,43 @@ for (let credentials of allCredentials) {
|
||||
}
|
||||
});
|
||||
|
||||
it("should be able to set content-disposition", async () => {
|
||||
await using tmpfile = await tmp();
|
||||
{
|
||||
const s3file = bucket.file(tmpfile.name, options!);
|
||||
await s3file.write("Hello Bun!", { contentDisposition: 'attachment; filename="test.txt"' });
|
||||
const response = await fetch(s3file.presign());
|
||||
expect(response.headers.get("content-disposition")).toBe('attachment; filename="test.txt"');
|
||||
}
|
||||
{
|
||||
const s3file = bucket.file(tmpfile.name, options!);
|
||||
await s3file.write("Hello Bun!", { contentDisposition: "inline" });
|
||||
const response = await fetch(s3file.presign());
|
||||
expect(response.headers.get("content-disposition")).toBe("inline");
|
||||
}
|
||||
{
|
||||
await bucket.write(tmpfile.name, "Hello Bun!", {
|
||||
...options,
|
||||
contentDisposition: 'attachment; filename="report.pdf"',
|
||||
});
|
||||
const response = await fetch(bucket.file(tmpfile.name, options!).presign());
|
||||
expect(response.headers.get("content-disposition")).toBe('attachment; filename="report.pdf"');
|
||||
}
|
||||
});
|
||||
it("should be able to set content-disposition in writer", async () => {
|
||||
await using tmpfile = await tmp();
|
||||
{
|
||||
const s3file = bucket.file(tmpfile.name, options!);
|
||||
const writer = s3file.writer({
|
||||
contentDisposition: 'attachment; filename="test.txt"',
|
||||
});
|
||||
writer.write("Hello Bun!!");
|
||||
await writer.end();
|
||||
const response = await fetch(s3file.presign());
|
||||
expect(response.headers.get("content-disposition")).toBe('attachment; filename="test.txt"');
|
||||
}
|
||||
});
|
||||
|
||||
it("should be able to upload large files using bucket.write + readable Request", async () => {
|
||||
await using tmpfile = await tmp();
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user