Skip to content

Commit 3f423f3

Browse files
committed
feat: S3 support
Signed-off-by: Nathan <[email protected]>
1 parent b72d183 commit 3f423f3

File tree

6 files changed

+1291
-0
lines changed

6 files changed

+1291
-0
lines changed

gradle/elide.versions.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ auto-common = "1.2.2"
2727
auto-factory = "1.1.0"
2828
auto-service = "1.1.1"
2929
auto-value = "1.11.0"
30+
aws-sdk = "2.31.74"
3031
bouncycastle = "1.81"
3132
brotli = "0.1.2"
3233
brotli4j = "1.18.0"
@@ -194,6 +195,7 @@ larray = "0.2.1"
194195
lettuce = "6.2.5.RELEASE"
195196
lmaxDisruptor = "4.0.0"
196197
lmaxDisruptorProxy = "3.1.1"
198+
locals3 = "1.25"
197199
logback = "1.5.18"
198200
lz4 = "1.3.0"
199201
markdown = "0.7.3"
@@ -363,6 +365,8 @@ asm-core = { group = "org.ow2.asm", name = "asm", version.ref = "asm" }
363365
asm-tree = { group = "org.ow2.asm", name = "asm-tree", version.ref = "asm" }
364366
assertk = { group = "com.willowtreeapps.assertk", name = "assertk", version.ref = "assertk" }
365367
autoService-ksp = { module = "dev.zacsweers.autoservice:auto-service-ksp", version = "1.1.0" }
368+
aws-sdk-bom = { group = "software.amazon.awssdk", name = "bom", version.ref = "aws-sdk" }
369+
aws-sdk-s3 = { group = "software.amazon.awssdk", name = "s3" }
366370
bouncycastle = { group = "org.bouncycastle", name = "bcprov-jdk18on", version.ref = "bouncycastle" }
367371
bouncycastle-pkix = { group = "org.bouncycastle", name = "bcpkix-jdk18on", version.ref = "bouncycastle" }
368372
bouncycastle-tls = { group = "org.bouncycastle", name = "bctls-jdk18on", version.ref = "bouncycastle" }
@@ -706,6 +710,7 @@ larray-mmap = { group = "org.xerial.larray", name = "larray-mmap", version.ref =
706710
lettuce-core = { group = "io.lettuce", name = "lettuce-core", version.ref = "lettuce" }
707711
lmax-disruptor-core = { group = "com.lmax", name = "disruptor", version.ref = "lmaxDisruptor" }
708712
lmax-disruptor-proxy = { group = "com.lmax", name = "disruptor-proxy", version.ref = "lmaxDisruptorProxy" }
713+
locals3 = { group = "io.github.robothy", name = "local-s3-rest", version.ref = "locals3" }
709714
logback = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" }
710715
logback-core = { group = "ch.qos.logback", name = "logback-core", version.ref = "logback" }
711716
lz4 = { group = "net.jpountz.lz4", name = "lz4", version.ref = "lz4" }

packages/graalvm/api/graalvm.api

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6526,6 +6526,104 @@ public abstract interface class elide/runtime/intrinsics/js/node/zlib/ZlibOption
65266526
public abstract fun getWindowBits ()I
65276527
}
65286528

