feat(s3Client): add support for AWS S3 Object Storage Class (#16617)

This commit is contained in:
Inqnuam
2025-01-23 14:11:41 +01:00
committed by GitHub
parent b54f3f33f0
commit 98c7b8452d
13 changed files with 615 additions and 76 deletions

View File

@@ -1435,6 +1435,28 @@ declare module "bun" {
*/
type?: string;
/**
* By default, Amazon S3 uses the STANDARD Storage Class to store newly created objects.
*
* @example
* // Setting explicit Storage class
* const file = s3("my-file.json", {
* storageClass: "STANDARD_IA"
* });
*/
storageClass?:
| "STANDARD"
| "DEEP_ARCHIVE"
| "EXPRESS_ONEZONE"
| "GLACIER"
| "GLACIER_IR"
| "INTELLIGENT_TIERING"
| "ONEZONE_IA"
| "OUTPOSTS"
| "REDUCED_REDUNDANCY"
| "SNOW"
| "STANDARD_IA";
/**
* @deprecated The size of the internal buffer in bytes. Defaults to 5 MiB. use `partSize` and `queueSize` instead.
*/

View File

@@ -440,12 +440,13 @@ pub fn nodeFSStatWatcherScheduler(rare: *RareData, vm: *JSC.VirtualMachine) *Sta
pub fn s3DefaultClient(rare: *RareData, globalThis: *JSC.JSGlobalObject) JSC.JSValue {
return rare.s3_default_client.get() orelse {
const vm = globalThis.bunVM();
var aws_options = bun.S3.S3Credentials.getCredentialsWithOptions(vm.transpiler.env.getS3Credentials(), .{}, null, null, globalThis) catch bun.outOfMemory();
var aws_options = bun.S3.S3Credentials.getCredentialsWithOptions(vm.transpiler.env.getS3Credentials(), .{}, null, null, null, globalThis) catch bun.outOfMemory();
defer aws_options.deinit();
const client = JSC.WebCore.S3Client.new(.{
.credentials = aws_options.credentials.dupe(),
.options = aws_options.options,
.acl = aws_options.acl,
.storage_class = aws_options.storage_class,
});
const js_client = client.toJS(globalThis);
js_client.ensureStillAlive();

View File

@@ -94,17 +94,19 @@ pub const S3Client = struct {
credentials: *S3Credentials,
options: bun.S3.MultiPartUploadOptions = .{},
acl: ?bun.S3.ACL = null,
storage_class: ?bun.S3.StorageClass = null,
pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!*@This() {
const arguments = callframe.arguments_old(1).slice();
var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments);
defer args.deinit();
var aws_options = try S3Credentials.getCredentialsWithOptions(globalThis.bunVM().transpiler.env.getS3Credentials(), .{}, args.nextEat(), null, globalThis);
var aws_options = try S3Credentials.getCredentialsWithOptions(globalThis.bunVM().transpiler.env.getS3Credentials(), .{}, args.nextEat(), null, null, globalThis);
defer aws_options.deinit();
return S3Client.new(.{
.credentials = aws_options.credentials.dupe(),
.options = aws_options.options,
.acl = aws_options.acl,
.storage_class = aws_options.storage_class,
});
}
@@ -138,7 +140,7 @@ pub const S3Client = struct {
};
errdefer path.deinit();
const options = args.nextEat();
var blob = Blob.new(try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl));
var blob = Blob.new(try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class));
blob.allocator = bun.default_allocator;
return blob.toJS(globalThis);
}
@@ -156,7 +158,7 @@ pub const S3Client = struct {
errdefer path.deinit();
const options = args.nextEat();
var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl);
var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class);
defer blob.detach();
return S3File.getPresignUrlFrom(&blob, globalThis, options);
}
@@ -173,7 +175,7 @@ pub const S3Client = struct {
};
errdefer path.deinit();
const options = args.nextEat();
var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl);
var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class);
defer blob.detach();
return S3File.S3BlobStatTask.exists(globalThis, &blob);
}
@@ -190,7 +192,7 @@ pub const S3Client = struct {
};
errdefer path.deinit();
const options = args.nextEat();
var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl);
var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class);
defer blob.detach();
return S3File.S3BlobStatTask.size(globalThis, &blob);
}
@@ -207,7 +209,7 @@ pub const S3Client = struct {
};
errdefer path.deinit();
const options = args.nextEat();
var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl);
var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class);
defer blob.detach();
return S3File.S3BlobStatTask.stat(globalThis, &blob);
}
@@ -225,7 +227,7 @@ pub const S3Client = struct {
};
const options = args.nextEat();
var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl);
var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class);
defer blob.detach();
var blob_internal: PathOrBlob = .{ .blob = blob };
return Blob.writeFileInternal(globalThis, &blob_internal, data, .{
@@ -243,7 +245,7 @@ pub const S3Client = struct {
};
errdefer path.deinit();
const options = args.nextEat();
var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl);
var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class);
defer blob.detach();
return blob.store.?.data.s3.unlink(blob.store.?, globalThis, options);
}

View File

