@react-native-gpui/fs 0.0.0-stage → 0.0.1-alpha.1
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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +36 -2
- package/desktop/Cargo.toml +29 -0
- package/desktop/build.rs +3 -0
- package/desktop/src/dialogs.rs +394 -0
- package/desktop/src/dirs.rs +363 -0
- package/desktop/src/download.rs +329 -0
- package/desktop/src/errors.rs +149 -0
- package/desktop/src/files.rs +640 -0
- package/desktop/src/handles.rs +697 -0
- package/desktop/src/lib.rs +286 -0
- package/desktop/src/links.rs +336 -0
- package/desktop/src/module.rs +192 -0
- package/desktop/src/paths.rs +231 -0
- package/desktop/src/upload.rs +237 -0
- package/desktop/src/watching.rs +392 -0
- package/package.json +38 -4
- package/react-native.config.js +9 -0
- package/src/NativeFs.ts +235 -0
- package/src/constants.ts +43 -0
- package/src/download.ts +110 -0
- package/src/index.ts +788 -0
- package/src/promises.ts +771 -0
- package/src/shared.ts +539 -0
- package/src/stats.ts +196 -0
- package/src/upload.ts +97 -0
- package/src/watch.ts +141 -0
package/src/promises.ts
ADDED
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fs.promises`: the Node-shaped asynchronous face of the module.
|
|
3
|
+
*
|
|
4
|
+
* Every call here is a thin translation. The native side does the work on its own
|
|
5
|
+
* thread and reports Node codes, so this layer's job is argument shapes, the
|
|
6
|
+
* encodings Node implements above the bytes, and the fields (`syscall`, `path`,
|
|
7
|
+
* `dest`) that only a wrapper can know.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import NativeFs from './NativeFs';
|
|
11
|
+
import { constants } from './constants';
|
|
12
|
+
import {
|
|
13
|
+
checkBufferRange,
|
|
14
|
+
decodeString,
|
|
15
|
+
encodeString,
|
|
16
|
+
flagFrom,
|
|
17
|
+
modeFrom,
|
|
18
|
+
nodeError,
|
|
19
|
+
normalizeEncoding,
|
|
20
|
+
relativeEntryName,
|
|
21
|
+
toArrayBuffer,
|
|
22
|
+
toBytes,
|
|
23
|
+
toPath,
|
|
24
|
+
withContext,
|
|
25
|
+
type BinaryData,
|
|
26
|
+
type Encoding,
|
|
27
|
+
type NormalizedEncoding,
|
|
28
|
+
} from './shared';
|
|
29
|
+
import { Dirent, Stats, type DirEntryResult, type StatResult } from './stats';
|
|
30
|
+
|
|
31
|
+
/** An encoding, an options object, or nothing, as Node accepts for reads. */
|
|
32
|
+
export type ReadOptions =
|
|
33
|
+
Encoding | { encoding?: Encoding | null } | null | undefined;
|
|
34
|
+
|
|
35
|
+
export interface WriteOptions {
|
|
36
|
+
encoding?: Encoding | null;
|
|
37
|
+
mode?: number | string;
|
|
38
|
+
flag?: string | number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface MkdirOptions {
|
|
42
|
+
recursive?: boolean;
|
|
43
|
+
mode?: number | string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RmOptions {
|
|
47
|
+
recursive?: boolean;
|
|
48
|
+
force?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ReaddirOptions {
|
|
52
|
+
withFileTypes?: boolean;
|
|
53
|
+
recursive?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface CpOptions {
|
|
57
|
+
recursive?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface OpenOptions {
|
|
61
|
+
flags?: string | number;
|
|
62
|
+
mode?: number | string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The chunk size for a read that has no caller-supplied buffer. */
|
|
66
|
+
const READ_CHUNK = 64 * 1024;
|
|
67
|
+
|
|
68
|
+
/** A time as Node accepts one: `Date`, milliseconds, or a date string. */
|
|
69
|
+
export type TimeLike = Date | number | string;
|
|
70
|
+
|
|
71
|
+
function toMillis(value: TimeLike, name: string): number {
|
|
72
|
+
if (value instanceof Date) {
|
|
73
|
+
return value.getTime();
|
|
74
|
+
}
|
|
75
|
+
if (typeof value === 'number') {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
const parsed = Date.parse(value);
|
|
79
|
+
if (Number.isNaN(parsed)) {
|
|
80
|
+
throw nodeError(
|
|
81
|
+
'ERR_INVALID_ARG_VALUE',
|
|
82
|
+
`The argument '${name}' is invalid time. Received '${value}'`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return parsed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function encodingOf(options: ReadOptions): NormalizedEncoding | null {
|
|
89
|
+
if (typeof options === 'string') {
|
|
90
|
+
return normalizeEncoding(options);
|
|
91
|
+
}
|
|
92
|
+
return normalizeEncoding(options?.encoding ?? null);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* `readFile`, in whichever of the two shapes the caller asked for.
|
|
97
|
+
*
|
|
98
|
+
* UTF-8 goes through the dedicated text method: the native side decodes it while
|
|
99
|
+
* the bytes are still in Rust, so a text read never allocates an `ArrayBuffer`
|
|
100
|
+
* that JS then copies. Every other encoding is decoded here, because Node's
|
|
101
|
+
* `utf16le`, `latin1`, `base64` and `hex` are JS-level conventions rather than
|
|
102
|
+
* something `std::fs` has an opinion about.
|
|
103
|
+
*/
|
|
104
|
+
function readFile(path: string): Promise<Uint8Array>;
|
|
105
|
+
function readFile(
|
|
106
|
+
path: string,
|
|
107
|
+
options: { encoding?: null } | null | undefined,
|
|
108
|
+
): Promise<Uint8Array>;
|
|
109
|
+
function readFile(
|
|
110
|
+
path: string,
|
|
111
|
+
options: Encoding | { encoding: Encoding },
|
|
112
|
+
): Promise<string>;
|
|
113
|
+
async function readFile(
|
|
114
|
+
path: string,
|
|
115
|
+
options?: ReadOptions,
|
|
116
|
+
): Promise<Uint8Array | string> {
|
|
117
|
+
const target = toPath(path);
|
|
118
|
+
const encoding = encodingOf(options);
|
|
119
|
+
try {
|
|
120
|
+
if (encoding === 'utf8') {
|
|
121
|
+
return await NativeFs.readTextFile(target);
|
|
122
|
+
}
|
|
123
|
+
const buffer = await NativeFs.readFile(target);
|
|
124
|
+
const bytes = new Uint8Array(buffer);
|
|
125
|
+
return encoding === null ? bytes : decodeString(bytes, encoding);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw withContext(error, 'open', target);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The same read, typed coarsely.
|
|
133
|
+
*
|
|
134
|
+
* `readFile`'s public overloads are what a caller sees; the callback layer calls
|
|
135
|
+
* through this alias because at that point the encoding is a runtime value and
|
|
136
|
+
* the result is genuinely either shape.
|
|
137
|
+
*/
|
|
138
|
+
export const readFileImpl: (
|
|
139
|
+
path: string,
|
|
140
|
+
options?: ReadOptions,
|
|
141
|
+
) => Promise<Uint8Array | string> = readFile;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* `writeFile`.
|
|
145
|
+
*
|
|
146
|
+
* A string is written through `writeTextFile` when it is UTF-8; anything else —
|
|
147
|
+
* a different encoding, or binary data — is normalised to an `ArrayBuffer` and
|
|
148
|
+
* sent through the byte method.
|
|
149
|
+
*/
|
|
150
|
+
async function writeFile(
|
|
151
|
+
path: string,
|
|
152
|
+
data: BinaryData,
|
|
153
|
+
options?: WriteOptions | Encoding,
|
|
154
|
+
): Promise<void> {
|
|
155
|
+
const target = toPath(path);
|
|
156
|
+
const opts: WriteOptions =
|
|
157
|
+
typeof options === 'string' ? { encoding: options } : (options ?? {});
|
|
158
|
+
const flag = flagFrom(
|
|
159
|
+
opts.flag,
|
|
160
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC,
|
|
161
|
+
);
|
|
162
|
+
const mode = modeFrom(opts.mode);
|
|
163
|
+
try {
|
|
164
|
+
if (typeof data === 'string') {
|
|
165
|
+
// `normalizeEncoding` answers `null` only when it is given nothing, and a
|
|
166
|
+
// default is supplied here, so the fallback to `utf8` is unreachable — it
|
|
167
|
+
// exists to keep the type honest instead of asserting it away.
|
|
168
|
+
const encoding = normalizeEncoding(opts.encoding ?? 'utf8') ?? 'utf8';
|
|
169
|
+
if (encoding === 'utf8') {
|
|
170
|
+
await NativeFs.writeTextFile(target, data, flag, mode);
|
|
171
|
+
} else {
|
|
172
|
+
await NativeFs.writeFile(
|
|
173
|
+
target,
|
|
174
|
+
toArrayBuffer(encodeString(data, encoding)),
|
|
175
|
+
flag,
|
|
176
|
+
mode,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
await NativeFs.writeFile(target, toArrayBuffer(data), flag, mode);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw withContext(error, 'open', target);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function appendFile(
|
|
188
|
+
path: string,
|
|
189
|
+
data: BinaryData,
|
|
190
|
+
options?: WriteOptions | Encoding,
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
const opts: WriteOptions =
|
|
193
|
+
typeof options === 'string' ? { encoding: options } : (options ?? {});
|
|
194
|
+
return writeFile(path, data, {
|
|
195
|
+
...opts,
|
|
196
|
+
flag:
|
|
197
|
+
opts.flag ?? constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function readdir(
|
|
202
|
+
path: string,
|
|
203
|
+
options?: { withFileTypes?: false; recursive?: boolean } | null,
|
|
204
|
+
): Promise<string[]>;
|
|
205
|
+
function readdir(
|
|
206
|
+
path: string,
|
|
207
|
+
options: { withFileTypes: true; recursive?: boolean },
|
|
208
|
+
): Promise<Dirent[]>;
|
|
209
|
+
async function readdir(
|
|
210
|
+
path: string,
|
|
211
|
+
options?: ReaddirOptions | null,
|
|
212
|
+
): Promise<string[] | Dirent[]> {
|
|
213
|
+
const target = toPath(path);
|
|
214
|
+
try {
|
|
215
|
+
const recursive = options?.recursive ?? false;
|
|
216
|
+
const entries = await NativeFs.readdir(target, recursive);
|
|
217
|
+
return options?.withFileTypes
|
|
218
|
+
? entries.map(entry => new Dirent(entry as DirEntryResult))
|
|
219
|
+
: entries.map(entry =>
|
|
220
|
+
recursive
|
|
221
|
+
? relativeEntryName(target, entry.parentPath, entry.name)
|
|
222
|
+
: entry.name,
|
|
223
|
+
);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
throw withContext(error, 'scandir', target);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function stat(path: string): Promise<Stats> {
|
|
230
|
+
const target = toPath(path);
|
|
231
|
+
try {
|
|
232
|
+
return new Stats((await NativeFs.stat(target, true)) as StatResult);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
throw withContext(error, 'stat', target);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function lstat(path: string): Promise<Stats> {
|
|
239
|
+
const target = toPath(path);
|
|
240
|
+
try {
|
|
241
|
+
return new Stats((await NativeFs.stat(target, false)) as StatResult);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
throw withContext(error, 'lstat', target);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function mkdir(
|
|
248
|
+
path: string,
|
|
249
|
+
options?: MkdirOptions | number,
|
|
250
|
+
): Promise<string | undefined> {
|
|
251
|
+
const target = toPath(path);
|
|
252
|
+
const opts: MkdirOptions =
|
|
253
|
+
typeof options === 'number' ? { mode: options } : (options ?? {});
|
|
254
|
+
try {
|
|
255
|
+
await NativeFs.mkdir(
|
|
256
|
+
target,
|
|
257
|
+
opts.recursive ?? false,
|
|
258
|
+
modeFrom(opts.mode, 0o777),
|
|
259
|
+
);
|
|
260
|
+
return undefined;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
throw withContext(error, 'mkdir', target);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function rm(path: string, options?: RmOptions): Promise<void> {
|
|
267
|
+
const target = toPath(path);
|
|
268
|
+
try {
|
|
269
|
+
await NativeFs.rm(
|
|
270
|
+
target,
|
|
271
|
+
options?.recursive ?? false,
|
|
272
|
+
options?.force ?? false,
|
|
273
|
+
);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
throw withContext(error, 'rm', target);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function rename(from: string, to: string): Promise<void> {
|
|
280
|
+
const source = toPath(from);
|
|
281
|
+
const destination = toPath(to);
|
|
282
|
+
try {
|
|
283
|
+
await NativeFs.rename(source, destination);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
// Node's rename error names both ends; the second is `dest`, not `path`.
|
|
286
|
+
const wrapped = withContext(error, 'rename', source);
|
|
287
|
+
if (wrapped.dest === undefined) {
|
|
288
|
+
wrapped.dest = destination;
|
|
289
|
+
}
|
|
290
|
+
throw wrapped;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function copyFile(
|
|
295
|
+
from: string,
|
|
296
|
+
to: string,
|
|
297
|
+
mode?: number,
|
|
298
|
+
): Promise<void> {
|
|
299
|
+
const source = toPath(from);
|
|
300
|
+
const destination = toPath(to);
|
|
301
|
+
if (mode !== undefined && (mode & constants.COPYFILE_EXCL) !== 0) {
|
|
302
|
+
// The native copy has no exclusive flag, so the check is made here. It is
|
|
303
|
+
// advisory — a file created between the two calls still wins — but it gives
|
|
304
|
+
// the caller the error Node promises for the ordinary case.
|
|
305
|
+
try {
|
|
306
|
+
await NativeFs.stat(destination, false);
|
|
307
|
+
throw nodeError(
|
|
308
|
+
'EEXIST',
|
|
309
|
+
`EEXIST: file already exists, copyfile '${source}' -> '${destination}'`,
|
|
310
|
+
);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
if ((error as { code?: string }).code !== 'ENOENT') {
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
await NativeFs.copy(source, destination, false);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
const wrapped = withContext(error, 'copyfile', source);
|
|
321
|
+
if (wrapped.dest === undefined) {
|
|
322
|
+
wrapped.dest = destination;
|
|
323
|
+
}
|
|
324
|
+
throw wrapped;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function cp(
|
|
329
|
+
from: string,
|
|
330
|
+
to: string,
|
|
331
|
+
options?: CpOptions,
|
|
332
|
+
): Promise<void> {
|
|
333
|
+
const source = toPath(from);
|
|
334
|
+
const destination = toPath(to);
|
|
335
|
+
try {
|
|
336
|
+
await NativeFs.copy(source, destination, options?.recursive ?? false);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
const wrapped = withContext(error, 'cp', source);
|
|
339
|
+
if (wrapped.dest === undefined) {
|
|
340
|
+
wrapped.dest = destination;
|
|
341
|
+
}
|
|
342
|
+
throw wrapped;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function access(path: string, mode?: number): Promise<void> {
|
|
347
|
+
const target = toPath(path);
|
|
348
|
+
try {
|
|
349
|
+
await NativeFs.access(target, mode ?? constants.F_OK);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
throw withContext(error, 'access', target);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function realpath(path: string): Promise<string> {
|
|
356
|
+
const target = toPath(path);
|
|
357
|
+
try {
|
|
358
|
+
return await NativeFs.realpath(target);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
throw withContext(error, 'realpath', target);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function readlink(path: string): Promise<string> {
|
|
365
|
+
const target = toPath(path);
|
|
366
|
+
try {
|
|
367
|
+
return await NativeFs.readlink(target);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
throw withContext(error, 'readlink', target);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function symlink(
|
|
374
|
+
target: string,
|
|
375
|
+
path: string,
|
|
376
|
+
type?: 'file' | 'dir' | 'junction',
|
|
377
|
+
): Promise<void> {
|
|
378
|
+
// `type` is passed through rather than dropped. It is the only way to ask for a
|
|
379
|
+
// directory link on Windows, where creating one as a file link leaves a path
|
|
380
|
+
// that cannot be traversed; Unix ignores it, as the syscall does.
|
|
381
|
+
const link = toPath(path);
|
|
382
|
+
try {
|
|
383
|
+
await NativeFs.symlink(toPath(target), link, type ?? null);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
throw withContext(error, 'symlink', link);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function truncate(path: string, length?: number): Promise<void> {
|
|
390
|
+
const target = toPath(path);
|
|
391
|
+
try {
|
|
392
|
+
await NativeFs.truncate(target, length ?? 0);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
throw withContext(error, 'truncate', target);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function utimes(
|
|
399
|
+
path: string,
|
|
400
|
+
atime: TimeLike,
|
|
401
|
+
mtime: TimeLike,
|
|
402
|
+
): Promise<void> {
|
|
403
|
+
const target = toPath(path);
|
|
404
|
+
try {
|
|
405
|
+
await NativeFs.utimes(
|
|
406
|
+
target,
|
|
407
|
+
toMillis(atime, 'atime'),
|
|
408
|
+
toMillis(mtime, 'mtime'),
|
|
409
|
+
);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
throw withContext(error, 'utime', target);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function chmod(path: string, mode: number | string): Promise<void> {
|
|
416
|
+
const target = toPath(path);
|
|
417
|
+
try {
|
|
418
|
+
await NativeFs.chmod(target, modeFrom(mode));
|
|
419
|
+
} catch (error) {
|
|
420
|
+
throw withContext(error, 'chmod', target);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function mkdtemp(
|
|
425
|
+
prefix: string,
|
|
426
|
+
options?: Encoding | { encoding?: Encoding },
|
|
427
|
+
): Promise<string> {
|
|
428
|
+
// The native side creates the directory under the system temp directory and
|
|
429
|
+
// returns the path; the encoding only affects how a non-ASCII prefix arrives,
|
|
430
|
+
// and every string crosses the bridge as UTF-8.
|
|
431
|
+
options;
|
|
432
|
+
try {
|
|
433
|
+
return await NativeFs.mkdtemp(toPath(prefix));
|
|
434
|
+
} catch (error) {
|
|
435
|
+
throw withContext(error, 'mkdtemp', prefix);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* A descriptor plus the operations Node puts on `FileHandle`.
|
|
441
|
+
*
|
|
442
|
+
* Positioned reads and writes do not touch the file's offset, which is what
|
|
443
|
+
* makes concurrent chunked I/O safe: two readers on one handle cannot move each
|
|
444
|
+
* other's cursor.
|
|
445
|
+
*/
|
|
446
|
+
export class FileHandle {
|
|
447
|
+
/** The native descriptor. */
|
|
448
|
+
readonly fd: number;
|
|
449
|
+
private closed = false;
|
|
450
|
+
|
|
451
|
+
constructor(fd: number) {
|
|
452
|
+
this.fd = fd;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private assertOpen(): void {
|
|
456
|
+
if (this.closed) {
|
|
457
|
+
throw nodeError('EBADF', `EBADF: bad file descriptor, ${this.fd}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Reads from the current offset when `position` is `-1`. */
|
|
462
|
+
async read(): Promise<{ bytesRead: number; buffer: Uint8Array }>;
|
|
463
|
+
async read(options: {
|
|
464
|
+
buffer?: Uint8Array;
|
|
465
|
+
offset?: number;
|
|
466
|
+
length?: number;
|
|
467
|
+
position?: number | null;
|
|
468
|
+
}): Promise<{ bytesRead: number; buffer: Uint8Array }>;
|
|
469
|
+
async read(
|
|
470
|
+
buffer: Uint8Array,
|
|
471
|
+
offset?: number,
|
|
472
|
+
length?: number,
|
|
473
|
+
position?: number | null,
|
|
474
|
+
): Promise<{ bytesRead: number; buffer: Uint8Array }>;
|
|
475
|
+
/**
|
|
476
|
+
* Node's positional form with the buffer implied: `read(size, offset, length,
|
|
477
|
+
* position)`. `offset` and `length` are a window into the freshly allocated
|
|
478
|
+
* buffer, and a missing `position` reads from the cursor.
|
|
479
|
+
*/
|
|
480
|
+
async read(
|
|
481
|
+
size: number,
|
|
482
|
+
offset?: number,
|
|
483
|
+
length?: number,
|
|
484
|
+
position?: number | null,
|
|
485
|
+
): Promise<{ bytesRead: number; buffer: Uint8Array }>;
|
|
486
|
+
async read(
|
|
487
|
+
first?:
|
|
488
|
+
| number
|
|
489
|
+
| Uint8Array
|
|
490
|
+
| {
|
|
491
|
+
buffer?: Uint8Array;
|
|
492
|
+
offset?: number;
|
|
493
|
+
length?: number;
|
|
494
|
+
position?: number | null;
|
|
495
|
+
},
|
|
496
|
+
second?: number,
|
|
497
|
+
third?: number,
|
|
498
|
+
fourth?: number | null,
|
|
499
|
+
): Promise<{ bytesRead: number; buffer: Uint8Array }> {
|
|
500
|
+
this.assertOpen();
|
|
501
|
+
let destination: Uint8Array;
|
|
502
|
+
let offset = 0;
|
|
503
|
+
let length: number;
|
|
504
|
+
let position: number;
|
|
505
|
+
// The two no-buffer forms mean "read to the end", which is a different
|
|
506
|
+
// operation from a single bounded read and is handled by `readToEnd`.
|
|
507
|
+
let toEnd = false;
|
|
508
|
+
if (first === undefined) {
|
|
509
|
+
toEnd = true;
|
|
510
|
+
destination = new Uint8Array(0);
|
|
511
|
+
length = 0;
|
|
512
|
+
position = -1;
|
|
513
|
+
} else if (typeof first === 'number') {
|
|
514
|
+
// A size instead of a buffer: allocate it and apply the same window
|
|
515
|
+
// arguments Node's buffer form takes.
|
|
516
|
+
destination = new Uint8Array(first);
|
|
517
|
+
offset = second ?? 0;
|
|
518
|
+
length = third ?? Math.max(destination.byteLength - offset, 0);
|
|
519
|
+
position = fourth ?? -1;
|
|
520
|
+
} else if (first instanceof Uint8Array) {
|
|
521
|
+
destination = first;
|
|
522
|
+
offset = second ?? 0;
|
|
523
|
+
length = third ?? destination.byteLength - offset;
|
|
524
|
+
position = fourth ?? -1;
|
|
525
|
+
} else {
|
|
526
|
+
destination = first.buffer ?? new Uint8Array(0);
|
|
527
|
+
offset = first.offset ?? 0;
|
|
528
|
+
length = first.length ?? Math.max(destination.byteLength - offset, 0);
|
|
529
|
+
position = first.position ?? -1;
|
|
530
|
+
toEnd = first.buffer === undefined;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
if (toEnd) {
|
|
535
|
+
const bytes = await this.readToEnd(position);
|
|
536
|
+
return { bytesRead: bytes.byteLength, buffer: bytes };
|
|
537
|
+
}
|
|
538
|
+
// Validated before the native call, not after. The native side reads
|
|
539
|
+
// `length` bytes and advances the cursor, so a window that overruns the
|
|
540
|
+
// buffer used to consume bytes and then drop them when the result was
|
|
541
|
+
// trimmed to fit. Node rejects the call instead, and so must this.
|
|
542
|
+
const window = checkBufferRange(destination, offset, length);
|
|
543
|
+
const buffer = await NativeFs.read(this.fd, window.length, position);
|
|
544
|
+
const bytes = new Uint8Array(buffer);
|
|
545
|
+
const bytesRead = Math.min(bytes.byteLength, window.length);
|
|
546
|
+
destination.set(bytes.subarray(0, bytesRead), window.offset);
|
|
547
|
+
return { bytesRead, buffer: destination };
|
|
548
|
+
} catch (error) {
|
|
549
|
+
throw withContext(error, 'read', undefined);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Reads until a short read, which is end of file.
|
|
555
|
+
*
|
|
556
|
+
* The native `read` returns at most what it was asked for, so "the rest of the
|
|
557
|
+
* file" has to be assembled: a single call with a huge length would ask Rust to
|
|
558
|
+
* allocate that much up front. A cursor read (`position` -1) advances the
|
|
559
|
+
* handle's offset, so the loop continues where the previous chunk ended.
|
|
560
|
+
*/
|
|
561
|
+
private async readToEnd(position: number): Promise<Uint8Array> {
|
|
562
|
+
const chunks: Uint8Array[] = [];
|
|
563
|
+
let total = 0;
|
|
564
|
+
let at = position;
|
|
565
|
+
for (;;) {
|
|
566
|
+
const buffer = await NativeFs.read(this.fd, READ_CHUNK, at);
|
|
567
|
+
const chunk = new Uint8Array(buffer);
|
|
568
|
+
if (chunk.byteLength === 0) {
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
chunks.push(chunk);
|
|
572
|
+
total += chunk.byteLength;
|
|
573
|
+
if (chunk.byteLength < READ_CHUNK) {
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
if (at >= 0) {
|
|
577
|
+
at += chunk.byteLength;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const all = new Uint8Array(total);
|
|
581
|
+
let cursor = 0;
|
|
582
|
+
for (const chunk of chunks) {
|
|
583
|
+
all.set(chunk, cursor);
|
|
584
|
+
cursor += chunk.byteLength;
|
|
585
|
+
}
|
|
586
|
+
return all;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async write(
|
|
590
|
+
data: BinaryData,
|
|
591
|
+
position?: number | null,
|
|
592
|
+
encoding?: Encoding,
|
|
593
|
+
): Promise<{ bytesWritten: number; buffer: Uint8Array }>;
|
|
594
|
+
async write(
|
|
595
|
+
buffer: Uint8Array,
|
|
596
|
+
offset: number,
|
|
597
|
+
length: number,
|
|
598
|
+
position?: number | null,
|
|
599
|
+
): Promise<{ bytesWritten: number; buffer: Uint8Array }>;
|
|
600
|
+
async write(
|
|
601
|
+
data: BinaryData,
|
|
602
|
+
second?: number | null,
|
|
603
|
+
third?: number | Encoding,
|
|
604
|
+
fourth?: number | null,
|
|
605
|
+
): Promise<{ bytesWritten: number; buffer: Uint8Array }> {
|
|
606
|
+
this.assertOpen();
|
|
607
|
+
let bytes: Uint8Array;
|
|
608
|
+
let position: number | null = null;
|
|
609
|
+
if (typeof data === 'string') {
|
|
610
|
+
// `write(string, position, encoding)`: here the number is a file position.
|
|
611
|
+
const encodingName =
|
|
612
|
+
typeof third === 'string' ? normalizeEncoding(third) : 'utf8';
|
|
613
|
+
bytes = encodeString(data, encodingName ?? 'utf8');
|
|
614
|
+
position = second ?? null;
|
|
615
|
+
} else if (
|
|
616
|
+
data instanceof Uint8Array &&
|
|
617
|
+
third !== undefined &&
|
|
618
|
+
typeof third !== 'number'
|
|
619
|
+
) {
|
|
620
|
+
// `write(buffer, { offset, length, position })`, which Node also accepts.
|
|
621
|
+
const options = third as unknown as {
|
|
622
|
+
offset?: number;
|
|
623
|
+
length?: number;
|
|
624
|
+
position?: number | null;
|
|
625
|
+
};
|
|
626
|
+
const window = checkBufferRange(data, options.offset, options.length);
|
|
627
|
+
bytes = data.subarray(window.offset, window.offset + window.length);
|
|
628
|
+
position = options.position ?? null;
|
|
629
|
+
} else if (
|
|
630
|
+
data instanceof Uint8Array &&
|
|
631
|
+
second !== undefined &&
|
|
632
|
+
second !== null
|
|
633
|
+
) {
|
|
634
|
+
// `write(buffer, offset[, length[, position]])` — the numeric argument is
|
|
635
|
+
// into the *buffer*, not into the file. Reading it as a file position
|
|
636
|
+
// wrote the whole buffer at that offset instead of a slice at the cursor.
|
|
637
|
+
const window = checkBufferRange(
|
|
638
|
+
data,
|
|
639
|
+
second,
|
|
640
|
+
typeof third === 'number' ? third : undefined,
|
|
641
|
+
);
|
|
642
|
+
bytes = data.subarray(window.offset, window.offset + window.length);
|
|
643
|
+
position = fourth ?? null;
|
|
644
|
+
} else {
|
|
645
|
+
bytes = toBytes(data);
|
|
646
|
+
position = typeof second === 'number' ? second : null;
|
|
647
|
+
}
|
|
648
|
+
try {
|
|
649
|
+
const bytesWritten = await NativeFs.write(
|
|
650
|
+
this.fd,
|
|
651
|
+
toArrayBuffer(bytes),
|
|
652
|
+
position ?? -1,
|
|
653
|
+
);
|
|
654
|
+
return { bytesWritten, buffer: bytes };
|
|
655
|
+
} catch (error) {
|
|
656
|
+
throw withContext(error, 'write', undefined);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async stat(): Promise<Stats> {
|
|
661
|
+
this.assertOpen();
|
|
662
|
+
try {
|
|
663
|
+
return new Stats((await NativeFs.fstat(this.fd)) as StatResult);
|
|
664
|
+
} catch (error) {
|
|
665
|
+
throw withContext(error, 'fstat', undefined);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async sync(): Promise<void> {
|
|
670
|
+
this.assertOpen();
|
|
671
|
+
try {
|
|
672
|
+
await NativeFs.fsync(this.fd);
|
|
673
|
+
} catch (error) {
|
|
674
|
+
throw withContext(error, 'fsync', undefined);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async truncate(length?: number): Promise<void> {
|
|
679
|
+
this.assertOpen();
|
|
680
|
+
try {
|
|
681
|
+
await NativeFs.ftruncate(this.fd, length ?? 0);
|
|
682
|
+
} catch (error) {
|
|
683
|
+
throw withContext(error, 'ftruncate', undefined);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async close(): Promise<void> {
|
|
688
|
+
if (this.closed) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
this.closed = true;
|
|
692
|
+
try {
|
|
693
|
+
await NativeFs.close(this.fd);
|
|
694
|
+
} catch (error) {
|
|
695
|
+
throw withContext(error, 'close', undefined);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async function open(
|
|
701
|
+
path: string,
|
|
702
|
+
flags?: string | number | OpenOptions,
|
|
703
|
+
mode?: number | string,
|
|
704
|
+
): Promise<FileHandle> {
|
|
705
|
+
const target = toPath(path);
|
|
706
|
+
const opts: OpenOptions =
|
|
707
|
+
typeof flags === 'object' && flags !== null ? flags : { flags, mode };
|
|
708
|
+
const flag = flagFrom(opts.flags, constants.O_RDONLY);
|
|
709
|
+
try {
|
|
710
|
+
const fd = await NativeFs.open(target, flag, modeFrom(opts.mode));
|
|
711
|
+
return new FileHandle(fd);
|
|
712
|
+
} catch (error) {
|
|
713
|
+
throw withContext(error, 'open', target);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async function reveal(path: string): Promise<void> {
|
|
718
|
+
const target = toPath(path);
|
|
719
|
+
try {
|
|
720
|
+
await NativeFs.reveal(target);
|
|
721
|
+
} catch (error) {
|
|
722
|
+
throw withContext(error, 'reveal', target);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async function openExternal(path: string): Promise<void> {
|
|
727
|
+
const target = toPath(path);
|
|
728
|
+
try {
|
|
729
|
+
await NativeFs.openExternal(target);
|
|
730
|
+
} catch (error) {
|
|
731
|
+
throw withContext(error, 'openExternal', target);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* The `fs/promises` object.
|
|
737
|
+
*
|
|
738
|
+
* The functions are declared separately and collected here so the callback layer
|
|
739
|
+
* in `index.ts` can wrap the same implementations rather than reimplementing
|
|
740
|
+
* them.
|
|
741
|
+
*/
|
|
742
|
+
export const promises = {
|
|
743
|
+
readFile,
|
|
744
|
+
writeFile,
|
|
745
|
+
appendFile,
|
|
746
|
+
readdir,
|
|
747
|
+
stat,
|
|
748
|
+
lstat,
|
|
749
|
+
mkdir,
|
|
750
|
+
rm,
|
|
751
|
+
rename,
|
|
752
|
+
copyFile,
|
|
753
|
+
cp,
|
|
754
|
+
access,
|
|
755
|
+
realpath,
|
|
756
|
+
readlink,
|
|
757
|
+
symlink,
|
|
758
|
+
truncate,
|
|
759
|
+
utimes,
|
|
760
|
+
chmod,
|
|
761
|
+
mkdtemp,
|
|
762
|
+
open,
|
|
763
|
+
reveal,
|
|
764
|
+
openExternal,
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
export type Promises = typeof promises;
|
|
768
|
+
|
|
769
|
+
/** Re-exported so `fs.stat` and `fs.promises.stat` share one class identity. */
|
|
770
|
+
export { Dirent, Stats };
|
|
771
|
+
export type { StatResult };
|