6529+
public synthetic class elide/runtime/intrinsics/js/s3/$S3Module$Definition : io/micronaut/context/AbstractInitializableBeanDefinitionAndReference {
6530+
public static final field $ANNOTATION_METADATA Lio/micronaut/core/annotation/AnnotationMetadata;
6531+
public fun <init> ()V
6532+
protected fun <init> (Ljava/lang/Class;Lio/micronaut/context/AbstractInitializableBeanDefinition$MethodOrFieldReference;)V
6533+
public fun instantiate (Lio/micronaut/context/BeanResolutionContext;Lio/micronaut/context/BeanContext;)Ljava/lang/Object;
6534+
public fun isEnabled (Lio/micronaut/context/BeanContext;)Z
6535+
public fun isEnabled (Lio/micronaut/context/BeanContext;Lio/micronaut/context/BeanResolutionContext;)Z
6536+
public fun load ()Lio/micronaut/inject/BeanDefinition;
6537+
}
6538+
6539+
public final synthetic class elide/runtime/intrinsics/js/s3/$S3Module$Introspection : io/micronaut/inject/beans/AbstractInitializableBeanIntrospectionAndReference {
6540+
public static final field $ANNOTATION_METADATA Lio/micronaut/core/annotation/AnnotationMetadata;
6541+
public fun <init> ()V
6542+
public fun hasBuilder ()Z
6543+
public fun isBuildable ()Z
6544+
}
6545+
6546+
public abstract interface class elide/runtime/intrinsics/js/s3/S3Client : org/graalvm/polyglot/proxy/ProxyObject {
6547+
public abstract fun file (Ljava/lang/String;)Lelide/runtime/intrinsics/js/s3/S3File;
6548+
}
6549+
6550+
public final class elide/runtime/intrinsics/js/s3/S3ClientConstructorOptions : java/lang/Record {
6551+
public static final field Companion Lelide/runtime/intrinsics/js/s3/S3ClientConstructorOptions$Companion;
6552+
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)V
6553+
public final fun accessKeyId ()Ljava/lang/String;
6554+
public final fun acl ()Ljava/lang/String;
6555+
public final fun bucket ()Ljava/lang/String;
6556+
public final fun component1 ()Ljava/lang/String;
6557+
public final fun component2 ()Ljava/lang/String;
6558+
public final fun component3 ()Ljava/lang/String;
6559+
public final fun component4 ()Ljava/lang/String;
6560+
public final fun component5 ()Ljava/lang/String;
6561+
public final fun component6 ()Ljava/lang/String;
6562+
public final fun component7 ()Ljava/lang/String;
6563+
public final fun component8 ()Ljava/lang/Boolean;
6564+
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)Lelide/runtime/intrinsics/js/s3/S3ClientConstructorOptions;
6565+
public static synthetic fun copy$default (Lelide/runtime/intrinsics/js/s3/S3ClientConstructorOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;ILjava/lang/Object;)Lelide/runtime/intrinsics/js/s3/S3ClientConstructorOptions;
6566+
public final fun endpoint ()Ljava/lang/String;
6567+
public fun equals (Ljava/lang/Object;)Z
6568+
public static final fun from (Lorg/graalvm/polyglot/Value;)Lelide/runtime/intrinsics/js/s3/S3ClientConstructorOptions;
6569+
public fun hashCode ()I
6570+
public final fun region ()Ljava/lang/String;
6571+
public final fun secretAccessKey ()Ljava/lang/String;
6572+
public final fun sessionToken ()Ljava/lang/String;
6573+
public fun toString ()Ljava/lang/String;
6574+
public final fun virtualHostedStyle ()Ljava/lang/Boolean;
6575+
}
6576+
6577+
public final class elide/runtime/intrinsics/js/s3/S3ClientConstructorOptions$Companion {
6578+
public final fun from (Lorg/graalvm/polyglot/Value;)Lelide/runtime/intrinsics/js/s3/S3ClientConstructorOptions;
6579+
}
6580+
6581+
public final class elide/runtime/intrinsics/js/s3/S3ClientProxy : elide/runtime/intrinsics/js/s3/S3Client {
6582+
public fun <init> (Lelide/runtime/exec/GuestExecutorProvider;Lelide/runtime/intrinsics/js/s3/S3ClientConstructorOptions;)V
6583+
public fun file (Ljava/lang/String;)Lelide/runtime/intrinsics/js/s3/S3File;
6584+
public fun getMember (Ljava/lang/String;)Ljava/lang/Object;
6585+
public fun getMemberKeys ()Ljava/lang/Object;
6586+
public fun hasMember (Ljava/lang/String;)Z
6587+
public fun putMember (Ljava/lang/String;Lorg/graalvm/polyglot/Value;)Ljava/lang/Void;
6588+
public synthetic fun putMember (Ljava/lang/String;Lorg/graalvm/polyglot/Value;)V
6589+
}
6590+
6591+
public abstract interface class elide/runtime/intrinsics/js/s3/S3File : org/graalvm/polyglot/proxy/ProxyObject {
6592+
public abstract fun arrayBuffer ()Lelide/runtime/intrinsics/js/JsPromise;
6593+
public abstract fun bytes ()Lelide/runtime/intrinsics/js/JsPromise;
6594+
public abstract fun delete ()Lelide/runtime/intrinsics/js/JsPromise;
6595+
public abstract fun exists ()Lelide/runtime/intrinsics/js/JsPromise;
6596+
public abstract fun json ()Lelide/runtime/intrinsics/js/JsPromise;
6597+
public abstract fun presign (Ljava/lang/String;J)Ljava/lang/String;
6598+
public abstract fun stat ()Lelide/runtime/intrinsics/js/JsPromise;
6599+
public abstract fun text ()Lelide/runtime/intrinsics/js/JsPromise;
6600+
public abstract fun write (Lelide/runtime/intrinsics/js/Blob;Ljava/lang/String;)Lelide/runtime/intrinsics/js/JsPromise;
6601+
public abstract fun write (Lelide/runtime/intrinsics/js/ReadableStream;Ljava/lang/String;)Lelide/runtime/intrinsics/js/JsPromise;
6602+
public abstract fun write (Ljava/lang/String;Ljava/lang/String;)Lelide/runtime/intrinsics/js/JsPromise;
6603+
public abstract fun write-AgME8lA (Lorg/graalvm/polyglot/Value;Ljava/lang/String;)Lelide/runtime/intrinsics/js/JsPromise;
6604+
}
6605+
6606+
public final class elide/runtime/intrinsics/js/s3/S3FileProxy : elide/runtime/intrinsics/js/s3/S3File {
6607+
public fun <init> (Lelide/runtime/exec/GuestExecutorProvider;Lsoftware/amazon/awssdk/services/s3/S3AsyncClient;Lsoftware/amazon/awssdk/regions/Region;Lsoftware/amazon/awssdk/auth/credentials/AwsCredentialsProvider;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
6608+
public fun arrayBuffer ()Lelide/runtime/intrinsics/js/JsPromise;
6609+
public fun bytes ()Lelide/runtime/intrinsics/js/JsPromise;
6610+
public fun delete ()Lelide/runtime/intrinsics/js/JsPromise;
6611+
public fun exists ()Lelide/runtime/intrinsics/js/JsPromise;
6612+
public fun getMember (Ljava/lang/String;)Ljava/lang/Object;
6613+
public fun getMemberKeys ()Ljava/lang/Object;
6614+
public fun hasMember (Ljava/lang/String;)Z
6615+
public fun json ()Lelide/runtime/intrinsics/js/JsPromise;
6616+
public fun presign (Ljava/lang/String;J)Ljava/lang/String;
6617+
public fun putMember (Ljava/lang/String;Lorg/graalvm/polyglot/Value;)Ljava/lang/Void;
6618+
public synthetic fun putMember (Ljava/lang/String;Lorg/graalvm/polyglot/Value;)V
6619+
public fun stat ()Lelide/runtime/intrinsics/js/JsPromise;
6620+
public fun text ()Lelide/runtime/intrinsics/js/JsPromise;
6621+
public fun write (Lelide/runtime/intrinsics/js/Blob;Ljava/lang/String;)Lelide/runtime/intrinsics/js/JsPromise;
6622+
public fun write (Lelide/runtime/intrinsics/js/ReadableStream;Ljava/lang/String;)Lelide/runtime/intrinsics/js/JsPromise;
6623+
public fun write (Ljava/lang/String;Ljava/lang/String;)Lelide/runtime/intrinsics/js/JsPromise;
6624+
public fun write-AgME8lA (Lorg/graalvm/polyglot/Value;Ljava/lang/String;)Lelide/runtime/intrinsics/js/JsPromise;
6625+
}
6626+
65296627
public final class elide/runtime/intrinsics/js/stream/ByteLengthQueuingStrategy : elide/runtime/intrinsics/js/stream/QueuingStrategy {
65306628
public static final field Companion Lelide/runtime/intrinsics/js/stream/ByteLengthQueuingStrategy$Companion;
65316629
public static final synthetic fun box-impl (D)Lelide/runtime/intrinsics/js/stream/ByteLengthQueuingStrategy;

packages/graalvm/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,10 +555,14 @@ dependencies {
555555
api(libs.graalvm.polyglot.js.community)
556556
}
557557

558+
implementation(platform(libs.aws.sdk.bom))
559+
implementation(libs.aws.sdk.s3)
560+
558561
// Testing
559562
testApi(project(":packages:engine", configuration = "testInternals"))
560563
testApi(libs.graalvm.truffle.api)
561564
testApi(libs.graalvm.truffle.runtime)
565+
testImplementation(libs.locals3)
562566
testImplementation(libs.jackson.core)
563567
testImplementation(libs.jackson.databind)
564568
testImplementation(libs.jackson.module.kotlin)
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Copyright (c) 2024-2025 Elide Technologies, Inc.
3+
*
4+
* Licensed under the MIT license (the "License"); you may not use this file except in compliance
5+
* with the License. You may obtain a copy of the License at
6+
*
7+
* https://opensource.org/license/mit/
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11+
* License for the specific language governing permissions and limitations under the License.
12+
*/
13+
package elide.runtime.intrinsics.js.s3
14+
15+
import org.graalvm.polyglot.Value
16+
import org.graalvm.polyglot.proxy.ProxyObject
17+
import elide.runtime.gvm.js.JsError
18+
import elide.runtime.intrinsics.js.Blob
19+
import elide.runtime.intrinsics.js.JsPromise
20+
import elide.runtime.intrinsics.js.ReadableStream
21+
import elide.runtime.node.buffer.GuestBytes
22+
import elide.vm.annotations.Polyglot
23+
24+
private const val ACCESS_KEY_ID = "accessKeyId"
25+
private const val SECRET_ACCESS_KEY = "secretAccessKey"
26+
private const val BUCKET = "bucket"
27+
private const val SESSION_TOKEN = "sessionToken"
28+
private const val ACL = "acl"
29+
private const val ENDPOINT = "endpoint"
30+
private const val REGION = "region"
31+
private const val VIRTUAL_HOSTED_STYLE = "virtualHostedStyle"
32+
33+
/**
34+
* # S3
35+
*
36+
* Facade for calling out to AWS S3 SDK. Meant to be used to provide an S3
37+
* API from within the Elide runtime.
38+
*
39+
* It consists of two major objects, the S3Client which tracks the configuration for authentication and how to connect
40+
* and the S3File which represents a file in S3 that can be read, written to, or presigned.
41+
*
42+
* &nbsp;
43+
*
44+
* ## Usage
45+
*
46+
* In the following example, the S3Client will connect to Backblaze B2 (an S3 compatible service)
47+
* using a path style endpoint. If the endpoint is not specified, by default it will use AWS S3.
48+
* If region is unspecified it will use us-east-1.
49+
* It will use path style URLs by default.
50+
*
51+
* Please look at AWS's S3 documentation or the documentation for whatever S3 compatible API is being used for more
52+
* information on what these options mean.
53+
*
54+
* ```javascript
55+
* const { S3Client } = require("elide:s3");
56+
* const client = new S3Client({
57+
* // required options
58+
* accessKeyId: "...",
59+
* secretAccessKey: "...",
60+
* bucket: "my-bucket",
61+
* // optional options
62+
* sessionToken: "...",
63+
* acl: "public-read",
64+
* endpoint: "https://s3.us-east-005.backblazeb2.com/my-bucket",
65+
* region: "us-east-005",
66+
* virtualHostedStyle: false
67+
* });
68+
* const file = client.file("test.txt");
69+
* await file.write("This is a test", { type: "text/plain" });
70+
* console.assert(await file.text() === "This is a test");
71+
* ```
72+
*
73+
* Write method takes either a String or any array type object and returns a promise
74+
* containing the content length.
75+
*
76+
* Read methods include: text, json, bytes, and arrayBuffer. Which are self-explanatory.
77+
*
78+
* Presign allows one to presign a URL to be passed to the client so the client can do the
79+
* actual action, minimizing a redundant data transfer, a common pattern with S3 usage.
80+
* eg
81+
* ```
82+
* file.presign({ method: "GET", expiresIn: 3600 }); // expiresIn is in seconds
83+
* ```
84+
*
85+
* `delete` and `unlink` are identical. Both allow the user to delete a file from S3.
86+
*
87+
* Finally, `stat` returns the metadata and objects in the HTTP header in a JSON format. It
88+
* is equivalent to the "HeadObject" action.
89+
*/
90+
public interface S3Client : ProxyObject {
91+
@Polyglot public fun file(path: String): S3File
92+
}
93+
94+
public interface S3File : ProxyObject {
95+
// write methods
96+
@Polyglot public fun write(data: String, contentType: String?): JsPromise<Number>
97+
@Polyglot public fun write(data: GuestBytes, contentType: String?): JsPromise<Number>
98+
@Polyglot public fun write(data: Blob, contentType: String?): JsPromise<Number>
99+
@Polyglot public fun write(data: ReadableStream, contentType: String?): JsPromise<Number>
100+
101+
// read methods
102+
@Polyglot public fun text(): JsPromise<String>
103+
@Polyglot public fun json(): JsPromise<Value>
104+
@Polyglot public fun bytes(): JsPromise<Value>
105+
@Polyglot public fun arrayBuffer(): JsPromise<Value>
106+
107+
@Polyglot public fun presign(method: String, duration: Long): String
108+
@Polyglot public fun delete(): JsPromise<Unit>
109+
110+
@Polyglot public fun exists(): JsPromise<Boolean>
111+
@Polyglot public fun stat(): JsPromise<ProxyObject>
112+
}
113+
114+
@JvmRecord public data class S3ClientConstructorOptions(
115+
@get:Polyglot val accessKeyId: String,
116+
@get:Polyglot val secretAccessKey: String,
117+
@get:Polyglot val bucket: String,
118+
@get:Polyglot val sessionToken: String?,
119+
@get:Polyglot val acl: String?,
120+
@get:Polyglot val endpoint: String?,
121+
@get:Polyglot val region: String?,
122+
@get:Polyglot val virtualHostedStyle: Boolean?
123+
) {
124+
public companion object {
125+
@JvmStatic public fun from(value: Value): S3ClientConstructorOptions {
126+
return S3ClientConstructorOptions(
127+
// required
128+
accessKeyId = value.getMember(ACCESS_KEY_ID).asString()
129+
?: throw JsError.typeError("S3Client constructor missing accessKeyId"),
130+
secretAccessKey = value.getMember(SECRET_ACCESS_KEY).asString()
131+
?: throw JsError.typeError("S3Client constructor missing secretAccessKey"),
132+
bucket = value.getMember(BUCKET).asString()
133+
?: throw JsError.typeError("S3Client constructor missing bucket"),
134+
135+
// optional
136+
sessionToken = value.getMember(SESSION_TOKEN)?.asString(),
137+
acl = value.getMember(ACL)?.asString(),
138+
endpoint = value.getMember(ENDPOINT)?.asString(),
139+
region = value.getMember(REGION)?.asString(),
140+
virtualHostedStyle = value.getMember(VIRTUAL_HOSTED_STYLE)?.asBoolean()
141+
)
142+
}
143+
}
144+
}

0 commit comments

Comments
 (0)