r2-fs 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,127 @@
1
+ # r2-fs
2
+
3
+ An R2-backed filesystem implementation for Cloudflare Workers. Can be mounted via `worker-fs-mount` to provide persistent filesystem storage using Cloudflare R2 object storage.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install r2-fs worker-fs-mount
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### 1. Configure wrangler.toml
14
+
15
+ ```toml
16
+ [[r2_buckets]]
17
+ binding = "MY_BUCKET"
18
+ bucket_name = "my-bucket-name"
19
+
20
+ [alias]
21
+ "node:fs/promises" = "worker-fs-mount/fs"
22
+ ```
23
+
24
+ ### 2. Generate types with wrangler
25
+
26
+ ```bash
27
+ wrangler types
28
+ ```
29
+
30
+ This generates a `worker-configuration.d.ts` file with typed bindings for your R2 bucket.
31
+
32
+ ### 3. Use in your Worker
33
+
34
+ ```typescript
35
+ import { env } from 'cloudflare:workers';
36
+ import { R2Filesystem } from 'r2-fs';
37
+ import { mount } from 'worker-fs-mount';
38
+ import fs from 'node:fs/promises';
39
+
40
+ // Create filesystem backed by R2 at module level
41
+ const r2fs = new R2Filesystem(env.MY_BUCKET);
42
+ mount('/storage', r2fs);
43
+
44
+ export default {
45
+ async fetch(request: Request): Promise<Response> {
46
+ // Use standard fs operations
47
+ await fs.writeFile('/storage/hello.txt', 'Hello, World!');
48
+ const content = await fs.readFile('/storage/hello.txt', 'utf8');
49
+
50
+ // Create directories
51
+ await fs.mkdir('/storage/projects/my-app', { recursive: true });
52
+
53
+ // List directory contents
54
+ const files = await fs.readdir('/storage/projects');
55
+
56
+ return new Response(content);
57
+ }
58
+ }
59
+ ```
60
+
61
+ ### Alternative: Wrap in a WorkerEntrypoint
62
+
63
+ For service bindings or more complex setups, you can wrap `R2Filesystem` in a WorkerEntrypoint:
64
+
65
+ ```typescript
66
+ import { R2Filesystem } from 'r2-fs';
67
+ import type { WorkerFilesystem } from 'worker-fs-mount';
68
+ import { WorkerEntrypoint } from 'cloudflare:workers';
69
+
70
+ export class MyFilesystem extends WorkerEntrypoint<Env> implements WorkerFilesystem {
71
+ private fs = new R2Filesystem(this.env.MY_BUCKET);
72
+
73
+ // Required methods (6)
74
+ stat = this.fs.stat.bind(this.fs);
75
+ createReadStream = this.fs.createReadStream.bind(this.fs);
76
+ createWriteStream = this.fs.createWriteStream.bind(this.fs);
77
+ readdir = this.fs.readdir.bind(this.fs);
78
+ mkdir = this.fs.mkdir.bind(this.fs);
79
+ rm = this.fs.rm.bind(this.fs);
80
+
81
+ // Optional methods (2)
82
+ symlink = this.fs.symlink.bind(this.fs);
83
+ readlink = this.fs.readlink.bind(this.fs);
84
+ }
85
+ ```
86
+
87
+ ## Features
88
+
89
+ - Full `WorkerFilesystem` interface implementation
90
+ - Persistent storage via R2 (survives restarts)
91
+ - Support for files, directories, and symlinks
92
+ - Streaming read/write support
93
+ - Large file support (up to 5GB per file)
94
+ - Automatic symlink resolution
95
+
96
+ ## API
97
+
98
+ The `R2Filesystem` class implements the full `WorkerFilesystem` interface. See the [worker-fs-mount README](../worker-fs-mount/README.md) for the complete API reference.
99
+
100
+ ## Storage
101
+
102
+ Data is stored in R2 using the following conventions:
103
+
104
+ - **Files**: Stored as R2 objects at their path (e.g., `/foo/bar.txt` -> `foo/bar.txt`)
105
+ - **Directories**: Created as marker objects with a `.dir` suffix (e.g., `/foo/mydir` -> `foo/mydir.dir`)
106
+ - **Symlinks**: Stored as empty objects with `symlinkTarget` in custom metadata
107
+ - **Metadata**: File type, creation time, and symlink targets stored in R2 custom metadata
108
+
109
+ ## Limitations
110
+
111
+ - **File size**: R2 supports files up to 5GB, but very large files may impact performance
112
+ - **Partial writes**: R2 doesn't support partial writes, so writing at an offset requires read-modify-write
113
+ - **Streaming writes**: Streams buffer content in memory before writing to R2
114
+
115
+ ## R2 vs Durable Object Storage
116
+
117
+ | Feature | r2-fs | durable-object-fs |
118
+ |---------|-------|-------------------|
119
+ | Max file size | 5GB | ~100MB recommended |
120
+ | Storage cost | Lower (R2 pricing) | Higher (DO pricing) |
121
+ | Latency | Higher (object storage) | Lower (edge SQLite) |
122
+ | Concurrency | Automatic | Single-threaded DO |
123
+ | Use case | Large files, archives | Small files, metadata |
124
+
125
+ ## License
126
+
127
+ MIT
@@ -0,0 +1,65 @@
1
+ import type { DirEntry, Stat, WorkerFilesystem } from 'worker-fs-mount';
2
+ /**
3
+ * An R2-backed filesystem implementation.
4
+ * Can be used directly or extended in a WorkerEntrypoint.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { R2Filesystem } from 'r2-fs';
9
+ * import { mount } from 'worker-fs-mount';
10
+ * import { env } from 'cloudflare:workers';
11
+ * import fs from 'node:fs/promises';
12
+ *
13
+ * // Mount at module level using importable env
14
+ * const r2fs = new R2Filesystem(env.MY_BUCKET);
15
+ * mount('/storage', r2fs);
16
+ *
17
+ * export default {
18
+ * async fetch(request: Request) {
19
+ * await fs.writeFile('/storage/hello.txt', 'Hello, World!');
20
+ * const content = await fs.readFile('/storage/hello.txt', 'utf8');
21
+ * return new Response(content);
22
+ * }
23
+ * }
24
+ * ```
25
+ */
26
+ export declare class R2Filesystem implements WorkerFilesystem {
27
+ private bucket;
28
+ constructor(bucket: R2Bucket);
29
+ /**
30
+ * Parse R2 object metadata into our internal format.
31
+ */
32
+ private parseMetadata;
33
+ /**
34
+ * Resolve symlinks in a path, following up to 40 levels deep.
35
+ */
36
+ private resolveSymlinks;
37
+ /**
38
+ * Check if a directory exists (either implicitly via prefix or explicitly via marker).
39
+ */
40
+ private directoryExists;
41
+ stat(path: string, options?: {
42
+ followSymlinks?: boolean;
43
+ }): Promise<Stat | null>;
44
+ createReadStream(path: string, options?: {
45
+ start?: number;
46
+ end?: number;
47
+ }): Promise<ReadableStream<Uint8Array>>;
48
+ createWriteStream(path: string, options?: {
49
+ start?: number;
50
+ flags?: 'w' | 'a' | 'r+';
51
+ }): Promise<WritableStream<Uint8Array>>;
52
+ readdir(path: string, options?: {
53
+ recursive?: boolean;
54
+ }): Promise<DirEntry[]>;
55
+ mkdir(path: string, options?: {
56
+ recursive?: boolean;
57
+ }): Promise<string | undefined>;
58
+ rm(path: string, options?: {
59
+ recursive?: boolean;
60
+ force?: boolean;
61
+ }): Promise<void>;
62
+ symlink(linkPath: string, targetPath: string): Promise<void>;
63
+ readlink(path: string): Promise<string>;
64
+ }
65
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AA0BxE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,YAAa,YAAW,gBAAgB;IACvC,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,QAAQ;IAEpC;;OAEG;IACH,OAAO,CAAC,aAAa;IAYrB;;OAEG;YACW,eAAe;IAsB7B;;OAEG;YACW,eAAe;IAiBvB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAgEhF,gBAAgB,CACpB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,GACzC,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;IAwBhC,iBAAiB,CACrB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,IAAI,CAAA;KAAE,GACrD,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;IAgFhC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IA2E7E,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAyCnF,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAkDnF,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAgB9C"}
package/dist/index.js ADDED
@@ -0,0 +1,429 @@
1
+ import { createFsError, getParentPath, normalizePath, resolvePath } from 'worker-fs-mount/utils';
2
+ /**
3
+ * Convert a filesystem path to an R2 key.
4
+ * R2 keys don't have a leading slash.
5
+ */
6
+ function pathToKey(path) {
7
+ const normalized = normalizePath(path);
8
+ return normalized === '/' ? '' : normalized.slice(1);
9
+ }
10
+ /**
11
+ * Suffix used to mark directory objects in R2.
12
+ */
13
+ const DIR_MARKER = '.dir';
14
+ /**
15
+ * An R2-backed filesystem implementation.
16
+ * Can be used directly or extended in a WorkerEntrypoint.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * import { R2Filesystem } from 'r2-fs';
21
+ * import { mount } from 'worker-fs-mount';
22
+ * import { env } from 'cloudflare:workers';
23
+ * import fs from 'node:fs/promises';
24
+ *
25
+ * // Mount at module level using importable env
26
+ * const r2fs = new R2Filesystem(env.MY_BUCKET);
27
+ * mount('/storage', r2fs);
28
+ *
29
+ * export default {
30
+ * async fetch(request: Request) {
31
+ * await fs.writeFile('/storage/hello.txt', 'Hello, World!');
32
+ * const content = await fs.readFile('/storage/hello.txt', 'utf8');
33
+ * return new Response(content);
34
+ * }
35
+ * }
36
+ * ```
37
+ */
38
+ export class R2Filesystem {
39
+ bucket;
40
+ constructor(bucket) {
41
+ this.bucket = bucket;
42
+ }
43
+ /**
44
+ * Parse R2 object metadata into our internal format.
45
+ */
46
+ parseMetadata(obj) {
47
+ const meta = obj.customMetadata;
48
+ return {
49
+ // biome-ignore lint/complexity/useLiteralKeys: TypeScript requires bracket notation for index signatures
50
+ type: meta?.['type'] ?? 'file',
51
+ // biome-ignore lint/complexity/useLiteralKeys: TypeScript requires bracket notation for index signatures
52
+ created: meta?.['created'] ?? obj.uploaded.toISOString(),
53
+ // biome-ignore lint/complexity/useLiteralKeys: TypeScript requires bracket notation for index signatures
54
+ symlinkTarget: meta?.['symlinkTarget'],
55
+ };
56
+ }
57
+ /**
58
+ * Resolve symlinks in a path, following up to 40 levels deep.
59
+ */
60
+ async resolveSymlinks(path, depth = 0) {
61
+ if (depth > 40) {
62
+ throw createFsError('ELOOP', path);
63
+ }
64
+ const normalized = normalizePath(path);
65
+ if (normalized === '/')
66
+ return normalized;
67
+ const key = pathToKey(normalized);
68
+ const obj = await this.bucket.head(key);
69
+ if (!obj)
70
+ return normalized;
71
+ const meta = this.parseMetadata(obj);
72
+ if (meta.type !== 'symlink' || !meta.symlinkTarget) {
73
+ return normalized;
74
+ }
75
+ const target = resolvePath(getParentPath(normalized), meta.symlinkTarget);
76
+ return this.resolveSymlinks(target, depth + 1);
77
+ }
78
+ /**
79
+ * Check if a directory exists (either implicitly via prefix or explicitly via marker).
80
+ */
81
+ async directoryExists(path) {
82
+ if (path === '/')
83
+ return true;
84
+ const key = pathToKey(path);
85
+ // Check for explicit directory marker
86
+ const dirMarker = await this.bucket.head(key + DIR_MARKER);
87
+ if (dirMarker)
88
+ return true;
89
+ // Check for any objects with this prefix (implicit directory)
90
+ const prefix = `${key}/`;
91
+ const listed = await this.bucket.list({ prefix, limit: 1 });
92
+ return listed.objects.length > 0;
93
+ }
94
+ // === Metadata Operations ===
95
+ async stat(path, options) {
96
+ let normalized = normalizePath(path);
97
+ // Handle root directory
98
+ if (normalized === '/') {
99
+ return {
100
+ type: 'directory',
101
+ size: 0,
102
+ writable: true,
103
+ };
104
+ }
105
+ if (options?.followSymlinks !== false) {
106
+ try {
107
+ normalized = await this.resolveSymlinks(normalized);
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ }
113
+ const key = pathToKey(normalized);
114
+ // First check if it's a file or symlink
115
+ const obj = await this.bucket.head(key);
116
+ if (obj) {
117
+ const meta = this.parseMetadata(obj);
118
+ return {
119
+ type: meta.type,
120
+ size: obj.size,
121
+ created: new Date(meta.created),
122
+ lastModified: obj.uploaded,
123
+ writable: true,
124
+ };
125
+ }
126
+ // Check if it's a directory
127
+ const dirMarker = await this.bucket.head(key + DIR_MARKER);
128
+ if (dirMarker) {
129
+ const meta = this.parseMetadata(dirMarker);
130
+ return {
131
+ type: 'directory',
132
+ size: 0,
133
+ created: new Date(meta.created),
134
+ lastModified: dirMarker.uploaded,
135
+ writable: true,
136
+ };
137
+ }
138
+ // Check for implicit directory (objects with this prefix)
139
+ const prefix = `${key}/`;
140
+ const listed = await this.bucket.list({ prefix, limit: 1 });
141
+ if (listed.objects.length > 0) {
142
+ return {
143
+ type: 'directory',
144
+ size: 0,
145
+ writable: true,
146
+ };
147
+ }
148
+ return null;
149
+ }
150
+ // === Streaming Operations ===
151
+ async createReadStream(path, options) {
152
+ const normalized = await this.resolveSymlinks(path);
153
+ const key = pathToKey(normalized);
154
+ const r2Options = {};
155
+ if (options?.start !== undefined || options?.end !== undefined) {
156
+ const start = options?.start ?? 0;
157
+ const length = options?.end !== undefined ? options.end - start + 1 : undefined;
158
+ r2Options.range = length !== undefined ? { offset: start, length } : { offset: start };
159
+ }
160
+ const obj = await this.bucket.get(key, r2Options);
161
+ if (!obj) {
162
+ throw createFsError('ENOENT', path);
163
+ }
164
+ const meta = this.parseMetadata(obj);
165
+ if (meta.type === 'directory') {
166
+ throw createFsError('EISDIR', path);
167
+ }
168
+ return obj.body;
169
+ }
170
+ async createWriteStream(path, options) {
171
+ const normalized = normalizePath(path);
172
+ const parentPath = getParentPath(normalized);
173
+ // Verify parent directory exists
174
+ if (parentPath !== '/') {
175
+ const parentExists = await this.directoryExists(parentPath);
176
+ if (!parentExists) {
177
+ throw createFsError('ENOENT', parentPath);
178
+ }
179
+ }
180
+ const key = pathToKey(normalized);
181
+ // Check if trying to write to a directory
182
+ const dirMarker = await this.bucket.head(key + DIR_MARKER);
183
+ if (dirMarker) {
184
+ throw createFsError('EISDIR', path);
185
+ }
186
+ const self = this;
187
+ let offset = options?.start ?? 0;
188
+ // Collect all chunks and write at once on close
189
+ const chunks = [];
190
+ let existingContent = null;
191
+ let existingMeta = null;
192
+ // Get existing content if needed
193
+ if (options?.flags === 'r+' || options?.flags === 'a') {
194
+ const existing = await this.bucket.get(key);
195
+ if (existing) {
196
+ existingContent = new Uint8Array(await existing.arrayBuffer());
197
+ existingMeta = this.parseMetadata(existing);
198
+ if (options?.flags === 'a') {
199
+ offset = existingContent.length;
200
+ }
201
+ }
202
+ else if (options?.flags === 'r+') {
203
+ throw createFsError('ENOENT', path);
204
+ }
205
+ }
206
+ return new WritableStream({
207
+ write(chunk) {
208
+ chunks.push(chunk);
209
+ },
210
+ async close() {
211
+ // Combine all chunks
212
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
213
+ const combinedChunks = new Uint8Array(totalLength);
214
+ let pos = 0;
215
+ for (const chunk of chunks) {
216
+ combinedChunks.set(chunk, pos);
217
+ pos += chunk.length;
218
+ }
219
+ // Build final content
220
+ let finalContent;
221
+ if (existingContent && options?.flags !== 'w') {
222
+ const newLength = Math.max(existingContent.length, offset + combinedChunks.length);
223
+ finalContent = new Uint8Array(newLength);
224
+ finalContent.set(existingContent, 0);
225
+ finalContent.set(combinedChunks, offset);
226
+ }
227
+ else {
228
+ finalContent = combinedChunks;
229
+ }
230
+ const now = new Date().toISOString();
231
+ await self.bucket.put(key, finalContent, {
232
+ customMetadata: {
233
+ type: 'file',
234
+ created: existingMeta?.created ?? now,
235
+ },
236
+ });
237
+ },
238
+ });
239
+ }
240
+ // === Directory Operations ===
241
+ async readdir(path, options) {
242
+ const normalized = normalizePath(path);
243
+ // Verify directory exists
244
+ const stat = await this.stat(normalized);
245
+ if (!stat) {
246
+ throw createFsError('ENOENT', path);
247
+ }
248
+ if (stat.type !== 'directory') {
249
+ throw createFsError('ENOTDIR', path);
250
+ }
251
+ const prefix = normalized === '/' ? '' : `${pathToKey(normalized)}/`;
252
+ const entries = [];
253
+ const seenDirs = new Set();
254
+ let cursor;
255
+ do {
256
+ const listOptions = { prefix };
257
+ if (!options?.recursive) {
258
+ listOptions.delimiter = '/';
259
+ }
260
+ if (cursor) {
261
+ listOptions.cursor = cursor;
262
+ }
263
+ const listed = await this.bucket.list(listOptions);
264
+ // Process objects
265
+ for (const obj of listed.objects) {
266
+ // Skip directory markers
267
+ if (obj.key.endsWith(DIR_MARKER)) {
268
+ // Extract directory name from marker
269
+ const dirKey = obj.key.slice(0, -DIR_MARKER.length);
270
+ const relativePath = prefix ? dirKey.slice(prefix.length) : dirKey;
271
+ if (relativePath && !seenDirs.has(relativePath)) {
272
+ seenDirs.add(relativePath);
273
+ entries.push({
274
+ name: relativePath,
275
+ type: 'directory',
276
+ });
277
+ }
278
+ continue;
279
+ }
280
+ const relativePath = prefix ? obj.key.slice(prefix.length) : obj.key;
281
+ if (!relativePath)
282
+ continue;
283
+ const meta = this.parseMetadata(obj);
284
+ entries.push({
285
+ name: relativePath,
286
+ type: meta.type,
287
+ });
288
+ }
289
+ // Process common prefixes (implicit directories from delimiter)
290
+ if (!options?.recursive && listed.delimitedPrefixes) {
291
+ for (const dirPrefix of listed.delimitedPrefixes) {
292
+ const dirName = dirPrefix.slice(prefix.length, -1); // Remove trailing /
293
+ if (dirName && !seenDirs.has(dirName)) {
294
+ seenDirs.add(dirName);
295
+ entries.push({
296
+ name: dirName,
297
+ type: 'directory',
298
+ });
299
+ }
300
+ }
301
+ }
302
+ cursor = listed.truncated ? listed.cursor : undefined;
303
+ } while (cursor);
304
+ return entries.sort((a, b) => a.name.localeCompare(b.name));
305
+ }
306
+ async mkdir(path, options) {
307
+ const normalized = normalizePath(path);
308
+ if (normalized === '/') {
309
+ if (options?.recursive)
310
+ return undefined;
311
+ throw createFsError('EEXIST', path);
312
+ }
313
+ const key = pathToKey(normalized);
314
+ // Check if already exists
315
+ const existing = await this.stat(normalized);
316
+ if (existing) {
317
+ if (options?.recursive)
318
+ return undefined;
319
+ throw createFsError('EEXIST', path);
320
+ }
321
+ // Verify parent exists
322
+ const parentPath = getParentPath(normalized);
323
+ if (parentPath !== '/') {
324
+ const parentExists = await this.directoryExists(parentPath);
325
+ if (!parentExists) {
326
+ if (options?.recursive) {
327
+ await this.mkdir(parentPath, { recursive: true });
328
+ }
329
+ else {
330
+ throw createFsError('ENOENT', parentPath);
331
+ }
332
+ }
333
+ }
334
+ // Create directory marker
335
+ await this.bucket.put(key + DIR_MARKER, new Uint8Array(0), {
336
+ customMetadata: {
337
+ type: 'directory',
338
+ created: new Date().toISOString(),
339
+ },
340
+ });
341
+ return normalized;
342
+ }
343
+ async rm(path, options) {
344
+ const normalized = normalizePath(path);
345
+ if (normalized === '/') {
346
+ throw createFsError('EINVAL', path);
347
+ }
348
+ const stat = await this.stat(normalized);
349
+ if (!stat) {
350
+ if (options?.force)
351
+ return;
352
+ throw createFsError('ENOENT', path);
353
+ }
354
+ const key = pathToKey(normalized);
355
+ if (stat.type === 'directory') {
356
+ const prefix = `${key}/`;
357
+ // Check for children
358
+ const listed = await this.bucket.list({ prefix, limit: 1 });
359
+ if (listed.objects.length > 0) {
360
+ if (!options?.recursive) {
361
+ throw createFsError('ENOTEMPTY', path);
362
+ }
363
+ // Delete all children
364
+ let cursor;
365
+ do {
366
+ const listOptions = { prefix, limit: 1000 };
367
+ if (cursor) {
368
+ listOptions.cursor = cursor;
369
+ }
370
+ const batch = await this.bucket.list(listOptions);
371
+ if (batch.objects.length > 0) {
372
+ await this.bucket.delete(batch.objects.map((o) => o.key));
373
+ }
374
+ cursor = batch.truncated ? batch.cursor : undefined;
375
+ } while (cursor);
376
+ }
377
+ // Delete directory marker if it exists
378
+ await this.bucket.delete(key + DIR_MARKER);
379
+ }
380
+ else {
381
+ // Delete file or symlink
382
+ await this.bucket.delete(key);
383
+ }
384
+ }
385
+ // === Link Operations ===
386
+ async symlink(linkPath, targetPath) {
387
+ const normalizedLink = normalizePath(linkPath);
388
+ const parentPath = getParentPath(normalizedLink);
389
+ // Verify parent exists
390
+ if (parentPath !== '/') {
391
+ const parentExists = await this.directoryExists(parentPath);
392
+ if (!parentExists) {
393
+ throw createFsError('ENOENT', parentPath);
394
+ }
395
+ }
396
+ // Check link doesn't exist
397
+ const key = pathToKey(normalizedLink);
398
+ const existing = await this.bucket.head(key);
399
+ if (existing) {
400
+ throw createFsError('EEXIST', linkPath);
401
+ }
402
+ // Check it's not a directory
403
+ const dirMarker = await this.bucket.head(key + DIR_MARKER);
404
+ if (dirMarker) {
405
+ throw createFsError('EEXIST', linkPath);
406
+ }
407
+ await this.bucket.put(key, new Uint8Array(0), {
408
+ customMetadata: {
409
+ type: 'symlink',
410
+ created: new Date().toISOString(),
411
+ symlinkTarget: targetPath,
412
+ },
413
+ });
414
+ }
415
+ async readlink(path) {
416
+ const normalized = normalizePath(path);
417
+ const key = pathToKey(normalized);
418
+ const obj = await this.bucket.head(key);
419
+ if (!obj) {
420
+ throw createFsError('ENOENT', path);
421
+ }
422
+ const meta = this.parseMetadata(obj);
423
+ if (meta.type !== 'symlink' || !meta.symlinkTarget) {
424
+ throw createFsError('EINVAL', path);
425
+ }
426
+ return meta.symlinkTarget;
427
+ }
428
+ }
429
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "r2-fs",
3
+ "version": "0.1.0",
4
+ "description": "R2-backed filesystem for Cloudflare Workers",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "dev": "tsc --watch",
22
+ "clean": "rm -rf dist",
23
+ "prepublishOnly": "npm run clean && npm run build",
24
+ "typecheck": "tsc --noEmit"
25
+ },
26
+ "keywords": [
27
+ "cloudflare",
28
+ "workers",
29
+ "r2",
30
+ "filesystem",
31
+ "storage"
32
+ ],
33
+ "author": "Cloudflare",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/cloudflare/worker-fs-mount.git",
38
+ "directory": "packages/r2-fs"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/cloudflare/worker-fs-mount/issues"
42
+ },
43
+ "homepage": "https://github.com/cloudflare/worker-fs-mount/tree/main/packages/r2-fs#readme",
44
+ "peerDependencies": {
45
+ "worker-fs-mount": "workspace:*"
46
+ },
47
+ "devDependencies": {
48
+ "@cloudflare/workers-types": "^4.20251225.0",
49
+ "@types/node": "^25.0.3",
50
+ "typescript": "^5.7.2",
51
+ "worker-fs-mount": "workspace:*"
52
+ },
53
+ "engines": {
54
+ "node": ">=18.0.0"
55
+ }
56
+ }
package/src/index.ts ADDED
@@ -0,0 +1,504 @@
1
+ import type { DirEntry, Stat, WorkerFilesystem } from 'worker-fs-mount';
2
+ import { createFsError, getParentPath, normalizePath, resolvePath } from 'worker-fs-mount/utils';
3
+
4
+ /**
5
+ * Convert a filesystem path to an R2 key.
6
+ * R2 keys don't have a leading slash.
7
+ */
8
+ function pathToKey(path: string): string {
9
+ const normalized = normalizePath(path);
10
+ return normalized === '/' ? '' : normalized.slice(1);
11
+ }
12
+
13
+ /**
14
+ * Metadata stored in R2 customMetadata for each object.
15
+ */
16
+ interface R2FsMetadata {
17
+ type: 'file' | 'directory' | 'symlink';
18
+ created: string; // ISO timestamp
19
+ symlinkTarget?: string | undefined;
20
+ }
21
+
22
+ /**
23
+ * Suffix used to mark directory objects in R2.
24
+ */
25
+ const DIR_MARKER = '.dir';
26
+
27
+ /**
28
+ * An R2-backed filesystem implementation.
29
+ * Can be used directly or extended in a WorkerEntrypoint.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * import { R2Filesystem } from 'r2-fs';
34
+ * import { mount } from 'worker-fs-mount';
35
+ * import { env } from 'cloudflare:workers';
36
+ * import fs from 'node:fs/promises';
37
+ *
38
+ * // Mount at module level using importable env
39
+ * const r2fs = new R2Filesystem(env.MY_BUCKET);
40
+ * mount('/storage', r2fs);
41
+ *
42
+ * export default {
43
+ * async fetch(request: Request) {
44
+ * await fs.writeFile('/storage/hello.txt', 'Hello, World!');
45
+ * const content = await fs.readFile('/storage/hello.txt', 'utf8');
46
+ * return new Response(content);
47
+ * }
48
+ * }
49
+ * ```
50
+ */
51
+ export class R2Filesystem implements WorkerFilesystem {
52
+ constructor(private bucket: R2Bucket) {}
53
+
54
+ /**
55
+ * Parse R2 object metadata into our internal format.
56
+ */
57
+ private parseMetadata(obj: R2Object): R2FsMetadata {
58
+ const meta = obj.customMetadata;
59
+ return {
60
+ // biome-ignore lint/complexity/useLiteralKeys: TypeScript requires bracket notation for index signatures
61
+ type: (meta?.['type'] as R2FsMetadata['type']) ?? 'file',
62
+ // biome-ignore lint/complexity/useLiteralKeys: TypeScript requires bracket notation for index signatures
63
+ created: meta?.['created'] ?? obj.uploaded.toISOString(),
64
+ // biome-ignore lint/complexity/useLiteralKeys: TypeScript requires bracket notation for index signatures
65
+ symlinkTarget: meta?.['symlinkTarget'],
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Resolve symlinks in a path, following up to 40 levels deep.
71
+ */
72
+ private async resolveSymlinks(path: string, depth = 0): Promise<string> {
73
+ if (depth > 40) {
74
+ throw createFsError('ELOOP', path);
75
+ }
76
+
77
+ const normalized = normalizePath(path);
78
+ if (normalized === '/') return normalized;
79
+
80
+ const key = pathToKey(normalized);
81
+ const obj = await this.bucket.head(key);
82
+
83
+ if (!obj) return normalized;
84
+
85
+ const meta = this.parseMetadata(obj);
86
+ if (meta.type !== 'symlink' || !meta.symlinkTarget) {
87
+ return normalized;
88
+ }
89
+
90
+ const target = resolvePath(getParentPath(normalized), meta.symlinkTarget);
91
+ return this.resolveSymlinks(target, depth + 1);
92
+ }
93
+
94
+ /**
95
+ * Check if a directory exists (either implicitly via prefix or explicitly via marker).
96
+ */
97
+ private async directoryExists(path: string): Promise<boolean> {
98
+ if (path === '/') return true;
99
+
100
+ const key = pathToKey(path);
101
+
102
+ // Check for explicit directory marker
103
+ const dirMarker = await this.bucket.head(key + DIR_MARKER);
104
+ if (dirMarker) return true;
105
+
106
+ // Check for any objects with this prefix (implicit directory)
107
+ const prefix = `${key}/`;
108
+ const listed = await this.bucket.list({ prefix, limit: 1 });
109
+ return listed.objects.length > 0;
110
+ }
111
+
112
+ // === Metadata Operations ===
113
+
114
+ async stat(path: string, options?: { followSymlinks?: boolean }): Promise<Stat | null> {
115
+ let normalized = normalizePath(path);
116
+
117
+ // Handle root directory
118
+ if (normalized === '/') {
119
+ return {
120
+ type: 'directory',
121
+ size: 0,
122
+ writable: true,
123
+ };
124
+ }
125
+
126
+ if (options?.followSymlinks !== false) {
127
+ try {
128
+ normalized = await this.resolveSymlinks(normalized);
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ const key = pathToKey(normalized);
135
+
136
+ // First check if it's a file or symlink
137
+ const obj = await this.bucket.head(key);
138
+ if (obj) {
139
+ const meta = this.parseMetadata(obj);
140
+ return {
141
+ type: meta.type,
142
+ size: obj.size,
143
+ created: new Date(meta.created),
144
+ lastModified: obj.uploaded,
145
+ writable: true,
146
+ };
147
+ }
148
+
149
+ // Check if it's a directory
150
+ const dirMarker = await this.bucket.head(key + DIR_MARKER);
151
+ if (dirMarker) {
152
+ const meta = this.parseMetadata(dirMarker);
153
+ return {
154
+ type: 'directory',
155
+ size: 0,
156
+ created: new Date(meta.created),
157
+ lastModified: dirMarker.uploaded,
158
+ writable: true,
159
+ };
160
+ }
161
+
162
+ // Check for implicit directory (objects with this prefix)
163
+ const prefix = `${key}/`;
164
+ const listed = await this.bucket.list({ prefix, limit: 1 });
165
+ if (listed.objects.length > 0) {
166
+ return {
167
+ type: 'directory',
168
+ size: 0,
169
+ writable: true,
170
+ };
171
+ }
172
+
173
+ return null;
174
+ }
175
+
176
+ // === Streaming Operations ===
177
+
178
+ async createReadStream(
179
+ path: string,
180
+ options?: { start?: number; end?: number }
181
+ ): Promise<ReadableStream<Uint8Array>> {
182
+ const normalized = await this.resolveSymlinks(path);
183
+ const key = pathToKey(normalized);
184
+
185
+ const r2Options: R2GetOptions = {};
186
+ if (options?.start !== undefined || options?.end !== undefined) {
187
+ const start = options?.start ?? 0;
188
+ const length = options?.end !== undefined ? options.end - start + 1 : undefined;
189
+ r2Options.range = length !== undefined ? { offset: start, length } : { offset: start };
190
+ }
191
+
192
+ const obj = await this.bucket.get(key, r2Options);
193
+ if (!obj) {
194
+ throw createFsError('ENOENT', path);
195
+ }
196
+
197
+ const meta = this.parseMetadata(obj);
198
+ if (meta.type === 'directory') {
199
+ throw createFsError('EISDIR', path);
200
+ }
201
+
202
+ return obj.body;
203
+ }
204
+
205
+ async createWriteStream(
206
+ path: string,
207
+ options?: { start?: number; flags?: 'w' | 'a' | 'r+' }
208
+ ): Promise<WritableStream<Uint8Array>> {
209
+ const normalized = normalizePath(path);
210
+ const parentPath = getParentPath(normalized);
211
+
212
+ // Verify parent directory exists
213
+ if (parentPath !== '/') {
214
+ const parentExists = await this.directoryExists(parentPath);
215
+ if (!parentExists) {
216
+ throw createFsError('ENOENT', parentPath);
217
+ }
218
+ }
219
+
220
+ const key = pathToKey(normalized);
221
+
222
+ // Check if trying to write to a directory
223
+ const dirMarker = await this.bucket.head(key + DIR_MARKER);
224
+ if (dirMarker) {
225
+ throw createFsError('EISDIR', path);
226
+ }
227
+
228
+ const self = this;
229
+ let offset = options?.start ?? 0;
230
+
231
+ // Collect all chunks and write at once on close
232
+ const chunks: Uint8Array[] = [];
233
+ let existingContent: Uint8Array | null = null;
234
+ let existingMeta: R2FsMetadata | null = null;
235
+
236
+ // Get existing content if needed
237
+ if (options?.flags === 'r+' || options?.flags === 'a') {
238
+ const existing = await this.bucket.get(key);
239
+ if (existing) {
240
+ existingContent = new Uint8Array(await existing.arrayBuffer());
241
+ existingMeta = this.parseMetadata(existing);
242
+ if (options?.flags === 'a') {
243
+ offset = existingContent.length;
244
+ }
245
+ } else if (options?.flags === 'r+') {
246
+ throw createFsError('ENOENT', path);
247
+ }
248
+ }
249
+
250
+ return new WritableStream({
251
+ write(chunk) {
252
+ chunks.push(chunk);
253
+ },
254
+ async close() {
255
+ // Combine all chunks
256
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
257
+ const combinedChunks = new Uint8Array(totalLength);
258
+ let pos = 0;
259
+ for (const chunk of chunks) {
260
+ combinedChunks.set(chunk, pos);
261
+ pos += chunk.length;
262
+ }
263
+
264
+ // Build final content
265
+ let finalContent: Uint8Array;
266
+ if (existingContent && options?.flags !== 'w') {
267
+ const newLength = Math.max(existingContent.length, offset + combinedChunks.length);
268
+ finalContent = new Uint8Array(newLength);
269
+ finalContent.set(existingContent, 0);
270
+ finalContent.set(combinedChunks, offset);
271
+ } else {
272
+ finalContent = combinedChunks;
273
+ }
274
+
275
+ const now = new Date().toISOString();
276
+ await self.bucket.put(key, finalContent, {
277
+ customMetadata: {
278
+ type: 'file',
279
+ created: existingMeta?.created ?? now,
280
+ },
281
+ });
282
+ },
283
+ });
284
+ }
285
+
286
+ // === Directory Operations ===
287
+
288
+ async readdir(path: string, options?: { recursive?: boolean }): Promise<DirEntry[]> {
289
+ const normalized = normalizePath(path);
290
+
291
+ // Verify directory exists
292
+ const stat = await this.stat(normalized);
293
+ if (!stat) {
294
+ throw createFsError('ENOENT', path);
295
+ }
296
+ if (stat.type !== 'directory') {
297
+ throw createFsError('ENOTDIR', path);
298
+ }
299
+
300
+ const prefix = normalized === '/' ? '' : `${pathToKey(normalized)}/`;
301
+ const entries: DirEntry[] = [];
302
+ const seenDirs = new Set<string>();
303
+
304
+ let cursor: string | undefined;
305
+
306
+ do {
307
+ const listOptions: R2ListOptions = { prefix };
308
+ if (!options?.recursive) {
309
+ listOptions.delimiter = '/';
310
+ }
311
+ if (cursor) {
312
+ listOptions.cursor = cursor;
313
+ }
314
+ const listed = await this.bucket.list(listOptions);
315
+
316
+ // Process objects
317
+ for (const obj of listed.objects) {
318
+ // Skip directory markers
319
+ if (obj.key.endsWith(DIR_MARKER)) {
320
+ // Extract directory name from marker
321
+ const dirKey = obj.key.slice(0, -DIR_MARKER.length);
322
+ const relativePath = prefix ? dirKey.slice(prefix.length) : dirKey;
323
+ if (relativePath && !seenDirs.has(relativePath)) {
324
+ seenDirs.add(relativePath);
325
+ entries.push({
326
+ name: relativePath,
327
+ type: 'directory',
328
+ });
329
+ }
330
+ continue;
331
+ }
332
+
333
+ const relativePath = prefix ? obj.key.slice(prefix.length) : obj.key;
334
+ if (!relativePath) continue;
335
+
336
+ const meta = this.parseMetadata(obj);
337
+ entries.push({
338
+ name: relativePath,
339
+ type: meta.type,
340
+ });
341
+ }
342
+
343
+ // Process common prefixes (implicit directories from delimiter)
344
+ if (!options?.recursive && listed.delimitedPrefixes) {
345
+ for (const dirPrefix of listed.delimitedPrefixes) {
346
+ const dirName = dirPrefix.slice(prefix.length, -1); // Remove trailing /
347
+ if (dirName && !seenDirs.has(dirName)) {
348
+ seenDirs.add(dirName);
349
+ entries.push({
350
+ name: dirName,
351
+ type: 'directory',
352
+ });
353
+ }
354
+ }
355
+ }
356
+
357
+ cursor = listed.truncated ? listed.cursor : undefined;
358
+ } while (cursor);
359
+
360
+ return entries.sort((a, b) => a.name.localeCompare(b.name));
361
+ }
362
+
363
+ async mkdir(path: string, options?: { recursive?: boolean }): Promise<string | undefined> {
364
+ const normalized = normalizePath(path);
365
+
366
+ if (normalized === '/') {
367
+ if (options?.recursive) return undefined;
368
+ throw createFsError('EEXIST', path);
369
+ }
370
+
371
+ const key = pathToKey(normalized);
372
+
373
+ // Check if already exists
374
+ const existing = await this.stat(normalized);
375
+ if (existing) {
376
+ if (options?.recursive) return undefined;
377
+ throw createFsError('EEXIST', path);
378
+ }
379
+
380
+ // Verify parent exists
381
+ const parentPath = getParentPath(normalized);
382
+ if (parentPath !== '/') {
383
+ const parentExists = await this.directoryExists(parentPath);
384
+ if (!parentExists) {
385
+ if (options?.recursive) {
386
+ await this.mkdir(parentPath, { recursive: true });
387
+ } else {
388
+ throw createFsError('ENOENT', parentPath);
389
+ }
390
+ }
391
+ }
392
+
393
+ // Create directory marker
394
+ await this.bucket.put(key + DIR_MARKER, new Uint8Array(0), {
395
+ customMetadata: {
396
+ type: 'directory',
397
+ created: new Date().toISOString(),
398
+ },
399
+ });
400
+
401
+ return normalized;
402
+ }
403
+
404
+ async rm(path: string, options?: { recursive?: boolean; force?: boolean }): Promise<void> {
405
+ const normalized = normalizePath(path);
406
+
407
+ if (normalized === '/') {
408
+ throw createFsError('EINVAL', path);
409
+ }
410
+
411
+ const stat = await this.stat(normalized);
412
+ if (!stat) {
413
+ if (options?.force) return;
414
+ throw createFsError('ENOENT', path);
415
+ }
416
+
417
+ const key = pathToKey(normalized);
418
+
419
+ if (stat.type === 'directory') {
420
+ const prefix = `${key}/`;
421
+
422
+ // Check for children
423
+ const listed = await this.bucket.list({ prefix, limit: 1 });
424
+ if (listed.objects.length > 0) {
425
+ if (!options?.recursive) {
426
+ throw createFsError('ENOTEMPTY', path);
427
+ }
428
+
429
+ // Delete all children
430
+ let cursor: string | undefined;
431
+ do {
432
+ const listOptions: R2ListOptions = { prefix, limit: 1000 };
433
+ if (cursor) {
434
+ listOptions.cursor = cursor;
435
+ }
436
+ const batch = await this.bucket.list(listOptions);
437
+ if (batch.objects.length > 0) {
438
+ await this.bucket.delete(batch.objects.map((o) => o.key));
439
+ }
440
+ cursor = batch.truncated ? batch.cursor : undefined;
441
+ } while (cursor);
442
+ }
443
+
444
+ // Delete directory marker if it exists
445
+ await this.bucket.delete(key + DIR_MARKER);
446
+ } else {
447
+ // Delete file or symlink
448
+ await this.bucket.delete(key);
449
+ }
450
+ }
451
+
452
+ // === Link Operations ===
453
+
454
+ async symlink(linkPath: string, targetPath: string): Promise<void> {
455
+ const normalizedLink = normalizePath(linkPath);
456
+ const parentPath = getParentPath(normalizedLink);
457
+
458
+ // Verify parent exists
459
+ if (parentPath !== '/') {
460
+ const parentExists = await this.directoryExists(parentPath);
461
+ if (!parentExists) {
462
+ throw createFsError('ENOENT', parentPath);
463
+ }
464
+ }
465
+
466
+ // Check link doesn't exist
467
+ const key = pathToKey(normalizedLink);
468
+ const existing = await this.bucket.head(key);
469
+ if (existing) {
470
+ throw createFsError('EEXIST', linkPath);
471
+ }
472
+
473
+ // Check it's not a directory
474
+ const dirMarker = await this.bucket.head(key + DIR_MARKER);
475
+ if (dirMarker) {
476
+ throw createFsError('EEXIST', linkPath);
477
+ }
478
+
479
+ await this.bucket.put(key, new Uint8Array(0), {
480
+ customMetadata: {
481
+ type: 'symlink',
482
+ created: new Date().toISOString(),
483
+ symlinkTarget: targetPath,
484
+ },
485
+ });
486
+ }
487
+
488
+ async readlink(path: string): Promise<string> {
489
+ const normalized = normalizePath(path);
490
+ const key = pathToKey(normalized);
491
+
492
+ const obj = await this.bucket.head(key);
493
+ if (!obj) {
494
+ throw createFsError('ENOENT', path);
495
+ }
496
+
497
+ const meta = this.parseMetadata(obj);
498
+ if (meta.type !== 'symlink' || !meta.symlinkTarget) {
499
+ throw createFsError('EINVAL', path);
500
+ }
501
+
502
+ return meta.symlinkTarget;
503
+ }
504
+ }