Skip to content
File

Blob: src/node/internal/internal_fs_promises.ts

typescript724 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4//
5// Copyright Joyent, Inc. and other Node contributors.
6//
7// Permission is hereby granted, free of charge, to any person obtaining a
8// copy of this software and associated documentation files (the
9// "Software"), to deal in the Software without restriction, including
10// without limitation the rights to use, copy, modify, merge, publish,
11// distribute, sublicense, and/or sell copies of the Software, and to permit
12// persons to whom the Software is furnished to do so, subject to the
13// following conditions:
14//
15// The above copyright notice and this permission notice shall be included
16// in all copies or substantial portions of the Software.
17//
18// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
21// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
22// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
23// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
24// USE OR OTHER DEALINGS IN THE SOFTWARE.
25 
26import * as fssync from 'node-internal:internal_fs_sync';
27import { EventEmitter } from 'node-internal:events';
28import { default as cffs } from 'cloudflare-internal:filesystem';
29import type {
30 MkdirTempSyncOptions,
31 ReadDirResult,
32 ReadFileSyncOptions,
33 ReadLinkSyncOptions,
34 StatOptions,
35} from 'node-internal:internal_fs_sync';
36import {
37 type ReadStream,
38 type ReadStreamOptions,
39 type WriteStream,
40 type WriteStreamOptions,
41 createReadStream,
42 createWriteStream,
43} from 'node-internal:internal_fs_streams';
44import {
45 kBadge,
46 kFileHandle,
47 Stats,
48 validatePosition,
49 type Position,
50 type RawTime,
51 type SymlinkType,
52 type FilePath,
53 type ReadDirOptions,
54 type WriteSyncOptions,
55 type ValidEncoding,
56} from 'node-internal:internal_fs_utils';
57import type { Dirent } from 'node-internal:internal_fs';
58import { Buffer } from 'node-internal:internal_buffer';
59import { type Dir } from 'node-internal:internal_fs';
60import { ERR_EBADF } from 'node-internal:internal_errors';
61import {
62 validateBoolean,
63 validateObject,
64 validateUint32,
65} from 'node-internal:validators';
66import * as constants from 'node-internal:internal_fs_constants';
67export { constants };
68import type {
69 BigIntStatsFs,
70 CopySyncOptions,
71 GlobOptions,
72 GlobOptionsWithFileTypes,
73 GlobOptionsWithoutFileTypes,
74 MakeDirectoryOptions,
75 OpenDirOptions,
76 ReadOptionsWithBuffer,
77 RmOptions,
78 StatsFs,
79 WriteFileOptions,
80} from 'node:fs';
81import type { RmDirOptions } from 'node-internal:internal_fs_utils';
82import type {
83 ReadableWebStreamOptions,
84 CreateReadStreamOptions,
85} from 'node:fs/promises';
86import { isArrayBufferView } from 'node-internal:internal_types';
87 
88export class FileHandle extends EventEmitter {
89 // The FileHandle class is a wrapper around a file descriptor.
90 // When the #handle is cleared, the reference to the underlying
91 // file descriptor is dropped. The user is expected to call
92 // close() explicitly but if they do not, the file descriptor
93 // will still be closed when the underlying handle is garbage
94 // collected.
95 #fd: number | undefined;
96 #handle: cffs.FdHandle | undefined;
97 [kFileHandle] = true;
98 
99 constructor(badge: symbol, fd: number) {
100 if (badge !== kBadge) {
101 throw new TypeError('Illegal constructor');
102 }
103 super();
104 this.#fd = fd;
105 this.#handle = cffs.getFdHandle(fd);
106 }
107 
108 get fd(): number | undefined {
109 // The fd property will be undefined if the handle has been closed.
110 return this.#fd;
111 }
112 
113 async appendFile(
114 data: string | ArrayBufferView,
115 options: WriteFileOptions = {}
116 ): Promise<void> {
117 if (this.#fd === undefined) {
118 throw new ERR_EBADF({ syscall: 'stat' });
119 }
120 await appendFile(this.#fd, data, options);
121 }
122 
123 async chmod(mode: string | number): Promise<void> {
124 if (this.#fd === undefined) {
125 throw new ERR_EBADF({ syscall: 'stat' });
126 }
127 await fchmod(this.#fd, mode);
128 }
129 
130 async chown(uid: number, gid: number): Promise<void> {
131 if (this.#fd === undefined) {
132 throw new ERR_EBADF({ syscall: 'stat' });
133 }
134 await fchown(this.#fd, uid, gid);
135 }
136 
137 async datasync(): Promise<void> {
138 if (this.#fd === undefined) {
139 throw new ERR_EBADF({ syscall: 'stat' });
140 }
141 await fdatasync(this.#fd);
142 }
143 
144 async sync(): Promise<void> {
145 if (this.#fd === undefined) {
146 throw new ERR_EBADF({ syscall: 'stat' });
147 }
148 await fsync(this.#fd);
149 }
150 
151 read<T extends NodeJS.ArrayBufferView>(
152 bufferOrOptions: T | ReadOptionsWithBuffer<T> = {},
153 offsetOrOptions: number | ReadOptionsWithBuffer<T> = {},
154 length?: number,
155 position: Position = null
156 ): Promise<{ bytesRead: number; buffer: T }> {
157 try {
158 if (this.#fd === undefined) {
159 throw new ERR_EBADF({ syscall: 'stat' });
160 }
161 
162 let options: ReadOptionsWithBuffer<T>;
163 if (isArrayBufferView(bufferOrOptions)) {
164 if (typeof offsetOrOptions === 'number') {
165 options = {
166 buffer: bufferOrOptions,
167 offset: offsetOrOptions,
168 length: length ?? bufferOrOptions.byteLength,
169 position: position ?? null,
170 };
171 } else {
172 options = {
173 buffer: bufferOrOptions,
174 ...offsetOrOptions,
175 };
176 }
177 } else {
178 options = bufferOrOptions;
179 }
180 
181 const {
182 buffer = Buffer.alloc(16384),
183 offset: actualOffset = buffer.byteOffset,
184 length: actualLength = buffer.byteLength - buffer.byteOffset,
185 position: actualPosition = null,
186 } = options;
187 
188 validateUint32(actualOffset, 'offset');
189 validateUint32(actualLength, 'length');
190 validatePosition(actualPosition, 'position');
191 
192 const bytesRead = fssync.readSync(
193 this.#fd,
194 buffer as NodeJS.ArrayBufferView,
195 actualOffset,
196 actualLength,
197 actualPosition
198 );
199 
200 return Promise.resolve({
201 bytesRead,
202 buffer: buffer as T,
203 });
204 } catch (err) {
205 return Promise.reject(err as Error);
206 }
207 }
208 
209 readv<T extends NodeJS.ArrayBufferView>(
210 buffers: T[],
211 position: Position = null
212 ): Promise<{ bytesRead: number; buffers: T[] }> {
213 try {
214 if (this.#fd === undefined) {
215 throw new ERR_EBADF({ syscall: 'stat' });
216 }
217 const bytesRead = fssync.readvSync(this.#fd, buffers, position);
218 return Promise.resolve({
219 bytesRead,
220 buffers,
221 });
222 } catch (err) {
223 return Promise.reject(err as Error);
224 }
225 }
226 
227 async readFile(
228 options: ValidEncoding | ReadFileSyncOptions = {}
229 ): Promise<string | Buffer> {
230 if (this.#fd === undefined) {
231 throw new ERR_EBADF({ syscall: 'stat' });
232 }
233 return await readFile(this.#fd, options);
234 }
235 
236 readLines(_options: CreateReadStreamOptions = {}): void {
237 // TODO(node-fs): This method is not yet implemented. In Node.js,
238 // it depends on the readline module which has not yet been
239 // implemented in the workers runtime. We'll want to implement
240 // it first then come back and implement this method.
241 if (this.#fd === undefined) {
242 throw new ERR_EBADF({ syscall: 'stat' });
243 }
244 throw new Error('not implemented');
245 }
246 
247 async stat(options: StatOptions = {}): Promise<Stats | undefined> {
248 if (this.#fd === undefined) {
249 throw new ERR_EBADF({ syscall: 'stat' });
250 }
251 return await fstat(this.#fd, options);
252 }
253 
254 async truncate(len: number = 0): Promise<void> {
255 if (this.#fd === undefined) {
256 throw new ERR_EBADF({ syscall: 'stat' });
257 }
258 await ftruncate(this.#fd, len);
259 }
260 
261 async utimes(atime: RawTime | Date, mtime: RawTime | Date): Promise<void> {
262 if (this.#fd === undefined) {
263 throw new ERR_EBADF({ syscall: 'stat' });
264 }
265 await futimes(this.#fd, atime, mtime);
266 }
267 
268 write(
269 buffer: NodeJS.ArrayBufferView | string,
270 offsetPositionOrOptions: WriteSyncOptions | Position = null,
271 lengthOrEncoding?: number | ValidEncoding,
272 position: Position = null
273 ): Promise<{ bytesWritten: number; buffer: NodeJS.ArrayBufferView }> {
274 try {
275 if (this.#fd === undefined) {
276 throw new ERR_EBADF({ syscall: 'stat' });
277 }
278 
279 if (typeof buffer === 'string') {
280 buffer = Buffer.from(buffer, lengthOrEncoding as string);
281 }
282 
283 const bytesWritten = fssync.writeSync(
284 this.#fd,
285 buffer,
286 offsetPositionOrOptions,
287 lengthOrEncoding,
288 position
289 );
290 
291 return Promise.resolve({
292 bytesWritten,
293 buffer,
294 });
295 } catch (err) {
296 return Promise.reject(err as Error);
297 }
298 }
299 
300 writev(
301 buffers: NodeJS.ArrayBufferView[],
302 position: Position = null
303 ): Promise<{ bytesWritten: number; buffers: NodeJS.ArrayBufferView[] }> {
304 try {
305 if (this.#fd === undefined) {
306 throw new ERR_EBADF({ syscall: 'stat' });
307 }
308 const bytesWritten = fssync.writevSync(this.#fd, buffers, position);
309 return Promise.resolve({
310 bytesWritten,
311 buffers,
312 });
313 } catch (err) {
314 return Promise.reject(err as Error);
315 }
316 }
317 
318 writeFile(
319 data: string | Buffer,
320 options: ValidEncoding | WriteFileOptions = {}
321 ): Promise<{ bytesWritten: number; buffer: Buffer }> {
322 try {
323 if (this.#fd === undefined) {
324 throw new ERR_EBADF({ syscall: 'stat' });
325 }
326 const bytesWritten = fssync.writeFileSync(this.#fd, data, options);
327 return Promise.resolve({
328 bytesWritten,
329 buffer: isArrayBufferView(data)
330 ? data
331 : Buffer.from(data, options as BufferEncoding),
332 });
333 } catch (err) {
334 return Promise.reject(err as Error);
335 }
336 }
337 
338 close(): Promise<void> {
339 try {
340 this.#handle?.close();
341 this.#fd = undefined;
342 this.#handle = undefined;
343 (this as unknown as EventEmitter).emit('close');
344 return Promise.resolve();
345 } catch (err) {
346 return Promise.reject(err as Error);
347 }
348 }
349 
350 async [Symbol.asyncDispose](): Promise<void> {
351 await this.close();
352 }
353 
354 readableWebStream(
355 options: ReadableWebStreamOptions = {}
356 ): ReadableStream<Uint8Array> {
357 if (this.#fd === undefined) {
358 throw new ERR_EBADF({ syscall: 'stat' });
359 }
360 validateObject(options, 'options');
361 // Node.js actually defaults autoClose to false here because of backwards
362 // compatibility issues but will change to autoClose = true in a semver-major
363 // soon.
364 const { autoClose = true } = options;
365 validateBoolean(autoClose, 'options.autoClose');
366 return getReadableWebStream(this, { autoClose });
367 }
368 
369 createReadStream(options: ReadStreamOptions = {}): ReadStream {
370 return createReadStream('', { ...options, fd: this });
371 }
372 
373 createWriteStream(_options: WriteStreamOptions = {}): WriteStream {
374 return createWriteStream('', { ..._options, fd: this });
375 }
376}
377 
378export function access(
379 path: FilePath,
380 mode: number = constants.F_OK
381): Promise<void> {
382 // Unlike the callback version, which throws input validation errors synchronously,
383 // rather than forwarding them to the callback, the promise version throws a errors
384 // asynchronously by rejecting the promise. So we can rely on accessSync to do the
385 // input validation for us.
386 return Promise.try(() => {
387 fssync.accessSync(path, mode);
388 });
389}
390 
391export function appendFile(
392 path: number | FilePath,
393 data: string | ArrayBufferView,
394 options: WriteFileOptions = {}
395): Promise<void> {
396 return Promise.try(() => {
397 // While appendFileSync returns the number of bytes written,
398 // the promise version does not return anything when successful.
399 fssync.appendFileSync(path, data, options);
400 });
401}
402 
403export function chmod(path: FilePath, mode: number): Promise<void> {
404 return Promise.try(() => {
405 fssync.chmodSync(path, mode);
406 });
407}
408 
409export function chown(path: FilePath, uid: number, gid: number): Promise<void> {
410 return Promise.try(() => {
411 fssync.chownSync(path, uid, gid);
412 });
413}
414 
415export function copyFile(
416 src: FilePath,
417 dest: FilePath,
418 mode: number
419): Promise<void> {
420 return Promise.try(() => {
421 fssync.copyFileSync(src, dest, mode);
422 });
423}
424 
425export function cp(
426 src: FilePath,
427 dest: FilePath,
428 options: CopySyncOptions
429): Promise<void> {
430 return Promise.try(() => {
431 fssync.cpSync(src, dest, options);
432 });
433}
434 
435function fchmod(fd: number, mode: string | number): Promise<void> {
436 return Promise.try(() => {
437 fssync.fchmodSync(fd, mode);
438 });
439}
440 
441function fchown(fd: number, uid: number, gid: number): Promise<void> {
442 return Promise.try(() => {
443 fssync.fchownSync(fd, uid, gid);
444 });
445}
446 
447function fdatasync(fd: number): Promise<void> {
448 return Promise.try(() => {
449 fssync.fdatasyncSync(fd);
450 });
451}
452 
453function fsync(fd: number): Promise<void> {
454 return Promise.try(() => {
455 fssync.fsyncSync(fd);
456 });
457}
458 
459function fstat(
460 fd: number,
461 options: StatOptions = {}
462): Promise<Stats | undefined> {
463 return Promise.try(() => {
464 return fssync.fstatSync(fd, options);
465 });
466}
467 
468function ftruncate(fd: number, len: number = 0): Promise<void> {
469 return Promise.try(() => {
470 fssync.ftruncateSync(fd, len);
471 });
472}
473 
474function futimes(
475 fd: number,
476 atime: RawTime | Date,
477 mtime: RawTime | Date
478): Promise<void> {
479 return Promise.try(() => {
480 fssync.futimesSync(fd, atime, mtime);
481 });
482}
483 
484export function lchmod(path: FilePath, mode: number): Promise<void> {
485 return Promise.try(() => {
486 fssync.lchmodSync(path, mode);
487 });
488}
489 
490export function lchown(
491 path: FilePath,
492 uid: number,
493 gid: number
494): Promise<void> {
495 return Promise.try(() => {
496 fssync.lchownSync(path, uid, gid);
497 });
498}
499 
500export function lutimes(
501 path: FilePath,
502 atime: RawTime | Date,
503 mtime: RawTime | Date
504): Promise<void> {
505 return Promise.try(() => {
506 fssync.lutimesSync(path, atime, mtime);
507 });
508}
509 
510export function link(existingPath: FilePath, newPath: FilePath): Promise<void> {
511 return Promise.try(() => {
512 fssync.linkSync(existingPath, newPath);
513 });
514}
515 
516export function lstat(
517 path: FilePath,
518 options: StatOptions = {}
519): Promise<Stats | undefined> {
520 return Promise.try(() => {
521 return fssync.lstatSync(path, options);
522 });
523}
524 
525export function mkdir(
526 path: FilePath,
527 options: number | MakeDirectoryOptions = {}
528): Promise<string | undefined> {
529 return Promise.try(() => fssync.mkdirSync(path, options));
530}
531 
532export function mkdtemp(
533 prefix: FilePath,
534 options: MkdirTempSyncOptions = {}
535): Promise<string> {
536 return Promise.try(() => fssync.mkdtempSync(prefix, options));
537}
538 
539export function open(
540 path: FilePath,
541 flags: number | string,
542 mode: number | string
543): Promise<FileHandle> {
544 return Promise.try(
545 () => new FileHandle(kBadge, fssync.openSync(path, flags, mode))
546 );
547}
548 
549export function opendir(
550 path: FilePath,
551 options: OpenDirOptions = {}
552): Promise<Dir> {
553 return Promise.try(() => fssync.opendirSync(path, options));
554}
555 
556export function readdir(
557 path: FilePath,
558 options: ReadDirOptions = {}
559): Promise<ReadDirResult> {
560 return Promise.try(() => fssync.readdirSync(path, options));
561}
562 
563export function readFile(
564 path: number | FilePath,
565 options: ValidEncoding | ReadFileSyncOptions = {}
566): Promise<string | Buffer> {
567 return Promise.try(() => fssync.readFileSync(path, options));
568}
569 
570export function readlink(
571 path: FilePath,
572 options: ValidEncoding | ReadLinkSyncOptions = {}
573): Promise<string | Buffer> {
574 return Promise.try(() => fssync.readlinkSync(path, options));
575}
576 
577export function realpath(
578 path: FilePath,
579 options: ValidEncoding | ReadLinkSyncOptions = {}
580): Promise<string | Buffer> {
581 return Promise.try(() => fssync.realpathSync(path, options));
582}
583 
584export function rename(oldPath: FilePath, newPath: FilePath): Promise<void> {
585 return Promise.try(() => {
586 fssync.renameSync(oldPath, newPath);
587 });
588}
589 
590export function rmdir(path: FilePath, options: RmDirOptions): Promise<void> {
591 return Promise.try(() => {
592 fssync.rmdirSync(path, options);
593 });
594}
595 
596export function rm(path: FilePath, options: RmOptions = {}): Promise<void> {
597 return Promise.try(() => {
598 fssync.rmSync(path, options);
599 });
600}
601 
602export function stat(
603 path: FilePath,
604 options: StatOptions = {}
605): Promise<Stats | undefined> {
606 return Promise.try(() => fssync.statSync(path, options));
607}
608 
609export function statfs(
610 path: FilePath,
611 options: { bigint?: boolean | undefined } = {}
612): Promise<StatsFs | BigIntStatsFs> {
613 return Promise.try(() => fssync.statfsSync(path, options));
614}
615 
616export function symlink(
617 target: FilePath,
618 path: FilePath,
619 type: SymlinkType = null
620): Promise<void> {
621 return Promise.try(() => {
622 fssync.symlinkSync(target, path, type);
623 });
624}
625 
626export function truncate(path: FilePath, len: number = 0): Promise<void> {
627 return Promise.try(() => {
628 fssync.truncateSync(path, len);
629 });
630}
631 
632export function unlink(path: FilePath): Promise<void> {
633 return Promise.try(() => {
634 fssync.unlinkSync(path);
635 });
636}
637 
638export function utimes(
639 path: FilePath,
640 atime: RawTime | Date,
641 mtime: RawTime | Date
642): Promise<void> {
643 return Promise.try(() => {
644 fssync.utimesSync(path, atime, mtime);
645 });
646}
647 
648export function watch(): Promise<void> {
649 // We do not implement the watch function.
650 throw new Error('Not implemented');
651}
652 
653export function writeFile(
654 path: number | FilePath,
655 data: string | ArrayBufferView,
656 options: ValidEncoding | WriteFileOptions = {}
657): Promise<void> {
658 return Promise.try(() => {
659 // While writeFileSync returns the number of bytes written,
660 // the promise version does not return anything when successful.
661 fssync.writeFileSync(path, data, options);
662 });
663}
664 
665export async function* glob(
666 pattern: string | readonly string[],
667 options:
668 | GlobOptions
669 | GlobOptionsWithFileTypes
670 | GlobOptionsWithoutFileTypes = {}
671): AsyncGenerator<string | Dirent> {
672 const results = await Promise.resolve(fssync.globSync(pattern, options));
673 for (const result of results) {
674 yield result;
675 }
676}
677 
678function getReadableWebStream(
679 fh: FileHandle,
680 options: ReadableWebStreamOptions
681): ReadableStream<Uint8Array> {
682 const readFn = fh.read.bind(fh);
683 const { autoClose } = options;
684 let controller: ReadableByteStreamController;
685 const ondone = async (): Promise<void> => {
686 if (autoClose) await fh.close();
687 };
688 
689 const readable = new ReadableStream({
690 type: 'bytes',
691 autoAllocateChunkSize: 16384,
692 start(c: ReadableByteStreamController): void {
693 controller = c;
694 },
695 
696 async pull(controller: ReadableByteStreamController): Promise<void> {
697 const req = controller.byobRequest as ReadableStreamBYOBRequest;
698 const view = req.view;
699 const { bytesRead } = await readFn(
700 view as Uint8Array,
701 (view as Uint8Array).byteOffset,
702 (view as Uint8Array).byteLength
703 );
704 
705 if (bytesRead === 0) {
706 controller.close();
707 await ondone();
708 }
709 
710 req.respond(bytesRead);
711 },
712 
713 async cancel(): Promise<void> {
714 await ondone();
715 },
716 });
717 
718 fh.once('close', () => {
719 controller.close();
720 });
721 
722 return readable;
723}