@porulle/adapter-r2 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # @porulle/adapter-r2
2
+
3
+ `StorageAdapter` for [Cloudflare R2](https://developers.cloudflare.com/r2/), specifically when running on **Cloudflare Workers**. The adapter receives the R2 binding directly — no AWS SDK, no credentials in env.
4
+
5
+ ## Usage (Workers)
6
+
7
+ ```ts
8
+ import { r2StorageAdapter } from "@porulle/adapter-r2";
9
+
10
+ // In a Workers fetch handler:
11
+ export default {
12
+ async fetch(request: Request, env: Env) {
13
+ const config = defineConfig({
14
+ storage: r2StorageAdapter({
15
+ bucket: env.MEDIA_BUCKET, // R2 binding
16
+ publicBaseUrl: "https://media.acme.com",
17
+ }),
18
+ // …
19
+ });
20
+ const { app } = await createServer(config);
21
+ return app.fetch(request, env);
22
+ },
23
+ };
24
+ ```
25
+
26
+ ## Why a separate adapter from S3
27
+
28
+ R2 is API-compatible with S3, but on Workers you bypass the SDK entirely — the binding is a native object with `put`, `get`, `delete`, `list`. Using `@porulle/adapter-s3` on Workers would bundle the AWS SDK (~500KB), eat CPU on cold start, and force you to handle credentials. `@porulle/adapter-r2` is a few hundred lines and zero deps.
29
+
30
+ ## Outside Workers
31
+
32
+ If you're consuming R2 from Node, use `@porulle/adapter-s3` with R2's S3 endpoint:
33
+
34
+ ```ts
35
+ s3StorageAdapter({
36
+ bucket: "acme-media",
37
+ region: "auto",
38
+ endpoint: "https://<account>.r2.cloudflarestorage.com",
39
+ // …
40
+ });
41
+ ```
42
+
43
+ ## What it implements
44
+
45
+ `upload`, `getUrl`, `delete`, `list`. Signed URLs aren't supported by the binding directly — generate via `R2Bucket.createPresignedUrl()` outside the adapter if needed.
46
+
47
+ ## See also
48
+
49
+ - [Cloudflare R2 docs](https://developers.cloudflare.com/r2/)
50
+ - `@porulle/adapter-s3` — for non-Workers consumption
@@ -0,0 +1,35 @@
1
+ import { type StorageAdapter } from "@porulle/core";
2
+ export interface R2ObjectEntry {
3
+ key: string;
4
+ size: number;
5
+ httpMetadata?: {
6
+ contentType?: string;
7
+ };
8
+ }
9
+ export interface R2BucketLike {
10
+ put(key: string, value: ArrayBuffer | ArrayBufferView, options?: {
11
+ httpMetadata?: {
12
+ contentType?: string;
13
+ };
14
+ }): Promise<unknown>;
15
+ get(key: string): Promise<{
16
+ arrayBuffer(): Promise<ArrayBuffer>;
17
+ httpMetadata?: {
18
+ contentType?: string;
19
+ };
20
+ } | null>;
21
+ delete(key: string): Promise<void>;
22
+ list(options?: {
23
+ prefix?: string;
24
+ }): Promise<{
25
+ objects: R2ObjectEntry[];
26
+ }>;
27
+ }
28
+ export interface R2StorageAdapterOptions {
29
+ bucket: R2BucketLike;
30
+ bucketName: string;
31
+ publicBaseUrl?: string;
32
+ signedUrl?: (key: string, expiresIn: number) => Promise<string>;
33
+ }
34
+ export declare function r2StorageAdapter(options: R2StorageAdapterOptions): StorageAdapter;
35
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAE1E,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE;QACb,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,CACD,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,WAAW,GAAG,eAAe,EACpC,OAAO,CAAC,EAAE;QACR,YAAY,CAAC,EAAE;YACb,WAAW,CAAC,EAAE,MAAM,CAAC;SACtB,CAAC;KACH,GACA,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QACxB,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;QACpC,YAAY,CAAC,EAAE;YACb,WAAW,CAAC,EAAE,MAAM,CAAC;SACtB,CAAC;KACH,GAAG,IAAI,CAAC,CAAC;IACV,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,CAAC,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,aAAa,EAAE,CAAA;KAAE,CAAC,CAAC;CAC5E;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACjE;AAcD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,cAAc,CA+EjF"}
@@ -0,0 +1,87 @@
1
+ import { Err, Ok } from "@porulle/core";
2
+ async function toArrayBuffer(data) {
3
+ if (data instanceof ArrayBuffer)
4
+ return data;
5
+ return new Response(data).arrayBuffer();
6
+ }
7
+ function urlFor(options, key) {
8
+ if (options.publicBaseUrl) {
9
+ return `${options.publicBaseUrl.replace(/\/$/, "")}/${key}`;
10
+ }
11
+ return `r2://${options.bucketName}/${key}`;
12
+ }
13
+ export function r2StorageAdapter(options) {
14
+ return {
15
+ providerId: "r2",
16
+ async upload(key, data, contentType) {
17
+ try {
18
+ const body = await toArrayBuffer(data);
19
+ await options.bucket.put(key, body, {
20
+ httpMetadata: {
21
+ contentType,
22
+ },
23
+ });
24
+ return Ok({
25
+ key,
26
+ url: urlFor(options, key),
27
+ contentType,
28
+ size: body.byteLength,
29
+ });
30
+ }
31
+ catch (error) {
32
+ return Err({
33
+ code: "R2_UPLOAD_FAILED",
34
+ message: error instanceof Error ? error.message : "Failed to upload object to R2.",
35
+ });
36
+ }
37
+ },
38
+ async getUrl(key) {
39
+ return Ok(urlFor(options, key));
40
+ },
41
+ async getSignedUrl(key, expiresIn) {
42
+ try {
43
+ if (options.signedUrl) {
44
+ const url = await options.signedUrl(key, expiresIn);
45
+ return Ok(url);
46
+ }
47
+ const base = urlFor(options, key);
48
+ return Ok(`${base}?expiresIn=${expiresIn}`);
49
+ }
50
+ catch (error) {
51
+ return Err({
52
+ code: "R2_SIGNED_URL_FAILED",
53
+ message: error instanceof Error ? error.message : "Failed to create R2 signed URL.",
54
+ });
55
+ }
56
+ },
57
+ async delete(key) {
58
+ try {
59
+ await options.bucket.delete(key);
60
+ return Ok(undefined);
61
+ }
62
+ catch (error) {
63
+ return Err({
64
+ code: "R2_DELETE_FAILED",
65
+ message: error instanceof Error ? error.message : "Failed to delete R2 object.",
66
+ });
67
+ }
68
+ },
69
+ async list(prefix) {
70
+ try {
71
+ const listed = await options.bucket.list({ prefix });
72
+ return Ok(listed.objects.map((item) => ({
73
+ key: item.key,
74
+ url: urlFor(options, item.key),
75
+ contentType: item.httpMetadata?.contentType ?? "application/octet-stream",
76
+ size: item.size,
77
+ })));
78
+ }
79
+ catch (error) {
80
+ return Err({
81
+ code: "R2_LIST_FAILED",
82
+ message: error instanceof Error ? error.message : "Failed to list R2 objects.",
83
+ });
84
+ }
85
+ },
86
+ };
87
+ }