@@ -227,8 +227,9 @@ pub fn constructS3FileWithS3CredentialsAndOptions(
default_credentials: *S3.S3Credentials,
default_options: bun.S3.MultiPartUploadOptions,
default_acl: ?bun.S3.ACL,
default_storage_class: ?bun.S3.StorageClass,
) bun.JSError!Blob {
var aws_options = try S3.S3Credentials.getCredentialsWithOptions(default_credentials.*, default_options, options, default_acl, globalObject);
var aws_options = try S3.S3Credentials.getCredentialsWithOptions(default_credentials.*, default_options, options, default_acl, default_storage_class, globalObject);
defer aws_options.deinit();
const store = brk: {
@@ -241,6 +242,8 @@ pub fn constructS3FileWithS3CredentialsAndOptions(
errdefer store.deinit();
store.data.s3.options = aws_options.options;
store.data.s3.acl = aws_options.acl;
store.data.s3.storage_class = aws_options.storage_class;
var blob = Blob.initWithStore(store, globalObject);
if (options) |opts| {
if (opts.isObject()) {
@@ -276,12 +279,14 @@ pub fn constructS3FileWithS3Credentials(
options: ?JSC.JSValue,
existing_credentials: S3.S3Credentials,
) bun.JSError!Blob {
var aws_options = try S3.S3Credentials.getCredentialsWithOptions(existing_credentials, .{}, options, null, globalObject);
var aws_options = try S3.S3Credentials.getCredentialsWithOptions(existing_credentials, .{}, options, null, null, globalObject);
defer aws_options.deinit();
const store = Blob.Store.initS3(path, null, aws_options.credentials, bun.default_allocator) catch bun.outOfMemory();
errdefer store.deinit();
store.data.s3.options = aws_options.options;
store.data.s3.acl = aws_options.acl;
store.data.s3.storage_class = aws_options.storage_class;
var blob = Blob.initWithStore(store, globalObject);
if (options) |opts| {
if (opts.isObject()) {
@@ -465,6 +470,7 @@ pub fn getPresignUrlFrom(this: *Blob, globalThis: *JSC.JSGlobalObject, extra_opt
.path = path,
.method = method,
.acl = credentialsWithOptions.acl,
.storage_class = credentialsWithOptions.storage_class,
}, .{ .expires = expires }) catch |sign_err| {
return S3.throwSignError(sign_err, globalThis);
};

View File

@@ -928,6 +928,7 @@ pub const Blob = struct {
destination_blob.contentTypeOrMimeType(),
aws_options.acl,
proxy_url,
aws_options.storage_class,
@ptrCast(&Wrapper.resolve),
Wrapper.new(.{
.promise = promise,
@@ -1056,6 +1057,7 @@ pub const Blob = struct {
ctx,
aws_options.options,
aws_options.acl,
aws_options.storage_class,
destination_blob.contentTypeOrMimeType(),
proxy_url,
null,
@@ -1098,6 +1100,7 @@ pub const Blob = struct {
destination_blob.contentTypeOrMimeType(),
aws_options.acl,
proxy_url,
aws_options.storage_class,
@ptrCast(&Wrapper.resolve),
Wrapper.new(.{
.store = store,
@@ -1121,6 +1124,7 @@ pub const Blob = struct {
ctx,
s3.options,
aws_options.acl,
aws_options.storage_class,
destination_blob.contentTypeOrMimeType(),
proxy_url,
null,
@@ -1310,6 +1314,7 @@ pub const Blob = struct {
globalThis,
aws_options.options,
aws_options.acl,
aws_options.storage_class,
destination_blob.contentTypeOrMimeType(),
proxy_url,
null,
@@ -1369,6 +1374,7 @@ pub const Blob = struct {
globalThis,
aws_options.options,
aws_options.acl,
aws_options.storage_class,
destination_blob.contentTypeOrMimeType(),
proxy_url,
null,
@@ -3507,6 +3513,8 @@ pub const Blob = struct {
credentials: ?*S3Credentials,
options: bun.S3.MultiPartUploadOptions = .{},
acl: ?S3.ACL = null,
storage_class: ?S3.StorageClass = null,
pub fn isSeekable(_: *const @This()) ?bool {
return true;
}
@@ -3517,7 +3525,7 @@ pub const Blob = struct {
}
pub fn getCredentialsWithOptions(this: *const @This(), options: ?JSValue, globalObject: *JSC.JSGlobalObject) bun.JSError!S3.S3CredentialsWithOptions {
return S3Credentials.getCredentialsWithOptions(this.getCredentials().*, this.options, options, this.acl, globalObject);
return S3Credentials.getCredentialsWithOptions(this.getCredentials().*, this.options, options, this.acl, this.storage_class, globalObject);
}
pub fn path(this: *@This()) []const u8 {
@@ -4102,6 +4110,7 @@ pub const Blob = struct {
globalThis,
aws_options.options,
aws_options.acl,
aws_options.storage_class,
this.contentTypeOrMimeType(),
proxy_url,
null,
@@ -4339,6 +4348,7 @@ pub const Blob = struct {
credentialsWithOptions.options,
this.contentTypeOrMimeType(),
proxy_url,
credentialsWithOptions.storage_class,
);
}
}
@@ -4349,6 +4359,7 @@ pub const Blob = struct {
.{},
this.contentTypeOrMimeType(),
proxy_url,
null,
);
}
if (store.data != .file) {

View File

@@ -3256,6 +3256,7 @@ pub const Fetch = struct {
.credentials = globalThis.bunVM().transpiler.env.getS3Credentials(),
.options = .{},
.acl = null,
.storage_class = null,
};
defer {
credentialsWithOptions.deinit();
@@ -3265,7 +3266,7 @@ pub const Fetch = struct {
if (try options.getTruthyComptime(globalThis, "s3")) |s3_options| {
if (s3_options.isObject()) {
s3_options.ensureStillAlive();
credentialsWithOptions = try s3.S3Credentials.getCredentialsWithOptions(credentialsWithOptions.credentials, .{}, s3_options, null, globalThis);
credentialsWithOptions = try s3.S3Credentials.getCredentialsWithOptions(credentialsWithOptions.credentials, .{}, s3_options, null, null, globalThis);
}
}
}
@@ -3341,6 +3342,7 @@ pub const Fetch = struct {
globalThis,
credentialsWithOptions.options,
credentialsWithOptions.acl,
credentialsWithOptions.storage_class,
if (headers) |h| h.getContentType() else null,
proxy_url,
@ptrCast(&Wrapper.resolve),

View File

@@ -7,6 +7,7 @@ pub const ACL = @import("./acl.zig").ACL;
pub const S3HttpDownloadStreamingTask = @import("./download_stream.zig").S3HttpDownloadStreamingTask;
pub const MultiPartUploadOptions = @import("./multipart_options.zig").MultiPartUploadOptions;
pub const MultiPartUpload = @import("./multipart.zig").MultiPartUpload;
pub const StorageClass = @import("./storage_class.zig").StorageClass;
pub const Error = @import("./error.zig");
pub const throwSignError = Error.throwSignError;
@@ -105,6 +106,7 @@ pub fn upload(
content_type: ?[]const u8,
acl: ?ACL,
proxy_url: ?[]const u8,
storage_class: ?StorageClass,
callback: *const fn (S3UploadResult, *anyopaque) void,
callback_context: *anyopaque,
) void {
@@ -115,6 +117,7 @@ pub fn upload(
.body = content,
.content_type = content_type,
.acl = acl,
.storage_class = storage_class,
}, .{ .upload = callback }, callback_context);
}
/// returns a writable stream that writes to the s3 path
@@ -125,6 +128,7 @@ pub fn writableStream(
options: MultiPartUploadOptions,
content_type: ?[]const u8,
proxy: ?[]const u8,
storage_class: ?StorageClass,
) bun.JSError!JSC.JSValue {
const Wrapper = struct {
pub fn callback(result: S3UploadResult, sink: *JSC.WebCore.NetworkSink) void {
@@ -158,6 +162,7 @@ pub fn writableStream(
.path = bun.default_allocator.dupe(u8, path) catch bun.outOfMemory(),
.proxy = if (proxy_url.len > 0) bun.default_allocator.dupe(u8, proxy_url) catch bun.outOfMemory() else "",
.content_type = if (content_type) |ct| bun.default_allocator.dupe(u8, ct) catch bun.outOfMemory() else null,
.storage_class = storage_class,
.callback = @ptrCast(&Wrapper.callback),
.callback_context = undefined,
@@ -290,6 +295,7 @@ pub fn uploadStream(
globalThis: *JSC.JSGlobalObject,
options: MultiPartUploadOptions,
acl: ?ACL,
storage_class: ?StorageClass,
content_type: ?[]const u8,
proxy: ?[]const u8,
callback: ?*const fn (S3UploadResult, *anyopaque) void,
@@ -333,6 +339,7 @@ pub fn uploadStream(
.state = .wait_stream_check,
.options = options,
.acl = acl,
.storage_class = storage_class,
.vm = JSC.VirtualMachine.get(),
});

View File

@@ -4,6 +4,8 @@ const std = @import("std");
const MultiPartUploadOptions = @import("./multipart_options.zig").MultiPartUploadOptions;
const ACL = @import("./acl.zig").ACL;
const StorageClass = @import("./storage_class.zig").StorageClass;
const JSC = bun.JSC;
const RareData = JSC.RareData;
const strings = bun.strings;
@@ -16,7 +18,7 @@ pub const S3Credentials = struct {
endpoint: []const u8,
bucket: []const u8,
sessionToken: []const u8,
storage_class: ?StorageClass = null,
/// Important for MinIO support.
insecure_http: bool = false,
@@ -42,12 +44,13 @@ pub const S3Credentials = struct {
return hasher.final();
}
pub fn getCredentialsWithOptions(this: S3Credentials, default_options: MultiPartUploadOptions, options: ?JSC.JSValue, default_acl: ?ACL, globalObject: *JSC.JSGlobalObject) bun.JSError!S3CredentialsWithOptions {
pub fn getCredentialsWithOptions(this: S3Credentials, default_options: MultiPartUploadOptions, options: ?JSC.JSValue, default_acl: ?ACL, default_storage_class: ?StorageClass, globalObject: *JSC.JSGlobalObject) bun.JSError!S3CredentialsWithOptions {
// get ENV config
var new_credentials = S3CredentialsWithOptions{
.credentials = this,
.options = default_options,
.acl = default_acl,
.storage_class = default_storage_class,
};
errdefer {
new_credentials.deinit();
@@ -197,6 +200,10 @@ pub const S3Credentials = struct {
if (try opts.getOptionalEnum(globalObject, "acl", ACL)) |acl| {
new_credentials.acl = acl;
}
if (try opts.getOptionalEnum(globalObject, "storageClass", StorageClass)) |storage_class| {
new_credentials.storage_class = storage_class;
}
}
}
return new_credentials;
@@ -313,7 +320,9 @@ pub const S3Credentials = struct {
content_disposition: []const u8 = "",
session_token: []const u8 = "",
acl: ?ACL = null,
_headers: [7]picohttp.Header = .{
storage_class: ?StorageClass = null,
_headers: [8]picohttp.Header = .{
.{ .name = "", .value = "" },
.{ .name = "", .value = "" },
.{ .name = "", .value = "" },
.{ .name = "", .value = "" },
@@ -375,6 +384,7 @@ pub const S3Credentials = struct {
search_params: ?[]const u8 = null,
content_disposition: ?[]const u8 = null,
acl: ?ACL = null,
storage_class: ?StorageClass = null,
};
pub fn guessRegion(endpoint: []const u8) []const u8 {
@@ -448,6 +458,8 @@ pub const S3Credentials = struct {
const acl: ?[]const u8 = if (signOptions.acl) |acl_value| acl_value.toString() else null;
const storage_class: ?[]const u8 = if (signOptions.storage_class) |storage_class| storage_class.toString() else null;
if (this.accessKeyId.len == 0 or this.secretAccessKey.len == 0) return error.MissingCredentials;
const signQuery = signQueryOption != null;
const expires = if (signQueryOption) |options| options.expires else 0;
@@ -519,32 +531,64 @@ pub const S3Credentials = struct {
const amz_day = amz_date[0..8];
const signed_headers = if (signQuery) "host" else brk: {
if (acl != null) {
if (content_disposition != null) {
if (session_token != null) {
break :brk "content-disposition;host;x-amz-acl;x-amz-content-sha256;x-amz-date;x-amz-security-token";
if (storage_class != null) {
if (acl != null) {
if (content_disposition != null) {
if (session_token != null) {
break :brk "content-disposition;host;x-amz-acl;x-amz-content-sha256;x-amz-date;x-amz-storage-class;x-amz-security-token";
} else {
break :brk "content-disposition;host;x-amz-acl;x-amz-content-sha256;x-amz-date;x-amz-storage-class";
}
} else {
break :brk "content-disposition;host;x-amz-acl;x-amz-content-sha256;x-amz-date";
if (session_token != null) {
break :brk "host;x-amz-acl;x-amz-content-sha256;x-amz-date;x-amz-storage-class;x-amz-security-token";
} else {
break :brk "host;x-amz-acl;x-amz-content-sha256;x-amz-date;x-amz-storage-class";
}
}
} else {
if (session_token != null) {
break :brk "host;x-amz-acl;x-amz-content-sha256;x-amz-date;x-amz-security-token";
if (content_disposition != null) {
if (session_token != null) {
break :brk "content-disposition;host;x-amz-content-sha256;x-amz-date;x-amz-storage-class;x-amz-security-token";
} else {
break :brk "content-disposition;host;x-amz-content-sha256;x-amz-date;x-amz-storage-class";
}
} else {
break :brk "host;x-amz-acl;x-amz-content-sha256;x-amz-date";
if (session_token != null) {
break :brk "host;x-amz-content-sha256;x-amz-date;x-amz-storage-class;x-amz-security-token";
} else {
break :brk "host;x-amz-content-sha256;x-amz-date;x-amz-storage-class";
}
}
}
} else {
if (content_disposition != null) {
if (session_token != null) {
break :brk "content-disposition;host;x-amz-content-sha256;x-amz-date;x-amz-security-token";
if (acl != null) {
if (content_disposition != null) {
if (session_token != null) {
break :brk "content-disposition;host;x-amz-acl;x-amz-content-sha256;x-amz-date;x-amz-security-token";
} else {
break :brk "content-disposition;host;x-amz-acl;x-amz-content-sha256;x-amz-date";
}
} else {
break :brk "content-disposition;host;x-amz-content-sha256;x-amz-date";
if (session_token != null) {
break :brk "host;x-amz-acl;x-amz-content-sha256;x-amz-date;x-amz-security-token";
} else {
break :brk "host;x-amz-acl;x-amz-content-sha256;x-amz-date";
}
}
} else {
if (session_token != null) {
break :brk "host;x-amz-content-sha256;x-amz-date;x-amz-security-token";
if (content_disposition != null) {
if (session_token != null) {
break :brk "content-disposition;host;x-amz-content-sha256;x-amz-date;x-amz-security-token";
} else {
break :brk "content-disposition;host;x-amz-content-sha256;x-amz-date";
}
} else {
break :brk "host;x-amz-content-sha256;x-amz-date";
if (session_token != null) {
break :brk "host;x-amz-content-sha256;x-amz-date;x-amz-security-token";
} else {
break :brk "host;x-amz-content-sha256;x-amz-date";
}
}
}
}
@@ -596,17 +640,33 @@ pub const S3Credentials = struct {
encoded_session_token = encodeURIComponent(token, &token_encoded_buffer, true) catch return error.InvalidSessionToken;
}
const canonical = brk_canonical: {
if (acl) |acl_value| {
if (encoded_session_token) |token| {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, host, signed_headers, aws_content_hash });
if (storage_class) |storage_class_value| {
if (acl) |acl_value| {
if (encoded_session_token) |token| {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&x-amz-storage-class={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, storage_class_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, host, signed_headers, aws_content_hash });
} else {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&x-amz-storage-class={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, storage_class_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, host, signed_headers, aws_content_hash });
}
} else {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, host, signed_headers, aws_content_hash });
if (encoded_session_token) |token| {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nx-amz-storage-class={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, storage_class_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, host, signed_headers, aws_content_hash });
} else {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nx-amz-storage-class={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, storage_class_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, host, signed_headers, aws_content_hash });
}
}
} else {
if (encoded_session_token) |token| {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, host, signed_headers, aws_content_hash });
if (acl) |acl_value| {
if (encoded_session_token) |token| {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, host, signed_headers, aws_content_hash });
} else {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, host, signed_headers, aws_content_hash });
}
} else {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, host, signed_headers, aws_content_hash });
if (encoded_session_token) |token| {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, host, signed_headers, aws_content_hash });
} else {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, host, signed_headers, aws_content_hash });
}
}
}
};
@@ -616,65 +676,130 @@ pub const S3Credentials = struct {
const signValue = try std.fmt.bufPrint(&tmp_buffer, "AWS4-HMAC-SHA256\n{s}\n{s}/{s}/{s}/aws4_request\n{s}", .{ amz_date, amz_day, region, service_name, bun.fmt.bytesToHex(sha_digest[0..bun.sha.SHA256.digest], .lower) });
const signature = bun.hmac.generate(sigDateRegionServiceReq, signValue, .sha256, &hmac_sig_service) orelse return error.FailedToGenerateSignature;
if (acl) |acl_value| {
if (encoded_session_token) |token| {
break :brk try std.fmt.allocPrint(
bun.default_allocator,
"{s}://{s}{s}?X-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}",
.{ protocol, host, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) },
);
if (storage_class) |storage_class_value| {
if (acl) |acl_value| {
if (encoded_session_token) |token| {
break :brk try std.fmt.allocPrint(
bun.default_allocator,
"{s}://{s}{s}?X-Amz-Acl={s}&x-amz-storage-class={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}",
.{ protocol, host, normalizedPath, acl_value, storage_class_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) },
);
} else {
break :brk try std.fmt.allocPrint(
bun.default_allocator,
"{s}://{s}{s}?X-Amz-Acl={s}&x-amz-storage-class={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}",
.{ protocol, host, normalizedPath, acl_value, storage_class_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) },
);
}
} else {
break :brk try std.fmt.allocPrint(
bun.default_allocator,
"{s}://{s}{s}?X-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}",
.{ protocol, host, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) },
);
if (encoded_session_token) |token| {
break :brk try std.fmt.allocPrint(
bun.default_allocator,
"{s}://{s}{s}?x-amz-storage-class={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}",
.{ protocol, host, normalizedPath, storage_class_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) },
);
} else {
break :brk try std.fmt.allocPrint(
bun.default_allocator,
"{s}://{s}{s}?x-amz-storage-class={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}",
.{ protocol, host, normalizedPath, storage_class_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) },
);
}
}
} else {
if (encoded_session_token) |token| {
break :brk try std.fmt.allocPrint(
bun.default_allocator,
"{s}://{s}{s}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}",
.{ protocol, host, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) },
);
if (acl) |acl_value| {
if (encoded_session_token) |token| {
break :brk try std.fmt.allocPrint(
bun.default_allocator,
"{s}://{s}{s}?X-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}",
.{ protocol, host, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) },
);
} else {
break :brk try std.fmt.allocPrint(
bun.default_allocator,
"{s}://{s}{s}?X-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}",
.{ protocol, host, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) },
);
}
} else {
break :brk try std.fmt.allocPrint(
bun.default_allocator,
"{s}://{s}{s}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}",
.{ protocol, host, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) },
);
if (encoded_session_token) |token| {
break :brk try std.fmt.allocPrint(
bun.default_allocator,
"{s}://{s}{s}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}",
.{ protocol, host, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) },
);
} else {
break :brk try std.fmt.allocPrint(
bun.default_allocator,
"{s}://{s}{s}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}",
.{ protocol, host, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) },
);
}
}
}
} 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 (acl) |acl_value| {
if (content_disposition != null) {
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 });
if (storage_class) |storage_class_value| {
if (acl) |acl_value| {
if (content_disposition != null) {
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-storage-class:{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, storage_class_value, 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}\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 });
}
} 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 });
if (session_token) |token| {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-storage-class:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", host, acl_value, aws_content_hash, amz_date, storage_class_value, token, signed_headers, aws_content_hash });
} else {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{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 "", host, acl_value, aws_content_hash, amz_date, storage_class_value, signed_headers, aws_content_hash });
}
}
} else {
if (session_token) |token| {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{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 "", host, acl_value, aws_content_hash, amz_date, token, signed_headers, aws_content_hash });
if (content_disposition != null) {
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-storage-class:{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, storage_class_value, 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}\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 });
}
} else {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{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 "", host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash });
if (session_token) |token| {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-storage-class:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", host, aws_content_hash, amz_date, storage_class_value, token, signed_headers, aws_content_hash });
} else {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{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 "", host, aws_content_hash, amz_date, storage_class_value, signed_headers, aws_content_hash });
}
}
}
} else {
if (content_disposition != null) {
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 });
if (acl) |acl_value| {
if (content_disposition != null) {
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 });
} 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 });
}
} 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 });
if (session_token) |token| {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{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 "", 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}\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 "", host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash });
}
}
} else {
if (session_token) |token| {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{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 "", host, aws_content_hash, amz_date, token, signed_headers, aws_content_hash });
if (content_disposition != null) {
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 });
} 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 });
}
} else {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{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 "", host, aws_content_hash, amz_date, signed_headers, aws_content_hash });
if (session_token) |token| {
break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{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 "", 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}\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 "", host, aws_content_hash, amz_date, signed_headers, aws_content_hash });
}
}
}
}
@@ -705,6 +830,7 @@ pub const S3Credentials = struct {
.authorization = "",
.acl = signOptions.acl,
.url = authorization,
.storage_class = signOptions.storage_class,
};
}
@@ -713,6 +839,7 @@ pub const S3Credentials = struct {
.host = host,
.authorization = authorization,
.acl = signOptions.acl,
.storage_class = signOptions.storage_class,
.url = try std.fmt.allocPrint(bun.default_allocator, "{s}://{s}{s}{s}", .{ protocol, host, normalizedPath, if (search_params) |s| s else "" }),
._headers = [_]picohttp.Header{
.{ .name = "x-amz-content-sha256", .value = aws_content_hash },
@@ -722,6 +849,7 @@ pub const S3Credentials = struct {
.{ .name = "", .value = "" },
.{ .name = "", .value = "" },
.{ .name = "", .value = "" },
.{ .name = "", .value = "" },
},
._headers_len = 4,
};
@@ -745,6 +873,11 @@ pub const S3Credentials = struct {
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;
}
return result;
}
};
@@ -753,6 +886,7 @@ pub const S3CredentialsWithOptions = struct {
credentials: S3Credentials,
options: MultiPartUploadOptions = .{},
acl: ?ACL = null,
storage_class: ?StorageClass = null,
/// indicates if the credentials have changed
changed_credentials: bool = false,

View File

@@ -3,6 +3,7 @@ const bun = @import("root").bun;
const strings = bun.strings;
const S3Credentials = @import("./credentials.zig").S3Credentials;
const ACL = @import("./acl.zig").ACL;
const Storageclass = @import("./storage_class.zig").StorageClass;
const JSC = bun.JSC;
const MultiPartUploadOptions = @import("./multipart_options.zig").MultiPartUploadOptions;
const S3SimpleRequest = @import("./simple_request.zig");
@@ -25,6 +26,7 @@ pub const MultiPartUpload = struct {
options: MultiPartUploadOptions = .{},
acl: ?ACL = null,
storage_class: ?Storageclass = null,
credentials: *S3Credentials,
poll_ref: bun.Async.KeepAlive = bun.Async.KeepAlive.init(),
vm: *JSC.VirtualMachine,
@@ -216,6 +218,7 @@ pub const MultiPartUpload = struct {
.body = this.buffered.items,
.content_type = this.content_type,
.acl = this.acl,
.storage_class = this.storage_class,
}, .{ .upload = @ptrCast(&singleSendUploadResponse) }, this);
return;
@@ -457,6 +460,7 @@ pub const MultiPartUpload = struct {
.search_params = "?uploads=",
.content_type = this.content_type,
.acl = this.acl,
.storage_class = this.storage_class,
}, .{ .download = @ptrCast(&startMultiPartRequestResult) }, this);
} else if (this.state == .multipart_completed) {
part.start();
@@ -532,6 +536,7 @@ pub const MultiPartUpload = struct {
.body = this.buffered.items,
.content_type = this.content_type,
.acl = this.acl,
.storage_class = this.storage_class,
}, .{ .upload = @ptrCast(&singleSendUploadResponse) }, this);
} else {
// we need to split

View File

@@ -8,6 +8,8 @@ const getSignErrorCodeAndMessage = @import("./error.zig").getSignErrorCodeAndMes
const S3Credentials = @import("./credentials.zig").S3Credentials;
const picohttp = bun.picohttp;
const ACL = @import("./acl.zig").ACL;
const StorageClass = @import("./storage_class.zig").StorageClass;
pub const S3StatResult = union(enum) {
success: struct {
size: usize = 0,
@@ -333,6 +335,7 @@ pub const S3SimpleRequestOptions = struct {
proxy_url: ?[]const u8 = null,
range: ?[]const u8 = null,
acl: ?ACL = null,
storage_class: ?StorageClass = null,
};
pub fn executeSimpleS3Request(
@@ -347,6 +350,7 @@ pub fn executeSimpleS3Request(
.search_params = options.search_params,
.content_disposition = options.content_disposition,
.acl = options.acl,
.storage_class = options.storage_class,
}, null) catch |sign_err| {
if (options.range) |range_| bun.default_allocator.free(range_);
const error_code_and_message = getSignErrorCodeAndMessage(sign_err);

45
src/s3/storage_class.zig Normal file
View File

@@ -0,0 +1,45 @@
const bun = @import("root").bun;
pub const StorageClass = enum {
STANDARD,
STANDARD_IA,
INTELLIGENT_TIERING,
EXPRESS_ONEZONE,
ONEZONE_IA,
GLACIER,
GLACIER_IR,
REDUCED_REDUNDANCY,
OUTPOSTS,
DEEP_ARCHIVE,
SNOW,
pub fn toString(this: @This()) []const u8 {
return switch (this) {
.STANDARD => "STANDARD",
.STANDARD_IA => "STANDARD_IA",
.INTELLIGENT_TIERING => "INTELLIGENT_TIERING",
.EXPRESS_ONEZONE => "EXPRESS_ONEZONE",
.ONEZONE_IA => "ONEZONE_IA",
.GLACIER => "GLACIER",
.GLACIER_IR => "GLACIER_IR",
.REDUCED_REDUNDANCY => "REDUCED_REDUNDANCY",
.OUTPOSTS => "OUTPOSTS",
.DEEP_ARCHIVE => "DEEP_ARCHIVE",
.SNOW => "SNOW",
};
}
pub const Map = bun.ComptimeStringMap(StorageClass, .{
.{ "STANDARD", .STANDARD },
.{ "STANDARD_IA", .STANDARD_IA },
.{ "INTELLIGENT_TIERING", .INTELLIGENT_TIERING },
.{ "EXPRESS_ONEZONE", .EXPRESS_ONEZONE },
.{ "ONEZONE_IA", .ONEZONE_IA },
.{ "GLACIER", .GLACIER },
.{ "GLACIER_IR", .GLACIER_IR },
.{ "REDUCED_REDUNDANCY", .REDUCED_REDUNDANCY },
.{ "OUTPOSTS", .OUTPOSTS },
.{ "DEEP_ARCHIVE", .DEEP_ARCHIVE },
.{ "SNOW", .SNOW },
});
};

View File

@@ -0,0 +1,284 @@
import { describe, it, expect } from "bun:test";
import { s3, S3Client, type S3Options } from "bun";
import { randomUUID } from "node:crypto";
describe("s3 - Storage class", () => {
const s3Options: S3Options = {
accessKeyId: "test",
secretAccessKey: "test",
region: "eu-west-3",
bucket: "my_bucket",
};
it("should throw TypeError if storage class isnt one of enum", async () => {
try {
new S3Client({
...s3Options,
endpoint: "anything",
// @ts-expect-error not an enum
storageClass: "INVALID_VALUE",
}).file("instance_file");
expect.unreachable();
} catch (e) {
expect(e).toBeInstanceOf(TypeError);
}
});
it("should work with static .file() method", async () => {
let reqHeaders: Headers | undefined = undefined;
using server = Bun.serve({
port: 0,
async fetch(req) {
reqHeaders = req.headers;
return new Response("", {
headers: {
"Content-Type": "text/plain",
},
status: 200,
});
},
});
const storageClass = "STANDARD_IA";
await S3Client.file("from_static_file", {
...s3Options,
endpoint: server.url.href,
storageClass,
}).write("This is a good file");
expect(reqHeaders!.get("authorization")).toInclude("x-amz-storage-class");
expect(reqHeaders!.get("x-amz-storage-class")).toBe(storageClass);
});
it("should work with static .write() method", async () => {
let reqHeaders: Headers | undefined = undefined;
using server = Bun.serve({
port: 0,
async fetch(req) {
reqHeaders = req.headers;
return new Response("", {
headers: {
"Content-Type": "text/plain",
},
status: 200,
});
},
});
const storageClass = "REDUCED_REDUNDANCY";
await S3Client.write("from_static_write", "This is a good file", {
...s3Options,
endpoint: server.url.href,
storageClass,
});
expect(reqHeaders!.get("authorization")).toInclude("x-amz-storage-class");
expect(reqHeaders!.get("x-amz-storage-class")).toBe(storageClass);
});
it("should work with static presign", () => {
const storageClass = "DEEP_ARCHIVE";
const result = S3Client.file("awsome_file").presign({
...s3Options,
storageClass,
});
expect(result).toInclude(`x-amz-storage-class=${storageClass}`);
});
it("should work with instance options + .file() method", async () => {
let reqHeaders: Headers | undefined = undefined;
using server = Bun.serve({
port: 0,
async fetch(req) {
reqHeaders = req.headers;
return new Response("", {
headers: {
"Content-Type": "text/plain",
},
status: 200,
});
},
});
const storageClass = "ONEZONE_IA";
const s3 = new S3Client({
...s3Options,
endpoint: server.url.href,
storageClass,
});
const file = s3.file("instance_file");
await file.write("Some content");
expect(reqHeaders!.get("authorization")).toInclude("x-amz-storage-class");
expect(reqHeaders!.get("x-amz-storage-class")).toBe(storageClass);
});
it("should work with instance .file() method + options", async () => {
let reqHeaders: Headers | undefined = undefined;
using server = Bun.serve({
port: 0,
async fetch(req) {
reqHeaders = req.headers;
return new Response("", {
headers: {
"Content-Type": "text/plain",
},
status: 200,
});
},
});
const storageClass = "SNOW";
const file = new S3Client({
...s3Options,
endpoint: server.url.href,
}).file("instance_file", { storageClass });
await file.write("Some content");
expect(reqHeaders!.get("authorization")).toInclude("x-amz-storage-class");
expect(reqHeaders!.get("x-amz-storage-class")).toBe(storageClass);
});
it("should work with writer + options on small file", async () => {
let reqHeaders: Headers | undefined = undefined;
using server = Bun.serve({
port: 0,
async fetch(req) {
reqHeaders = req.headers;
return new Response("", {
headers: {
"Content-Type": "text/plain",
},
status: 200,
});
},
});
const storageClass = "SNOW";
const s3 = new S3Client({
...s3Options,
endpoint: server.url.href,
});
const writer = s3.file("file_from_writer").writer({ storageClass });
const smallFile = Buffer.alloc(10 * 1024);
for (let i = 0; i < 10; i++) {
await writer.write(smallFile);
}
await writer.end();
expect(reqHeaders!.get("authorization")).toInclude("x-amz-storage-class");
expect(reqHeaders!.get("x-amz-storage-class")).toBe(storageClass);
});
it(
"should work with writer + options on big file",
async () => {
let reqHeaders: Headers | undefined = undefined;
using server = Bun.serve({
port: 0,
async fetch(req) {
const isCreateMultipartUploadRequest = req.method == "POST" && req.url.includes("?uploads=");
if (isCreateMultipartUploadRequest) {
reqHeaders = req.headers;
return new Response(
`<InitiateMultipartUploadResult>
<Bucket>my_bucket</Bucket>
<Key>file_from_writer</Key>
<UploadId>${randomUUID()}</UploadId>
</InitiateMultipartUploadResult>`,
{
headers: {
"Content-Type": "text/xml",
},
status: 200,
},
);
}
const isCompleteMultipartUploadRequets = req.method == "POST" && req.url.includes("uploadId=");
if (isCompleteMultipartUploadRequets) {
return new Response(
`<CompleteMultipartUploadResult>
<Location>http://my_bucket.s3.<Region>.amazonaws.com/file_from_writer</Location>
<Bucket>my_bucket</Bucket>
<Key>file_from_writer</Key>
<ETag>"f9a5ddddf9e0fcbd05c15bb44b389171-20"</ETag>
</CompleteMultipartUploadResult>`,
{
headers: {
"Content-Type": "text/xml",
},
status: 200,
},
);
}
return new Response(undefined, { status: 200, headers: { "Etag": `"f9a5ddddf9e0fcbd05c15bb44b389171-20"` } });
},
});
const storageClass = "SNOW";
const s3 = new S3Client({
...s3Options,
endpoint: server.url.href,
});
const writer = s3.file("file_from_writer").writer({
storageClass,
queueSize: 10,
partSize: 5 * 1024,
});
const bigFile = Buffer.alloc(10 * 1024 * 1024);
for (let i = 0; i < 10; i++) {
await writer.write(bigFile);
}
await writer.end();
expect(reqHeaders!.get("authorization")).toInclude("x-amz-storage-class");
expect(reqHeaders!.get("x-amz-storage-class")).toBe(storageClass);
},
{ timeout: 20_000 },
);
it("should work with default s3 instance", async () => {
let reqHeaders: Headers | undefined = undefined;
using server = Bun.serve({
port: 0,
async fetch(req) {
reqHeaders = req.headers;
return new Response("", {
headers: {
"Content-Type": "text/plain",
},
status: 200,
});
},
});
const storageClass = "INTELLIGENT_TIERING";
await s3.file("my_file", { ...s3Options, storageClass, endpoint: server.url.href }).write("any thing");
expect(reqHeaders!.get("authorization")).toInclude("x-amz-storage-class");
expect(reqHeaders!.get("x-amz-storage-class")).toBe(storageClass);
});
});

View File

@@ -996,6 +996,22 @@ for (let credentials of allCredentials) {
expect(url.includes("X-Amz-SignedHeaders")).toBe(true);
});
it("should work with storage class", async () => {
const s3file = s3("s3://bucket/credentials-test", s3Options);
const url = s3file.presign({
expiresIn: 10,
storageClass: "GLACIER_IR",
});
expect(url).toBeDefined();
expect(url.includes("X-Amz-Expires=10")).toBe(true);
expect(url.includes("x-amz-storage-class=GLACIER_IR")).toBe(true);
expect(url.includes("X-Amz-Date")).toBe(true);
expect(url.includes("X-Amz-Signature")).toBe(true);
expect(url.includes("X-Amz-Credential")).toBe(true);
expect(url.includes("X-Amz-Algorithm")).toBe(true);
expect(url.includes("X-Amz-SignedHeaders")).toBe(true);
});
it("s3().presign() should work", async () => {
const url = s3("s3://bucket/credentials-test", s3Options).presign({
expiresIn: 10,