worker-fs-mount 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 +360 -0
- package/dist/fs-promises.d.ts +83 -0
- package/dist/fs-promises.d.ts.map +1 -0
- package/dist/fs-promises.js +688 -0
- package/dist/fs-promises.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +101 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +165 -0
- package/dist/registry.js.map +1 -0
- package/dist/types.d.ts +95 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +40 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +80 -0
- package/dist/utils.js.map +1 -0
- package/package.json +65 -0
- package/src/fs-promises.ts +832 -0
- package/src/index.ts +50 -0
- package/src/registry.ts +199 -0
- package/src/types.ts +102 -0
- package/src/utils.ts +95 -0
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replacement module for node:fs/promises that routes mounted paths
|
|
3
|
+
* to their WorkerFilesystem implementations.
|
|
4
|
+
*
|
|
5
|
+
* Users should alias this in their wrangler.toml:
|
|
6
|
+
*
|
|
7
|
+
* [alias]
|
|
8
|
+
* "node:fs/promises" = "worker-fs-mount/fs"
|
|
9
|
+
*
|
|
10
|
+
* This module implements derived operations (readFile, writeFile, truncate,
|
|
11
|
+
* cp, access, rename) on top of the core streaming interface.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Buffer } from 'node:buffer';
|
|
15
|
+
import type { BigIntStats, Dirent, Stats } from 'node:fs';
|
|
16
|
+
// Import the SYNC fs module and use .promises to avoid alias loop
|
|
17
|
+
import * as nodeFs from 'node:fs';
|
|
18
|
+
import { findMount } from './registry.js';
|
|
19
|
+
import type { DirEntry, Stat, WorkerFilesystem } from './types.js';
|
|
20
|
+
|
|
21
|
+
// Get the real fs/promises from the sync module
|
|
22
|
+
const realFs = nodeFs.promises;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract a string path from various PathLike types.
|
|
26
|
+
*/
|
|
27
|
+
function getPath(pathLike: unknown): string | null {
|
|
28
|
+
if (typeof pathLike === 'string') return pathLike;
|
|
29
|
+
if (pathLike instanceof URL) return pathLike.pathname;
|
|
30
|
+
if (Buffer.isBuffer(pathLike)) return pathLike.toString('utf8');
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a Node.js-style filesystem error.
|
|
36
|
+
*/
|
|
37
|
+
function createFsError(
|
|
38
|
+
code: string,
|
|
39
|
+
syscall: string,
|
|
40
|
+
path: string,
|
|
41
|
+
message?: string
|
|
42
|
+
): NodeJS.ErrnoException {
|
|
43
|
+
const msg = message ?? `${code}: ${syscall} '${path}'`;
|
|
44
|
+
const err = new Error(msg) as NodeJS.ErrnoException;
|
|
45
|
+
err.code = code;
|
|
46
|
+
err.syscall = syscall;
|
|
47
|
+
err.path = path;
|
|
48
|
+
return err;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Convert our Stat type to a Node.js Stats-like object.
|
|
53
|
+
*/
|
|
54
|
+
function toNodeStats(s: Stat): Stats {
|
|
55
|
+
const isFile = s.type === 'file';
|
|
56
|
+
const isDir = s.type === 'directory';
|
|
57
|
+
const isSymlink = s.type === 'symlink';
|
|
58
|
+
|
|
59
|
+
const mtime = s.lastModified ?? new Date(0);
|
|
60
|
+
const birthtime = s.created ?? new Date(0);
|
|
61
|
+
|
|
62
|
+
const stats = {
|
|
63
|
+
isFile: () => isFile,
|
|
64
|
+
isDirectory: () => isDir,
|
|
65
|
+
isSymbolicLink: () => isSymlink,
|
|
66
|
+
isBlockDevice: () => false,
|
|
67
|
+
isCharacterDevice: () => false,
|
|
68
|
+
isFIFO: () => false,
|
|
69
|
+
isSocket: () => false,
|
|
70
|
+
dev: 0,
|
|
71
|
+
ino: 0,
|
|
72
|
+
mode: isDir ? 0o755 : 0o644,
|
|
73
|
+
nlink: 1,
|
|
74
|
+
uid: 0,
|
|
75
|
+
gid: 0,
|
|
76
|
+
rdev: 0,
|
|
77
|
+
size: s.size,
|
|
78
|
+
blksize: 4096,
|
|
79
|
+
blocks: Math.ceil(s.size / 512),
|
|
80
|
+
atimeMs: mtime.getTime(),
|
|
81
|
+
mtimeMs: mtime.getTime(),
|
|
82
|
+
ctimeMs: mtime.getTime(),
|
|
83
|
+
birthtimeMs: birthtime.getTime(),
|
|
84
|
+
atime: mtime,
|
|
85
|
+
mtime: mtime,
|
|
86
|
+
ctime: mtime,
|
|
87
|
+
birthtime: birthtime,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return stats as Stats;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Convert our DirEntry to a Node.js Dirent-like object.
|
|
95
|
+
*/
|
|
96
|
+
function toNodeDirent(entry: DirEntry, parentPath: string): Dirent {
|
|
97
|
+
const isFile = entry.type === 'file';
|
|
98
|
+
const isDir = entry.type === 'directory';
|
|
99
|
+
const isSymlink = entry.type === 'symlink';
|
|
100
|
+
|
|
101
|
+
const dirent = {
|
|
102
|
+
name: entry.name,
|
|
103
|
+
parentPath: parentPath,
|
|
104
|
+
path: parentPath,
|
|
105
|
+
isFile: () => isFile,
|
|
106
|
+
isDirectory: () => isDir,
|
|
107
|
+
isSymbolicLink: () => isSymlink,
|
|
108
|
+
isBlockDevice: () => false,
|
|
109
|
+
isCharacterDevice: () => false,
|
|
110
|
+
isFIFO: () => false,
|
|
111
|
+
isSocket: () => false,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return dirent as Dirent;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// === Helper functions for derived operations ===
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Collect all chunks from a ReadableStream into a single Uint8Array.
|
|
121
|
+
*/
|
|
122
|
+
async function collectStream(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
|
|
123
|
+
const reader = stream.getReader();
|
|
124
|
+
const chunks: Uint8Array[] = [];
|
|
125
|
+
let totalLength = 0;
|
|
126
|
+
|
|
127
|
+
while (true) {
|
|
128
|
+
const { done, value } = await reader.read();
|
|
129
|
+
if (done) break;
|
|
130
|
+
chunks.push(value);
|
|
131
|
+
totalLength += value.length;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const result = new Uint8Array(totalLength);
|
|
135
|
+
let offset = 0;
|
|
136
|
+
for (const chunk of chunks) {
|
|
137
|
+
result.set(chunk, offset);
|
|
138
|
+
offset += chunk.length;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Write data to a WritableStream.
|
|
146
|
+
*/
|
|
147
|
+
async function writeToStream(stream: WritableStream<Uint8Array>, data: Uint8Array): Promise<void> {
|
|
148
|
+
const writer = stream.getWriter();
|
|
149
|
+
try {
|
|
150
|
+
await writer.write(data);
|
|
151
|
+
} finally {
|
|
152
|
+
await writer.close();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Pipe a ReadableStream to a WritableStream.
|
|
158
|
+
*/
|
|
159
|
+
async function pipeStreams(
|
|
160
|
+
readable: ReadableStream<Uint8Array>,
|
|
161
|
+
writable: WritableStream<Uint8Array>
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
const reader = readable.getReader();
|
|
164
|
+
const writer = writable.getWriter();
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
while (true) {
|
|
168
|
+
const { done, value } = await reader.read();
|
|
169
|
+
if (done) break;
|
|
170
|
+
await writer.write(value);
|
|
171
|
+
}
|
|
172
|
+
} finally {
|
|
173
|
+
await writer.close();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Recursively copy a directory using streaming.
|
|
179
|
+
*/
|
|
180
|
+
async function copyDirectoryRecursive(
|
|
181
|
+
srcStub: WorkerFilesystem,
|
|
182
|
+
srcPath: string,
|
|
183
|
+
destStub: WorkerFilesystem,
|
|
184
|
+
destPath: string
|
|
185
|
+
): Promise<void> {
|
|
186
|
+
// Create destination directory
|
|
187
|
+
await destStub.mkdir(destPath, { recursive: true });
|
|
188
|
+
|
|
189
|
+
// List source directory
|
|
190
|
+
const entries = await srcStub.readdir(srcPath);
|
|
191
|
+
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
const srcChildPath = srcPath === '/' ? `/${entry.name}` : `${srcPath}/${entry.name}`;
|
|
194
|
+
const destChildPath = destPath === '/' ? `/${entry.name}` : `${destPath}/${entry.name}`;
|
|
195
|
+
|
|
196
|
+
if (entry.type === 'directory') {
|
|
197
|
+
await copyDirectoryRecursive(srcStub, srcChildPath, destStub, destChildPath);
|
|
198
|
+
} else if (entry.type === 'file') {
|
|
199
|
+
const readStream = await srcStub.createReadStream(srcChildPath);
|
|
200
|
+
const writeStream = await destStub.createWriteStream(destChildPath);
|
|
201
|
+
await pipeStreams(readStream, writeStream);
|
|
202
|
+
} else if (entry.type === 'symlink' && srcStub.readlink && destStub.symlink) {
|
|
203
|
+
const target = await srcStub.readlink(srcChildPath);
|
|
204
|
+
await destStub.symlink(destChildPath, target);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// === Wrapped Functions ===
|
|
210
|
+
|
|
211
|
+
export async function readFile(
|
|
212
|
+
path: Parameters<typeof realFs.readFile>[0],
|
|
213
|
+
options?: Parameters<typeof realFs.readFile>[1]
|
|
214
|
+
): Promise<Buffer | string> {
|
|
215
|
+
const pathStr = getPath(path);
|
|
216
|
+
if (pathStr) {
|
|
217
|
+
const match = findMount(pathStr);
|
|
218
|
+
if (match) {
|
|
219
|
+
// Use streaming to read file
|
|
220
|
+
const stream = await match.mount.stub.createReadStream(match.relativePath);
|
|
221
|
+
const data = await collectStream(stream);
|
|
222
|
+
const buffer = Buffer.from(data);
|
|
223
|
+
|
|
224
|
+
const encoding =
|
|
225
|
+
typeof options === 'string'
|
|
226
|
+
? options
|
|
227
|
+
: typeof options === 'object' && options !== null
|
|
228
|
+
? options.encoding
|
|
229
|
+
: undefined;
|
|
230
|
+
|
|
231
|
+
if (encoding) {
|
|
232
|
+
return buffer.toString(encoding as BufferEncoding);
|
|
233
|
+
}
|
|
234
|
+
return buffer;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return realFs.readFile(path, options) as Promise<Buffer | string>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export async function writeFile(
|
|
241
|
+
path: Parameters<typeof realFs.writeFile>[0],
|
|
242
|
+
data: Parameters<typeof realFs.writeFile>[1],
|
|
243
|
+
options?: Parameters<typeof realFs.writeFile>[2]
|
|
244
|
+
): Promise<void> {
|
|
245
|
+
const pathStr = getPath(path);
|
|
246
|
+
if (pathStr) {
|
|
247
|
+
const match = findMount(pathStr);
|
|
248
|
+
if (match) {
|
|
249
|
+
let bytes: Uint8Array;
|
|
250
|
+
if (typeof data === 'string') {
|
|
251
|
+
bytes = new TextEncoder().encode(data);
|
|
252
|
+
} else if (Buffer.isBuffer(data)) {
|
|
253
|
+
bytes = new Uint8Array(data);
|
|
254
|
+
} else if (data instanceof Uint8Array) {
|
|
255
|
+
bytes = data;
|
|
256
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
257
|
+
bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
258
|
+
} else {
|
|
259
|
+
const chunks: Uint8Array[] = [];
|
|
260
|
+
for await (const chunk of data as AsyncIterable<string | Uint8Array>) {
|
|
261
|
+
if (typeof chunk === 'string') {
|
|
262
|
+
chunks.push(new TextEncoder().encode(chunk));
|
|
263
|
+
} else {
|
|
264
|
+
chunks.push(new Uint8Array(chunk));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
268
|
+
bytes = new Uint8Array(totalLength);
|
|
269
|
+
let offset = 0;
|
|
270
|
+
for (const chunk of chunks) {
|
|
271
|
+
bytes.set(chunk, offset);
|
|
272
|
+
offset += chunk.length;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const flag = typeof options === 'object' && options !== null ? options.flag : undefined;
|
|
277
|
+
const isAppend = flag === 'a' || flag === 'a+';
|
|
278
|
+
const isExclusive = flag === 'wx' || flag === 'xw';
|
|
279
|
+
|
|
280
|
+
// Check exclusive flag
|
|
281
|
+
if (isExclusive) {
|
|
282
|
+
const existing = await match.mount.stub.stat(match.relativePath);
|
|
283
|
+
if (existing) {
|
|
284
|
+
throw createFsError('EEXIST', 'writeFile', pathStr);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Use streaming to write file
|
|
289
|
+
const stream = await match.mount.stub.createWriteStream(match.relativePath, {
|
|
290
|
+
flags: isAppend ? 'a' : 'w',
|
|
291
|
+
});
|
|
292
|
+
await writeToStream(stream, bytes);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return realFs.writeFile(path, data, options);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function appendFile(
|
|
300
|
+
path: Parameters<typeof realFs.appendFile>[0],
|
|
301
|
+
data: Parameters<typeof realFs.appendFile>[1],
|
|
302
|
+
options?: Parameters<typeof realFs.appendFile>[2]
|
|
303
|
+
): Promise<void> {
|
|
304
|
+
const pathStr = getPath(path);
|
|
305
|
+
if (pathStr) {
|
|
306
|
+
const match = findMount(pathStr);
|
|
307
|
+
if (match) {
|
|
308
|
+
let bytes: Uint8Array;
|
|
309
|
+
if (typeof data === 'string') {
|
|
310
|
+
bytes = new TextEncoder().encode(data);
|
|
311
|
+
} else {
|
|
312
|
+
bytes = new Uint8Array(data);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Use streaming with append flag
|
|
316
|
+
const stream = await match.mount.stub.createWriteStream(match.relativePath, {
|
|
317
|
+
flags: 'a',
|
|
318
|
+
});
|
|
319
|
+
await writeToStream(stream, bytes);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return realFs.appendFile(path, data, options);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export async function stat(
|
|
327
|
+
path: Parameters<typeof realFs.stat>[0],
|
|
328
|
+
options?: Parameters<typeof realFs.stat>[1]
|
|
329
|
+
): Promise<Stats | BigIntStats> {
|
|
330
|
+
const pathStr = getPath(path);
|
|
331
|
+
if (pathStr) {
|
|
332
|
+
const match = findMount(pathStr);
|
|
333
|
+
if (match) {
|
|
334
|
+
const s = await match.mount.stub.stat(match.relativePath, { followSymlinks: true });
|
|
335
|
+
if (!s) {
|
|
336
|
+
throw createFsError('ENOENT', 'stat', pathStr);
|
|
337
|
+
}
|
|
338
|
+
return toNodeStats(s);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return realFs.stat(path, options) as Promise<Stats | BigIntStats>;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function lstat(
|
|
345
|
+
path: Parameters<typeof realFs.lstat>[0],
|
|
346
|
+
options?: Parameters<typeof realFs.lstat>[1]
|
|
347
|
+
): Promise<Stats | BigIntStats> {
|
|
348
|
+
const pathStr = getPath(path);
|
|
349
|
+
if (pathStr) {
|
|
350
|
+
const match = findMount(pathStr);
|
|
351
|
+
if (match) {
|
|
352
|
+
const s = await match.mount.stub.stat(match.relativePath, { followSymlinks: false });
|
|
353
|
+
if (!s) {
|
|
354
|
+
throw createFsError('ENOENT', 'lstat', pathStr);
|
|
355
|
+
}
|
|
356
|
+
return toNodeStats(s);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return realFs.lstat(path, options) as Promise<Stats | BigIntStats>;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function readdir(
|
|
363
|
+
path: Parameters<typeof realFs.readdir>[0],
|
|
364
|
+
options?: Parameters<typeof realFs.readdir>[1]
|
|
365
|
+
): Promise<string[] | Buffer[] | Dirent[]> {
|
|
366
|
+
const pathStr = getPath(path);
|
|
367
|
+
if (pathStr) {
|
|
368
|
+
const match = findMount(pathStr);
|
|
369
|
+
if (match) {
|
|
370
|
+
const opts = typeof options === 'object' && options !== null ? options : {};
|
|
371
|
+
const recursive = 'recursive' in opts ? opts.recursive === true : false;
|
|
372
|
+
const entries = await match.mount.stub.readdir(match.relativePath, { recursive });
|
|
373
|
+
|
|
374
|
+
const withFileTypes = 'withFileTypes' in opts ? opts.withFileTypes === true : false;
|
|
375
|
+
|
|
376
|
+
if (withFileTypes) {
|
|
377
|
+
return entries.map((e) => toNodeDirent(e, pathStr));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const encoding =
|
|
381
|
+
typeof options === 'string'
|
|
382
|
+
? options
|
|
383
|
+
: typeof options === 'object' && options !== null && 'encoding' in options
|
|
384
|
+
? options.encoding
|
|
385
|
+
: undefined;
|
|
386
|
+
|
|
387
|
+
if (encoding === 'buffer') {
|
|
388
|
+
return entries.map((e) => Buffer.from(e.name));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return entries.map((e) => e.name);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
395
|
+
return realFs.readdir(path, options as any) as Promise<string[] | Buffer[] | Dirent[]>;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export async function mkdir(
|
|
399
|
+
path: Parameters<typeof realFs.mkdir>[0],
|
|
400
|
+
options?: Parameters<typeof realFs.mkdir>[1]
|
|
401
|
+
): Promise<string | undefined> {
|
|
402
|
+
const pathStr = getPath(path);
|
|
403
|
+
if (pathStr) {
|
|
404
|
+
const match = findMount(pathStr);
|
|
405
|
+
if (match) {
|
|
406
|
+
const recursive =
|
|
407
|
+
typeof options === 'object' && options !== null && options.recursive === true;
|
|
408
|
+
return match.mount.stub.mkdir(match.relativePath, { recursive });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return realFs.mkdir(path, options) as Promise<string | undefined>;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export async function rm(
|
|
415
|
+
path: Parameters<typeof realFs.rm>[0],
|
|
416
|
+
options?: Parameters<typeof realFs.rm>[1]
|
|
417
|
+
): Promise<void> {
|
|
418
|
+
const pathStr = getPath(path);
|
|
419
|
+
if (pathStr) {
|
|
420
|
+
const match = findMount(pathStr);
|
|
421
|
+
if (match) {
|
|
422
|
+
const recursive = options?.recursive === true;
|
|
423
|
+
const force = options?.force === true;
|
|
424
|
+
return match.mount.stub.rm(match.relativePath, { recursive, force });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return realFs.rm(path, options);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export async function rmdir(
|
|
431
|
+
path: Parameters<typeof realFs.rmdir>[0],
|
|
432
|
+
options?: { maxRetries?: number; retryDelay?: number }
|
|
433
|
+
): Promise<void> {
|
|
434
|
+
const pathStr = getPath(path);
|
|
435
|
+
if (pathStr) {
|
|
436
|
+
const match = findMount(pathStr);
|
|
437
|
+
if (match) {
|
|
438
|
+
return match.mount.stub.rm(match.relativePath, { recursive: false, force: false });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
442
|
+
return (realFs.rmdir as any)(path, options);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export async function unlink(path: Parameters<typeof realFs.unlink>[0]): Promise<void> {
|
|
446
|
+
const pathStr = getPath(path);
|
|
447
|
+
if (pathStr) {
|
|
448
|
+
const match = findMount(pathStr);
|
|
449
|
+
if (match) {
|
|
450
|
+
// Derive unlink from stat + rm
|
|
451
|
+
const s = await match.mount.stub.stat(match.relativePath);
|
|
452
|
+
if (!s) {
|
|
453
|
+
throw createFsError('ENOENT', 'unlink', pathStr);
|
|
454
|
+
}
|
|
455
|
+
if (s.type === 'directory') {
|
|
456
|
+
throw createFsError('EISDIR', 'unlink', pathStr);
|
|
457
|
+
}
|
|
458
|
+
return match.mount.stub.rm(match.relativePath);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return realFs.unlink(path);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export async function rename(
|
|
465
|
+
oldPath: Parameters<typeof realFs.rename>[0],
|
|
466
|
+
newPath: Parameters<typeof realFs.rename>[1]
|
|
467
|
+
): Promise<void> {
|
|
468
|
+
const oldPathStr = getPath(oldPath);
|
|
469
|
+
const newPathStr = getPath(newPath);
|
|
470
|
+
|
|
471
|
+
if (oldPathStr && newPathStr) {
|
|
472
|
+
const oldMatch = findMount(oldPathStr);
|
|
473
|
+
const newMatch = findMount(newPathStr);
|
|
474
|
+
|
|
475
|
+
// Cross-mount rename not supported
|
|
476
|
+
if (oldMatch?.mount !== newMatch?.mount) {
|
|
477
|
+
throw createFsError('EXDEV', 'rename', oldPathStr, 'Cross-mount rename not supported');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (oldMatch && newMatch) {
|
|
481
|
+
// Derive rename as copy + delete
|
|
482
|
+
const srcStat = await oldMatch.mount.stub.stat(oldMatch.relativePath);
|
|
483
|
+
if (!srcStat) {
|
|
484
|
+
throw createFsError('ENOENT', 'rename', oldPathStr);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (srcStat.type === 'directory') {
|
|
488
|
+
// Recursive directory copy + delete
|
|
489
|
+
await copyDirectoryRecursive(
|
|
490
|
+
oldMatch.mount.stub,
|
|
491
|
+
oldMatch.relativePath,
|
|
492
|
+
newMatch.mount.stub,
|
|
493
|
+
newMatch.relativePath
|
|
494
|
+
);
|
|
495
|
+
await oldMatch.mount.stub.rm(oldMatch.relativePath, { recursive: true });
|
|
496
|
+
} else if (srcStat.type === 'file') {
|
|
497
|
+
// Stream copy + delete
|
|
498
|
+
const readStream = await oldMatch.mount.stub.createReadStream(oldMatch.relativePath);
|
|
499
|
+
const writeStream = await newMatch.mount.stub.createWriteStream(newMatch.relativePath);
|
|
500
|
+
await pipeStreams(readStream, writeStream);
|
|
501
|
+
await oldMatch.mount.stub.rm(oldMatch.relativePath);
|
|
502
|
+
} else if (srcStat.type === 'symlink') {
|
|
503
|
+
// Copy symlink + delete
|
|
504
|
+
if (oldMatch.mount.stub.readlink && newMatch.mount.stub.symlink) {
|
|
505
|
+
const target = await oldMatch.mount.stub.readlink(oldMatch.relativePath);
|
|
506
|
+
await newMatch.mount.stub.symlink(newMatch.relativePath, target);
|
|
507
|
+
await oldMatch.mount.stub.rm(oldMatch.relativePath);
|
|
508
|
+
} else {
|
|
509
|
+
throw createFsError('ENOSYS', 'rename', oldPathStr, 'symlink operations not supported');
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return realFs.rename(oldPath, newPath);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export async function copyFile(
|
|
520
|
+
src: Parameters<typeof realFs.copyFile>[0],
|
|
521
|
+
dest: Parameters<typeof realFs.copyFile>[1],
|
|
522
|
+
mode?: Parameters<typeof realFs.copyFile>[2]
|
|
523
|
+
): Promise<void> {
|
|
524
|
+
const srcStr = getPath(src);
|
|
525
|
+
const destStr = getPath(dest);
|
|
526
|
+
|
|
527
|
+
if (srcStr && destStr) {
|
|
528
|
+
const srcMatch = findMount(srcStr);
|
|
529
|
+
const destMatch = findMount(destStr);
|
|
530
|
+
|
|
531
|
+
if (srcMatch || destMatch) {
|
|
532
|
+
// Use streaming for copy
|
|
533
|
+
if (srcMatch && destMatch) {
|
|
534
|
+
// Both on mounts - pipe streams
|
|
535
|
+
const readStream = await srcMatch.mount.stub.createReadStream(srcMatch.relativePath);
|
|
536
|
+
const writeStream = await destMatch.mount.stub.createWriteStream(destMatch.relativePath);
|
|
537
|
+
await pipeStreams(readStream, writeStream);
|
|
538
|
+
} else if (srcMatch) {
|
|
539
|
+
// Source on mount, dest on real fs
|
|
540
|
+
const readStream = await srcMatch.mount.stub.createReadStream(srcMatch.relativePath);
|
|
541
|
+
const data = await collectStream(readStream);
|
|
542
|
+
await realFs.writeFile(dest, data);
|
|
543
|
+
} else if (destMatch) {
|
|
544
|
+
// Source on real fs, dest on mount
|
|
545
|
+
const buffer = await realFs.readFile(src);
|
|
546
|
+
const writeStream = await destMatch.mount.stub.createWriteStream(destMatch.relativePath);
|
|
547
|
+
await writeToStream(writeStream, new Uint8Array(buffer));
|
|
548
|
+
}
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return realFs.copyFile(src, dest, mode);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export async function cp(
|
|
557
|
+
src: Parameters<typeof realFs.cp>[0],
|
|
558
|
+
dest: Parameters<typeof realFs.cp>[1],
|
|
559
|
+
options?: Parameters<typeof realFs.cp>[2]
|
|
560
|
+
): Promise<void> {
|
|
561
|
+
const srcStr = getPath(src);
|
|
562
|
+
const destStr = getPath(dest);
|
|
563
|
+
|
|
564
|
+
if (srcStr && destStr) {
|
|
565
|
+
const srcMatch = findMount(srcStr);
|
|
566
|
+
const destMatch = findMount(destStr);
|
|
567
|
+
|
|
568
|
+
if (srcMatch || destMatch) {
|
|
569
|
+
// Check if source is a directory
|
|
570
|
+
let srcStat: Stat | null | undefined;
|
|
571
|
+
let isDirectory = false;
|
|
572
|
+
|
|
573
|
+
if (srcMatch) {
|
|
574
|
+
srcStat = await srcMatch.mount.stub.stat(srcMatch.relativePath);
|
|
575
|
+
isDirectory = srcStat?.type === 'directory';
|
|
576
|
+
} else {
|
|
577
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
578
|
+
const realStat = await realFs.stat(src as any);
|
|
579
|
+
isDirectory = realStat.isDirectory();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (isDirectory) {
|
|
583
|
+
if (!options?.recursive) {
|
|
584
|
+
throw createFsError('EISDIR', 'cp', srcStr, 'cp requires recursive for directories');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (srcMatch && destMatch) {
|
|
588
|
+
await copyDirectoryRecursive(
|
|
589
|
+
srcMatch.mount.stub,
|
|
590
|
+
srcMatch.relativePath,
|
|
591
|
+
destMatch.mount.stub,
|
|
592
|
+
destMatch.relativePath
|
|
593
|
+
);
|
|
594
|
+
} else {
|
|
595
|
+
throw createFsError(
|
|
596
|
+
'EXDEV',
|
|
597
|
+
'cp',
|
|
598
|
+
srcStr,
|
|
599
|
+
'Directory copy across mount boundary not supported'
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
} else {
|
|
603
|
+
// File copy using streaming
|
|
604
|
+
if (srcMatch && destMatch) {
|
|
605
|
+
const readStream = await srcMatch.mount.stub.createReadStream(srcMatch.relativePath);
|
|
606
|
+
const writeStream = await destMatch.mount.stub.createWriteStream(destMatch.relativePath);
|
|
607
|
+
await pipeStreams(readStream, writeStream);
|
|
608
|
+
} else if (srcMatch) {
|
|
609
|
+
const readStream = await srcMatch.mount.stub.createReadStream(srcMatch.relativePath);
|
|
610
|
+
const data = await collectStream(readStream);
|
|
611
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
612
|
+
await realFs.writeFile(dest as any, data);
|
|
613
|
+
} else if (destMatch) {
|
|
614
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
615
|
+
const buffer = await realFs.readFile(src as any);
|
|
616
|
+
const writeStream = await destMatch.mount.stub.createWriteStream(destMatch.relativePath);
|
|
617
|
+
await writeToStream(writeStream, new Uint8Array(buffer));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return realFs.cp(src, dest, options);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export async function access(
|
|
628
|
+
path: Parameters<typeof realFs.access>[0],
|
|
629
|
+
mode?: Parameters<typeof realFs.access>[1]
|
|
630
|
+
): Promise<void> {
|
|
631
|
+
const pathStr = getPath(path);
|
|
632
|
+
if (pathStr) {
|
|
633
|
+
const match = findMount(pathStr);
|
|
634
|
+
if (match) {
|
|
635
|
+
// Derive from stat - just check if it exists
|
|
636
|
+
const s = await match.mount.stub.stat(match.relativePath);
|
|
637
|
+
if (!s) {
|
|
638
|
+
throw createFsError('ENOENT', 'access', pathStr);
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return realFs.access(path, mode);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export async function truncate(
|
|
647
|
+
path: Parameters<typeof realFs.truncate>[0],
|
|
648
|
+
len?: Parameters<typeof realFs.truncate>[1]
|
|
649
|
+
): Promise<void> {
|
|
650
|
+
const pathStr = getPath(path);
|
|
651
|
+
if (pathStr) {
|
|
652
|
+
const match = findMount(pathStr);
|
|
653
|
+
if (match) {
|
|
654
|
+
const length = len ?? 0;
|
|
655
|
+
|
|
656
|
+
// Derive truncate using streams
|
|
657
|
+
const srcStat = await match.mount.stub.stat(match.relativePath);
|
|
658
|
+
if (!srcStat) {
|
|
659
|
+
throw createFsError('ENOENT', 'truncate', pathStr);
|
|
660
|
+
}
|
|
661
|
+
if (srcStat.type !== 'file') {
|
|
662
|
+
throw createFsError('EISDIR', 'truncate', pathStr);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
let newData: Uint8Array;
|
|
666
|
+
if (length === 0) {
|
|
667
|
+
// Truncate to empty
|
|
668
|
+
newData = new Uint8Array(0);
|
|
669
|
+
} else if (length >= srcStat.size) {
|
|
670
|
+
// Extend with zeros
|
|
671
|
+
const readStream = await match.mount.stub.createReadStream(match.relativePath);
|
|
672
|
+
const existingData = await collectStream(readStream);
|
|
673
|
+
newData = new Uint8Array(length);
|
|
674
|
+
newData.set(existingData, 0);
|
|
675
|
+
// Rest is already zeros
|
|
676
|
+
} else {
|
|
677
|
+
// Truncate to smaller size - read only what we need
|
|
678
|
+
const readStream = await match.mount.stub.createReadStream(match.relativePath, {
|
|
679
|
+
start: 0,
|
|
680
|
+
end: length - 1,
|
|
681
|
+
});
|
|
682
|
+
newData = await collectStream(readStream);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const writeStream = await match.mount.stub.createWriteStream(match.relativePath);
|
|
686
|
+
await writeToStream(writeStream, newData);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return realFs.truncate(path, len);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export async function symlink(
|
|
694
|
+
target: Parameters<typeof realFs.symlink>[0],
|
|
695
|
+
path: Parameters<typeof realFs.symlink>[1],
|
|
696
|
+
type?: Parameters<typeof realFs.symlink>[2]
|
|
697
|
+
): Promise<void> {
|
|
698
|
+
const pathStr = getPath(path);
|
|
699
|
+
const targetStr = getPath(target);
|
|
700
|
+
|
|
701
|
+
if (pathStr && targetStr) {
|
|
702
|
+
const match = findMount(pathStr);
|
|
703
|
+
if (match) {
|
|
704
|
+
if (!match.mount.stub.symlink) {
|
|
705
|
+
throw createFsError('ENOSYS', 'symlink', pathStr, 'symlink not supported');
|
|
706
|
+
}
|
|
707
|
+
return match.mount.stub.symlink(match.relativePath, targetStr);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return realFs.symlink(target, path, type);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export async function readlink(
|
|
714
|
+
path: Parameters<typeof realFs.readlink>[0],
|
|
715
|
+
options?: Parameters<typeof realFs.readlink>[1]
|
|
716
|
+
): Promise<string | Buffer> {
|
|
717
|
+
const pathStr = getPath(path);
|
|
718
|
+
if (pathStr) {
|
|
719
|
+
const match = findMount(pathStr);
|
|
720
|
+
if (match) {
|
|
721
|
+
if (!match.mount.stub.readlink) {
|
|
722
|
+
throw createFsError('ENOSYS', 'readlink', pathStr, 'readlink not supported');
|
|
723
|
+
}
|
|
724
|
+
const target = await match.mount.stub.readlink(match.relativePath);
|
|
725
|
+
|
|
726
|
+
const encoding =
|
|
727
|
+
typeof options === 'string'
|
|
728
|
+
? options
|
|
729
|
+
: typeof options === 'object' && options !== null
|
|
730
|
+
? options.encoding
|
|
731
|
+
: undefined;
|
|
732
|
+
|
|
733
|
+
if (encoding === 'buffer') {
|
|
734
|
+
return Buffer.from(target);
|
|
735
|
+
}
|
|
736
|
+
return target;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return realFs.readlink(path, options) as Promise<string | Buffer>;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
export async function realpath(
|
|
743
|
+
path: Parameters<typeof realFs.realpath>[0],
|
|
744
|
+
options?: Parameters<typeof realFs.realpath>[1]
|
|
745
|
+
): Promise<string | Buffer> {
|
|
746
|
+
const pathStr = getPath(path);
|
|
747
|
+
if (pathStr) {
|
|
748
|
+
const match = findMount(pathStr);
|
|
749
|
+
if (match) {
|
|
750
|
+
const encoding =
|
|
751
|
+
typeof options === 'string'
|
|
752
|
+
? options
|
|
753
|
+
: typeof options === 'object' && options !== null
|
|
754
|
+
? options.encoding
|
|
755
|
+
: undefined;
|
|
756
|
+
|
|
757
|
+
if ((encoding as string) === 'buffer') {
|
|
758
|
+
return Buffer.from(pathStr);
|
|
759
|
+
}
|
|
760
|
+
return pathStr;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return realFs.realpath(path, options) as Promise<string | Buffer>;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export async function utimes(
|
|
767
|
+
path: Parameters<typeof realFs.utimes>[0],
|
|
768
|
+
atime: Parameters<typeof realFs.utimes>[1],
|
|
769
|
+
mtime: Parameters<typeof realFs.utimes>[2]
|
|
770
|
+
): Promise<void> {
|
|
771
|
+
const pathStr = getPath(path);
|
|
772
|
+
if (pathStr) {
|
|
773
|
+
const match = findMount(pathStr);
|
|
774
|
+
if (match) {
|
|
775
|
+
// utimes is not supported on mounted filesystems
|
|
776
|
+
// Just verify the file exists
|
|
777
|
+
const s = await match.mount.stub.stat(match.relativePath);
|
|
778
|
+
if (!s) {
|
|
779
|
+
throw createFsError('ENOENT', 'utimes', pathStr);
|
|
780
|
+
}
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return realFs.utimes(path, atime, mtime);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Re-export functions we don't need to wrap
|
|
788
|
+
export const chmod = realFs.chmod;
|
|
789
|
+
export const chown = realFs.chown;
|
|
790
|
+
export const lchmod = realFs.lchmod;
|
|
791
|
+
export const lchown = realFs.lchown;
|
|
792
|
+
export const lutimes = realFs.lutimes;
|
|
793
|
+
export const link = realFs.link;
|
|
794
|
+
export const open = realFs.open;
|
|
795
|
+
export const opendir = realFs.opendir;
|
|
796
|
+
export const mkdtemp = realFs.mkdtemp;
|
|
797
|
+
export const watch = realFs.watch;
|
|
798
|
+
export const constants = realFs.constants;
|
|
799
|
+
|
|
800
|
+
// Default export for `import fs from 'node:fs/promises'` style imports
|
|
801
|
+
export default {
|
|
802
|
+
readFile,
|
|
803
|
+
writeFile,
|
|
804
|
+
appendFile,
|
|
805
|
+
stat,
|
|
806
|
+
lstat,
|
|
807
|
+
readdir,
|
|
808
|
+
mkdir,
|
|
809
|
+
rm,
|
|
810
|
+
rmdir,
|
|
811
|
+
unlink,
|
|
812
|
+
rename,
|
|
813
|
+
copyFile,
|
|
814
|
+
cp,
|
|
815
|
+
access,
|
|
816
|
+
truncate,
|
|
817
|
+
symlink,
|
|
818
|
+
readlink,
|
|
819
|
+
realpath,
|
|
820
|
+
utimes,
|
|
821
|
+
chmod,
|
|
822
|
+
chown,
|
|
823
|
+
lchmod,
|
|
824
|
+
lchown,
|
|
825
|
+
lutimes,
|
|
826
|
+
link,
|
|
827
|
+
open,
|
|
828
|
+
opendir,
|
|
829
|
+
mkdtemp,
|
|
830
|
+
watch,
|
|
831
|
+
constants,
|
|
832
|
+
};
|