diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index 4a3afe2313ae9d..753f7eaac8f475 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -1,5 +1,6 @@ const conv = std.builtin.CallingConvention.Unspecified; - +const S3File = @import("../webcore/S3File.zig"); +const S3Bucket = @import("../webcore/S3Bucket.zig"); /// How to add a new function or property to the Bun global /// /// - Add a callback or property to the below struct @@ -31,7 +32,8 @@ pub const BunObject = struct { pub const registerMacro = toJSCallback(Bun.registerMacro); pub const resolve = toJSCallback(Bun.resolve); pub const resolveSync = toJSCallback(Bun.resolveSync); - pub const s3 = toJSCallback(WebCore.Blob.constructS3File); + pub const s3 = toJSCallback(S3File.constructS3File); + pub const S3 = S3Bucket.createJSS3Bucket; pub const serve = toJSCallback(Bun.serve); pub const sha = toJSCallback(JSC.wrapStaticMethod(Crypto.SHA512_256, "hash_", true)); pub const shellEscape = toJSCallback(Bun.shellEscape); @@ -57,7 +59,6 @@ pub const BunObject = struct { pub const SHA384 = toJSGetter(Crypto.SHA384.getter); pub const SHA512 = toJSGetter(Crypto.SHA512.getter); pub const SHA512_256 = toJSGetter(Crypto.SHA512_256.getter); - pub const S3 = toJSGetter(JSC.WebCore.Blob.getJSS3FileConstructor); pub const TOML = toJSGetter(Bun.getTOMLObject); pub const Transpiler = toJSGetter(Bun.getTranspilerConstructor); pub const argv = toJSGetter(Bun.getArgv); @@ -110,7 +111,6 @@ pub const BunObject = struct { @export(BunObject.FileSystemRouter, .{ .name = getterName("FileSystemRouter") }); @export(BunObject.MD4, .{ .name = getterName("MD4") }); @export(BunObject.MD5, .{ .name = getterName("MD5") }); - @export(BunObject.S3, .{ .name = getterName("S3") }); @export(BunObject.SHA1, .{ .name = getterName("SHA1") }); @export(BunObject.SHA224, .{ .name = getterName("SHA224") }); @export(BunObject.SHA256, .{ .name = getterName("SHA256") }); @@ -160,6 +160,7 @@ pub const BunObject = struct { @export(BunObject.resolveSync, .{ .name = callbackName("resolveSync") }); @export(BunObject.serve, .{ .name = callbackName("serve") }); @export(BunObject.s3, .{ .name = callbackName("s3") }); + @export(BunObject.S3, .{ .name = callbackName("S3") }); @export(BunObject.sha, .{ .name = callbackName("sha") }); @export(BunObject.shellEscape, .{ .name = callbackName("shellEscape") }); @export(BunObject.shrink, .{ .name = callbackName("shrink") }); diff --git a/src/bun.js/bindings/BunObject+exports.h b/src/bun.js/bindings/BunObject+exports.h index d4f267b8227dcb..b638d6eb26d846 100644 --- a/src/bun.js/bindings/BunObject+exports.h +++ b/src/bun.js/bindings/BunObject+exports.h @@ -17,7 +17,6 @@ macro(SHA512_256) \ macro(TOML) \ macro(Transpiler) \ - macro(S3) \ macro(argv) \ macro(assetPrefix) \ macro(cwd) \ @@ -59,6 +58,7 @@ macro(resolve) \ macro(resolveSync) \ macro(s3) \ + macro(S3) \ macro(serve) \ macro(sha) \ macro(shrink) \ diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index 502e8cf796ca7d..8dffcbdd201ba2 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -576,7 +576,6 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj Glob BunObject_getter_wrap_Glob DontDelete|PropertyCallback MD4 BunObject_getter_wrap_MD4 DontDelete|PropertyCallback MD5 BunObject_getter_wrap_MD5 DontDelete|PropertyCallback - S3 BunObject_getter_wrap_S3 DontDelete|PropertyCallback SHA1 BunObject_getter_wrap_SHA1 DontDelete|PropertyCallback SHA224 BunObject_getter_wrap_SHA224 DontDelete|PropertyCallback SHA256 BunObject_getter_wrap_SHA256 DontDelete|PropertyCallback @@ -639,6 +638,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj revision constructBunRevision ReadOnly|DontDelete|PropertyCallback semver BunObject_getter_wrap_semver ReadOnly|DontDelete|PropertyCallback s3 BunObject_callback_s3 DontDelete|Function 1 + S3 BunObject_callback_S3 DontDelete|Function 1 sql constructBunSQLObject DontDelete|PropertyCallback serve BunObject_callback_serve DontDelete|Function 1 sha BunObject_callback_sha DontDelete|Function 1 diff --git a/src/bun.js/bindings/JSS3Bucket.cpp b/src/bun.js/bindings/JSS3Bucket.cpp index 99c8b28ac5a8f2..0dcdffd2e4efa9 100644 --- a/src/bun.js/bindings/JSS3Bucket.cpp +++ b/src/bun.js/bindings/JSS3Bucket.cpp @@ -24,6 +24,8 @@ SYSV_ABI EncodedJSValue JSS3Bucket__call(void* ptr, JSC::JSGlobalObject*, JSC::C SYSV_ABI EncodedJSValue JSS3Bucket__unlink(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); SYSV_ABI EncodedJSValue JSS3Bucket__write(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); SYSV_ABI EncodedJSValue JSS3Bucket__presign(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); +SYSV_ABI EncodedJSValue JSS3Bucket__exists(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); +SYSV_ABI EncodedJSValue JSS3Bucket__size(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); SYSV_ABI void* JSS3Bucket__deinit(void* ptr); } @@ -31,11 +33,15 @@ SYSV_ABI void* JSS3Bucket__deinit(void* ptr); JSC_DECLARE_HOST_FUNCTION(functionS3Bucket_unlink); JSC_DECLARE_HOST_FUNCTION(functionS3Bucket_write); JSC_DECLARE_HOST_FUNCTION(functionS3Bucket_presign); +JSC_DECLARE_HOST_FUNCTION(functionS3Bucket_exists); +JSC_DECLARE_HOST_FUNCTION(functionS3Bucket_size); static const HashTableValue JSS3BucketPrototypeTableValues[] = { { "unlink"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_unlink, 0 } }, { "write"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_write, 1 } }, { "presign"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_presign, 1 } }, + { "exists"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_exists, 1 } }, + { "size"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_size, 1 } }, }; class JSS3BucketPrototype final : public JSC::JSNonFinalObject { @@ -106,8 +112,8 @@ JSC::GCClient::IsoSubspace* JSS3Bucket::subspaceForImpl(JSC::VM& vm) vm, [](auto& spaces) { return spaces.m_clientSubspaceForJSS3Bucket.get(); }, [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSS3Bucket = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForS3m_clientSubspaceForJSS3Bucket.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForS3m_clientSubspaceForJSS3Bucket = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForJSS3Bucket.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForJSS3Bucket = std::forward(space); }, [](auto& server) -> JSC::HeapCellType& { return server.m_heapCellTypeForJSWorkerGlobalScope; }); } @@ -191,6 +197,32 @@ JSC_DEFINE_HOST_FUNCTION(functionS3Bucket_presign, (JSGlobalObject * globalObjec return JSS3Bucket__presign(thisObject->ptr, globalObject, callframe); } +JSC_DEFINE_HOST_FUNCTION(functionS3Bucket_exists, (JSGlobalObject * globalObject, CallFrame* callframe)) +{ + auto* thisObject = jsDynamicCast(callframe->thisValue()); + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3Bucket instance"_s); + return {}; + } + + return JSS3Bucket__exists(thisObject->ptr, globalObject, callframe); +} + +JSC_DEFINE_HOST_FUNCTION(functionS3Bucket_size, (JSGlobalObject * globalObject, CallFrame* callframe)) +{ + auto* thisObject = jsDynamicCast(callframe->thisValue()); + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3Bucket instance"_s); + return {}; + } + + return JSS3Bucket__size(thisObject->ptr, globalObject, callframe); +} + JSValue constructS3Bucket(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe) { auto& vm = globalObject->vm(); @@ -201,6 +233,10 @@ JSValue constructS3Bucket(JSC::JSGlobalObject* globalObject, JSC::CallFrame* cal return JSS3Bucket::create(vm, defaultGlobalObject(globalObject), ptr); } +SYSV_ABI JSValue BUN__createJSS3Bucket(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe) +{ + return constructS3Bucket(globalObject, callframe); +}; Structure* createJSS3BucketStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) { diff --git a/src/bun.js/bindings/JSS3Bucket.h b/src/bun.js/bindings/JSS3Bucket.h index 9620e50caad9be..f6c878b6759bd5 100644 --- a/src/bun.js/bindings/JSS3Bucket.h +++ b/src/bun.js/bindings/JSS3Bucket.h @@ -10,9 +10,10 @@ using namespace JSC; class JSS3Bucket : public JSC::JSFunction { using Base = JSC::JSFunction; static constexpr unsigned StructureFlags = Base::StructureFlags; - static constexpr bool needsDestruction = true; public: + static constexpr bool needsDestruction = true; + JSS3Bucket(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, NativeExecutable* executable, void* ptr); DECLARE_INFO; diff --git a/src/bun.js/bindings/JSS3File.cpp b/src/bun.js/bindings/JSS3File.cpp index 07b0ab94299c82..7c7f7b38db2123 100644 --- a/src/bun.js/bindings/JSS3File.cpp +++ b/src/bun.js/bindings/JSS3File.cpp @@ -34,7 +34,7 @@ JSC::JSObject* createJSS3FileStaticObject(JSC::VM& vm, JSC::JSGlobalObject* glob } extern "C" { -JSC::EncodedJSValue BUN__createJSS3FileConstructor(JSGlobalObject* lexicalGlobalObject) +JSC::EncodedJSValue SYSV_ABI BUN__createJSS3FileConstructor(JSGlobalObject* lexicalGlobalObject) { return JSValue::encode(defaultGlobalObject(lexicalGlobalObject)->JSS3FileConstructor()); diff --git a/src/bun.js/webcore/S3Bucket.zig b/src/bun.js/webcore/S3Bucket.zig index 83dcf282cff20e..6db6f8d94427d5 100644 --- a/src/bun.js/webcore/S3Bucket.zig +++ b/src/bun.js/webcore/S3Bucket.zig @@ -13,24 +13,108 @@ pub fn presign(ptr: *AWSCredentials, globalThis: *JSC.JSGlobalObject, callframe: var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); defer args.deinit(); const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { - return globalThis.throwInvalidArguments("S3Bucket.prototype..presign(pathOrS3, options) expects a path to presign", .{}); + return globalThis.throwInvalidArguments("S3Bucket.prototype..presign(path, options) expects a path to presign", .{}); }; defer path.deinit(); const options = args.nextEat(); - var blob = try S3File.constructS3FileWithAWSCredentials(globalThis, path, options, ptr.*); + var blob = try S3File.constructS3FileWithAWSCredentialsNoCloneIfPossible(globalThis, path, options, ptr.*); defer blob.detach(); return S3File.getPresignUrlFrom(&blob, globalThis, options); } +pub fn exists(ptr: *AWSCredentials, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + return globalThis.throwInvalidArguments("S3Bucket.prototype..exists(path) expects a path to check if it exists", .{}); + }; + defer path.deinit(); + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithAWSCredentialsNoCloneIfPossible(globalThis, path, options, ptr.*); + defer blob.detach(); + return Blob.getExists(blob, globalThis, callframe); +} + +pub fn size(ptr: *AWSCredentials, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + return globalThis.throwInvalidArguments("S3Bucket.prototype..size(path) expects a path to check the size of", .{}); + }; + defer path.deinit(); + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithAWSCredentialsNoCloneIfPossible(globalThis, path, options, ptr.*); + defer blob.detach(); + return Blob.getSize(blob, globalThis, callframe); +} + +pub fn write(ptr: *AWSCredentials, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + return globalThis.throwInvalidArguments("S3Bucket.prototype..write(path, data) expects a path to write to", .{}); + }; + defer path.deinit(); + const data = args.nextEat() orelse { + return globalThis.throwInvalidArguments("S3Bucket.prototype..write(path, data) expects a Blob-y thing to write", .{}); + }; + + const options = args.nextEat(); + //TODO: replace this because we dont wanna to clone the AWS credentials we wanna to ref/unref + var blob = try S3File.constructS3FileWithAWSCredentialsNoCloneIfPossible(globalThis, path, options, ptr.*); + defer blob.detach(); + + return Blob.writeFileInternal(globalThis, &blob, data, .{ + .mkdirp_if_not_exists = false, + .extra_options = options, + }); +} + +pub fn unlink(ptr: *AWSCredentials, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + return globalThis.throwInvalidArguments("S3Bucket.prototype..unlink(path) expects a path to unlink", .{}); + }; + defer path.deinit(); + const options = args.nextEat(); + //TODO: replace this because we dont wanna to clone the AWS credentials we wanna to ref/unref + var blob = try S3File.constructS3FileWithAWSCredentialsNoCloneIfPossible(globalThis, path, options, ptr.*); + defer blob.detach(); + return blob.store.?.data.s3.unlink(globalThis, options); +} + // Rest of the methods ... pub fn finalize(ptr: *AWSCredentials) void { ptr.deref(); } -pub const exports = struct {}; +pub const exports = struct { + pub const JSS3Bucket__exists = JSC.toJSHostFunction(exists); + pub const JSS3Bucket__size = JSC.toJSHostFunction(size); + pub const JSS3Bucket__write = JSC.toJSHostFunction(write); + pub const JSS3Bucket__unlink = JSC.toJSHostFunction(unlink); + pub const JSS3Bucket__presign = JSC.toJSHostFunction(presign); + pub const JSS3Bucket__deinit = JSC.toJSHostFunction(finalize); +}; +extern fn BUN__createJSS3Bucket(*JSC.JSGlobalObject, *JSC.CallFrame) callconv(JSC.conv) JSValue; +pub fn createJSS3Bucket( + globalObject: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, +) callconv(JSC.conv) JSValue { + return BUN__createJSS3Bucket(globalObject, callframe); +} comptime { - // ...each of the exports + @export(exports.JSS3Bucket__exists, .{ .name = "JSS3Bucket__exists" }); + @export(exports.JSS3Bucket__size, .{ .name = "JSS3Bucket__size" }); + @export(exports.JSS3Bucket__write, .{ .name = "JSS3Bucket__write" }); + @export(exports.JSS3Bucket__unlink, .{ .name = "JSS3Bucket__unlink" }); + @export(exports.JSS3Bucket__presign, .{ .name = "JSS3Bucket__presign" }); } diff --git a/src/bun.js/webcore/S3File.zig b/src/bun.js/webcore/S3File.zig index 3f9ae057d3d7ba..0eb4208fb00764 100644 --- a/src/bun.js/webcore/S3File.zig +++ b/src/bun.js/webcore/S3File.zig @@ -197,7 +197,45 @@ fn constructS3FileInternalStore( const existing_credentials = globalObject.bunVM().transpiler.env.getAWSCredentials(); return constructS3FileWithAWSCredentials(globalObject, path, options, existing_credentials); } +/// if the credentials have changed, we need to clone it, if not we can just ref/deref it +pub fn constructS3FileWithAWSCredentialsNoCloneIfPossible( + globalObject: *JSC.JSGlobalObject, + path: JSC.Node.PathLike, + options: ?JSC.JSValue, + existing_credentials: *AWS.AWSCredentials, +) bun.JSError!Blob { + var aws_options = try AWS.getCredentialsWithOptions(existing_credentials, options, globalObject); + defer aws_options.deinit(); + const store = if (aws_options.credentials.changed_credentials) Blob.Store.initS3(path, null, aws_options.credentials, bun.default_allocator) catch bun.outOfMemory() else Blob.Store.initS3WithReferencedCredentials(path, null, existing_credentials); + errdefer store.deinit(); + store.data.s3.options = aws_options.options; + var blob = Blob.initWithStore(store, globalObject); + if (options) |opts| { + if (try opts.getTruthy(globalObject, "type")) |file_type| { + inner: { + if (file_type.isString()) { + var allocator = bun.default_allocator; + var str = file_type.toSlice(globalObject, bun.default_allocator); + defer str.deinit(); + const slice = str.slice(); + if (!strings.isAllASCII(slice)) { + break :inner; + } + blob.content_type_was_set = true; + if (globalObject.bunVM().mimeType(str.slice())) |entry| { + blob.content_type = entry.value; + break :inner; + } + const content_type_buf = allocator.alloc(u8, slice.len) catch bun.outOfMemory(); + blob.content_type = strings.copyLowercase(slice, content_type_buf); + blob.content_type_allocated = true; + } + } + } + } + return blob; +} pub fn constructS3FileWithAWSCredentials( globalObject: *JSC.JSGlobalObject, path: JSC.Node.PathLike, @@ -273,6 +311,7 @@ const AWS = bun.S3.AWSCredentials; pub const S3BlobStatTask = struct { promise: JSC.JSPromise.Strong, + store: *Blob.Store, usingnamespace bun.New(S3BlobStatTask); pub fn onS3ExistsResolved(result: AWS.S3StatResult, this: *S3BlobStatTask) void { @@ -303,8 +342,8 @@ pub const S3BlobStatTask = struct { switch (result) { .not_found => { const js_err = globalThis - .ERR_S3_FILE_NOT_FOUND("File {} not found", .{bun.fmt.quote(this.blob.store.?.data.s3.path())}).toJS(globalThis); - js_err.put(globalThis, ZigString.static("path"), ZigString.init(this.blob.store.?.data.s3.path()).withEncoding()); + .ERR_S3_FILE_NOT_FOUND("File {} not found", .{bun.fmt.quote(this.store.data.s3.path())}).toJS(); + js_err.put(globalThis, ZigString.static("path"), ZigString.init(this.store.data.s3.path()).withEncoding().toJS(globalThis)); this.promise.rejectOnNextTick(globalThis, js_err); }, @@ -320,7 +359,9 @@ pub const S3BlobStatTask = struct { pub fn exists(globalThis: *JSC.JSGlobalObject, blob: *Blob) JSValue { const this = S3BlobStatTask.new(.{ .promise = JSC.JSPromise.Strong.init(globalThis), + .store = blob.store.?, }); + this.store.ref(); const promise = this.promise.value(); const credentials = blob.store.?.data.s3.getCredentials(); const path = blob.store.?.data.s3.path(); @@ -333,7 +374,9 @@ pub const S3BlobStatTask = struct { pub fn size(globalThis: *JSC.JSGlobalObject, blob: *Blob) JSValue { const this = S3BlobStatTask.new(.{ .promise = JSC.JSPromise.Strong.init(globalThis), + .store = blob.store.?, }); + this.store.ref(); const promise = this.promise.value(); const credentials = blob.store.?.data.s3.getCredentials(); const path = blob.store.?.data.s3.path(); @@ -344,6 +387,7 @@ pub const S3BlobStatTask = struct { } pub fn deinit(this: *S3BlobStatTask) void { + this.store.deref(); this.promise.deinit(); this.destroy(); } diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 80b338e4828ad5..b6a6a6c5f4dc01 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -1893,7 +1893,34 @@ pub const Blob = struct { var this = bun.cast(*Store, ptr); this.deref(); } + pub fn initS3WithReferencedCredentials(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: *AWS.AWSCredentials, allocator: std.mem.Allocator) !*Store { + var path = pathlike; + // this actually protects/refs the pathlike + path.toThreadSafe(); + const store = Blob.Store.new(.{ + .data = .{ + .s3 = S3Store.initWithReferencedCredentials( + path, + mime_type orelse brk: { + const sliced = path.slice(); + if (sliced.len > 0) { + var extname = std.fs.path.extension(sliced); + extname = std.mem.trim(u8, extname, "."); + if (http.MimeType.byExtensionNoDefault(extname)) |mime| { + break :brk mime; + } + } + break :brk null; + }, + credentials, + ), + }, + .allocator = allocator, + .ref_count = std.atomic.Value(u32).init(1), + }); + return store; + } pub fn initS3(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: AWSCredentials, allocator: std.mem.Allocator) !*Store { var path = pathlike; // this actually protects/refs the pathlike @@ -3445,6 +3472,7 @@ pub const Blob = struct { pub fn unlink(this: *@This(), globalThis: *JSC.JSGlobalObject, extra_options: ?JSValue) bun.JSError!JSValue { const Wrapper = struct { promise: JSC.JSPromise.Strong, + store: *S3Store, pub usingnamespace bun.New(@This()); @@ -3456,8 +3484,9 @@ pub const Blob = struct { self.promise.resolve(globalObject, .true); }, .not_found => { - const js_err = globalObject.createErrorInstance("File not found", .{}); - js_err.put(globalObject, ZigString.static("code"), ZigString.init("FileNotFound").toJS(globalObject)); + const js_err = globalObject + .ERR_S3_FILE_NOT_FOUND("File {} not found", .{bun.fmt.quote(self.store.path())}).toJS(); + js_err.put(globalObject, ZigString.static("path"), ZigString.init(self.store.path()).withEncoding().toJS(globalObject)); self.promise.reject(globalObject, js_err); }, .failure => |err| { @@ -3479,11 +3508,19 @@ pub const Blob = struct { defer aws_options.deinit(); aws_options.credentials.s3Delete(this.path(), @ptrCast(&Wrapper.resolve), Wrapper.new(.{ .promise = promise, + .store = this, }), proxy); return value; } - + pub fn initWithReferencedCredentials(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: *AWS.AWSCredentials) S3Store { + credentials.ref(); + return .{ + .credentials = credentials, + .pathlike = pathlike, + .mime_type = mime_type orelse http.MimeType.other, + }; + } pub fn init(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: AWSCredentials) S3Store { return .{ .credentials = credentials.dupe(), diff --git a/src/s3.zig b/src/s3.zig index aa12dadd49629e..0d0fb9278851e6 100644 --- a/src/s3.zig +++ b/src/s3.zig @@ -24,6 +24,8 @@ pub const AWSCredentials = struct { pub const AWSCredentialsWithOptions = struct { credentials: AWSCredentials, options: MultiPartUpload.MultiPartUploadOptions = .{}, + /// indicates if the credentials have changed + changed_credentials: bool = false, _accessKeyIdSlice: ?JSC.ZigString.Slice = null, _secretAccessKeySlice: ?JSC.ZigString.Slice = null, @@ -59,6 +61,7 @@ pub const AWSCredentials = struct { if (str.tag != .Empty and str.tag != .Dead) { new_credentials._accessKeyIdSlice = str.toUTF8(bun.default_allocator); new_credentials.credentials.accessKeyId = new_credentials._accessKeyIdSlice.?.slice(); + new_credentials.changed_credentials = true; } } else { return globalObject.throwInvalidArgumentTypeValue("accessKeyId", "string", js_value); @@ -73,6 +76,7 @@ pub const AWSCredentials = struct { if (str.tag != .Empty and str.tag != .Dead) { new_credentials._secretAccessKeySlice = str.toUTF8(bun.default_allocator); new_credentials.credentials.secretAccessKey = new_credentials._secretAccessKeySlice.?.slice(); + new_credentials.changed_credentials = true; } } else { return globalObject.throwInvalidArgumentTypeValue("secretAccessKey", "string", js_value); @@ -87,6 +91,7 @@ pub const AWSCredentials = struct { if (str.tag != .Empty and str.tag != .Dead) { new_credentials._regionSlice = str.toUTF8(bun.default_allocator); new_credentials.credentials.region = new_credentials._regionSlice.?.slice(); + new_credentials.changed_credentials = true; } } else { return globalObject.throwInvalidArgumentTypeValue("region", "string", js_value); @@ -103,6 +108,7 @@ pub const AWSCredentials = struct { const normalized_endpoint = bun.URL.parse(new_credentials._endpointSlice.?.slice()).host; if (normalized_endpoint.len > 0) { new_credentials.credentials.endpoint = normalized_endpoint; + new_credentials.changed_credentials = true; } } } else { @@ -118,6 +124,7 @@ pub const AWSCredentials = struct { if (str.tag != .Empty and str.tag != .Dead) { new_credentials._bucketSlice = str.toUTF8(bun.default_allocator); new_credentials.credentials.bucket = new_credentials._bucketSlice.?.slice(); + new_credentials.changed_credentials = true; } } else { return globalObject.throwInvalidArgumentTypeValue("bucket", "string", js_value); @@ -147,6 +154,18 @@ pub const AWSCredentials = struct { new_credentials.options.queueSize = @intCast(@max(queueSize, std.math.maxInt(u8))); } } + + if (try opts.getOptional(globalObject, "retry", i32)) |retry| { + if (retry < 0 and retry > 255) { + return globalObject.throwRangeError(retry, .{ + .min = 0, + .max = 255, + .field_name = "retry", + }); + } else { + new_credentials.options.retry = @intCast(retry); + } + } } } return new_credentials;