@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/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@porulle/adapter-r2",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "bun": "./src/index.ts",
9
+ "import": "./dist/index.js",
10
+ "types": "./src/index.ts"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "build": "rm -rf dist tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
15
+ "check-types": "tsc --noEmit",
16
+ "lint": "eslint . --max-warnings 1000",
17
+ "test": "vitest run"
18
+ },
19
+ "dependencies": {
20
+ "@porulle/core": "workspace:*"
21
+ },
22
+ "devDependencies": {
23
+ "@repo/eslint-config": "*",
24
+ "@repo/typescript-config": "*",
25
+ "@types/node": "^24.5.2",
26
+ "eslint": "^9.39.1",
27
+ "typescript": "5.9.2",
28
+ "vitest": "^3.2.4"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "files": [
34
+ "src",
35
+ "dist",
36
+ "README.md"
37
+ ],
38
+ "description": "StorageAdapter for Cloudflare R2, specifically when running on **Cloudflare Workers**. The adapter receives the R2 binding directly — no AWS SDK, no credentials in env.",
39
+ "homepage": "https://porulle-docs.vercel.app",
40
+ "bugs": {
41
+ "url": "https://github.com/asyncdotengineering/porulle/issues"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/asyncdotengineering/porulle.git",
46
+ "directory": "packages/adapters/adapter-r2"
47
+ },
48
+ "author": "Porulle contributors"
49
+ }
package/src/index.ts ADDED
@@ -0,0 +1,129 @@
1
+ import { Err, Ok, type Result, type StorageAdapter } from "@porulle/core";
2
+
3
+ export interface R2ObjectEntry {
4
+ key: string;
5
+ size: number;
6
+ httpMetadata?: {
7
+ contentType?: string;
8
+ };
9
+ }
10
+
11
+ export interface R2BucketLike {
12
+ put(
13
+ key: string,
14
+ value: ArrayBuffer | ArrayBufferView,
15
+ options?: {
16
+ httpMetadata?: {
17
+ contentType?: string;
18
+ };
19
+ },
20
+ ): Promise<unknown>;
21
+ get(key: string): Promise<{
22
+ arrayBuffer(): Promise<ArrayBuffer>;
23
+ httpMetadata?: {
24
+ contentType?: string;
25
+ };
26
+ } | null>;
27
+ delete(key: string): Promise<void>;
28
+ list(options?: { prefix?: string }): Promise<{ objects: R2ObjectEntry[] }>;
29
+ }
30
+
31
+ export interface R2StorageAdapterOptions {
32
+ bucket: R2BucketLike;
33
+ bucketName: string;
34
+ publicBaseUrl?: string;
35
+ signedUrl?: (key: string, expiresIn: number) => Promise<string>;
36
+ }
37
+
38
+ async function toArrayBuffer(data: ArrayBuffer | ReadableStream): Promise<ArrayBuffer> {
39
+ if (data instanceof ArrayBuffer) return data;
40
+ return new Response(data).arrayBuffer();
41
+ }
42
+
43
+ function urlFor(options: R2StorageAdapterOptions, key: string): string {
44
+ if (options.publicBaseUrl) {
45
+ return `${options.publicBaseUrl.replace(/\/$/, "")}/${key}`;
46
+ }
47
+ return `r2://${options.bucketName}/${key}`;
48
+ }
49
+
50
+ export function r2StorageAdapter(options: R2StorageAdapterOptions): StorageAdapter {
51
+ return {
52
+ providerId: "r2",
53
+
54
+ async upload(key, data, contentType): Promise<Result<{ key: string; url: string; contentType: string; size?: number }>> {
55
+ try {
56
+ const body = await toArrayBuffer(data);
57
+ await options.bucket.put(key, body, {
58
+ httpMetadata: {
59
+ contentType,
60
+ },
61
+ });
62
+
63
+ return Ok({
64
+ key,
65
+ url: urlFor(options, key),
66
+ contentType,
67
+ size: body.byteLength,
68
+ });
69
+ } catch (error) {
70
+ return Err({
71
+ code: "R2_UPLOAD_FAILED",
72
+ message: error instanceof Error ? error.message : "Failed to upload object to R2.",
73
+ });
74
+ }
75
+ },
76
+
77
+ async getUrl(key): Promise<Result<string>> {
78
+ return Ok(urlFor(options, key));
79
+ },
80
+
81
+ async getSignedUrl(key, expiresIn): Promise<Result<string>> {
82
+ try {
83
+ if (options.signedUrl) {
84
+ const url = await options.signedUrl(key, expiresIn);
85
+ return Ok(url);
86
+ }
87
+
88
+ const base = urlFor(options, key);
89
+ return Ok(`${base}?expiresIn=${expiresIn}`);
90
+ } catch (error) {
91
+ return Err({
92
+ code: "R2_SIGNED_URL_FAILED",
93
+ message: error instanceof Error ? error.message : "Failed to create R2 signed URL.",
94
+ });
95
+ }
96
+ },
97
+
98
+ async delete(key): Promise<Result<void>> {
99
+ try {
100
+ await options.bucket.delete(key);
101
+ return Ok(undefined);
102
+ } catch (error) {
103
+ return Err({
104
+ code: "R2_DELETE_FAILED",
105
+ message: error instanceof Error ? error.message : "Failed to delete R2 object.",
106
+ });
107
+ }
108
+ },
109
+
110
+ async list(prefix): Promise<Result<Array<{ key: string; url: string; contentType: string; size?: number }>>> {
111
+ try {
112
+ const listed = await options.bucket.list({ prefix });
113
+ return Ok(
114
+ listed.objects.map((item) => ({
115
+ key: item.key,
116
+ url: urlFor(options, item.key),
117
+ contentType: item.httpMetadata?.contentType ?? "application/octet-stream",
118
+ size: item.size,
119
+ })),
120
+ );
121
+ } catch (error) {
122
+ return Err({
123
+ code: "R2_LIST_FAILED",
124
+ message: error instanceof Error ? error.message : "Failed to list R2 objects.",
125
+ });
126
+ }
127
+ },
128
+ };
129
+ }