File
Blob: src/node/internal/internal_fs.ts
| 1 | // Copyright (c) 2026 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 | import { |
| 6 | UV_DIRENT_DIR, |
| 7 | UV_DIRENT_FILE, |
| 8 | UV_DIRENT_BLOCK, |
| 9 | UV_DIRENT_CHAR, |
| 10 | UV_DIRENT_LINK, |
| 11 | UV_DIRENT_FIFO, |
| 12 | UV_DIRENT_SOCKET, |
| 13 | } from 'node-internal:internal_fs_constants'; |
| 14 | import { |
| 15 | type FilePath, |
| 16 | type ValidEncoding, |
| 17 | } from 'node-internal:internal_fs_utils'; |
| 18 | import { |
| 19 | default as cffs, |
| 20 | type DirEntryHandle, |
| 21 | } from 'cloudflare-internal:filesystem'; |
| 22 | import { |
| 23 | validateFunction, |
| 24 | validateObject, |
| 25 | validateString, |
| 26 | } from 'node-internal:validators'; |
| 27 | import { |
| 28 | ERR_DIR_CLOSED, |
| 29 | ERR_MISSING_ARGS, |
| 30 | } from 'node-internal:internal_errors'; |
| 31 | import { Buffer } from 'node-internal:internal_buffer'; |
| 32 | const kType = Symbol('type'); |
| 33 | |
| 34 | export class Dirent { |
| 35 | name: string | Buffer; |
| 36 | parentPath: string | Buffer; |
| 37 | private [kType]: number; |
| 38 | |
| 39 | constructor(name: string | Buffer, type: number, path: string | Buffer) { |
| 40 | this.name = name; |
| 41 | this.parentPath = path; |
| 42 | this[kType] = type; |
| 43 | } |
| 44 | |
| 45 | isDirectory(): boolean { |
| 46 | return this[kType] === UV_DIRENT_DIR; |
| 47 | } |
| 48 | |
| 49 | isFile(): boolean { |
| 50 | return this[kType] === UV_DIRENT_FILE; |
| 51 | } |
| 52 | |
| 53 | isBlockDevice(): boolean { |
| 54 | return this[kType] === UV_DIRENT_BLOCK; |
| 55 | } |
| 56 | |
| 57 | isCharacterDevice(): boolean { |
| 58 | return this[kType] === UV_DIRENT_CHAR; |
| 59 | } |
| 60 | |
| 61 | isSymbolicLink(): boolean { |
| 62 | return this[kType] === UV_DIRENT_LINK; |
| 63 | } |
| 64 | |
| 65 | isFIFO(): boolean { |
| 66 | return this[kType] === UV_DIRENT_FIFO; |
| 67 | } |
| 68 | |
| 69 | isSocket(): boolean { |
| 70 | return this[kType] === UV_DIRENT_SOCKET; |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | export interface DirOptions { |
| 75 | encoding?: ValidEncoding | undefined; |
| 76 | } |
| 77 | |
| 78 | export type DirentReadCallback = ( |
| 79 | err: Error | null, |
| 80 | dirent: Dirent | null |
| 81 | ) => void; |
| 82 | |
| 83 | export class Dir { |
| 84 | // Unlike our Node.js counterpart, we do not not use an underlying |
| 85 | // native handle for the directory. Instead we use a simple array |
| 86 | // of DirEntryHandle objects just like the one used in the readdir |
| 87 | // API. |
| 88 | #handle: DirEntryHandle[] | undefined; |
| 89 | #encoding: ValidEncoding | undefined; |
| 90 | #path: FilePath; |
| 91 | |
| 92 | constructor( |
| 93 | handle: cffs.DirEntryHandle[] | undefined, |
| 94 | path: FilePath, |
| 95 | options: DirOptions |
| 96 | ) { |
| 97 | if (handle == null) { |
| 98 | throw new ERR_MISSING_ARGS('handle'); |
| 99 | } |
| 100 | this.#handle = handle; |
| 101 | this.#path = path; |
| 102 | |
| 103 | validateObject(options, 'options'); |
| 104 | const { encoding = 'utf8' } = options; |
| 105 | validateString(encoding, 'options.encoding'); |
| 106 | this.#encoding = encoding as BufferEncoding; |
| 107 | } |
| 108 | |
| 109 | get path(): FilePath { |
| 110 | return this.#path; |
| 111 | } |
| 112 | |
| 113 | read(): Promise<Dirent | null>; |
| 114 | read(callback: DirentReadCallback): void; |
| 115 | read(callback?: DirentReadCallback): Promise<Dirent | null> | undefined { |
| 116 | if (typeof callback === 'function') { |
| 117 | validateFunction(callback, 'callback'); |
| 118 | try { |
| 119 | const ent = this.readSync(); |
| 120 | queueMicrotask(() => { |
| 121 | callback(null, ent); |
| 122 | }); |
| 123 | } catch (err) { |
| 124 | queueMicrotask(() => { |
| 125 | callback(err as Error, null); |
| 126 | }); |
| 127 | } |
| 128 | return; |
| 129 | } |
| 130 | |
| 131 | try { |
| 132 | const ent = this.readSync(); |
| 133 | return Promise.resolve(ent); |
| 134 | } catch (err: unknown) { |
| 135 | return Promise.reject(err as Error); |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | readSync(): Dirent | null { |
| 140 | if (this.#handle === undefined) throw new ERR_DIR_CLOSED(); |
| 141 | const ent = this.#handle.shift(); |
| 142 | if (ent == null) return null; |
| 143 | const buf = Buffer.from(ent.name); |
| 144 | if (this.#encoding === 'buffer') { |
| 145 | return new Dirent(buf, ent.type, ent.parentPath); |
| 146 | } |
| 147 | return new Dirent( |
| 148 | buf.toString(this.#encoding != null ? this.#encoding : undefined), |
| 149 | ent.type, |
| 150 | ent.parentPath |
| 151 | ); |
| 152 | } |
| 153 | |
| 154 | close(callback?: (err: unknown) => void): Promise<void> | undefined { |
| 155 | this.closeSync(); |
| 156 | if (typeof callback === 'function') { |
| 157 | validateFunction(callback, 'callback'); |
| 158 | queueMicrotask(() => { |
| 159 | callback(null); |
| 160 | }); |
| 161 | return undefined; |
| 162 | } |
| 163 | return Promise.resolve(); |
| 164 | } |
| 165 | |
| 166 | closeSync(): void { |
| 167 | if (this.#handle === undefined) { |
| 168 | throw new ERR_DIR_CLOSED(); |
| 169 | } |
| 170 | this.#handle = undefined; |
| 171 | } |
| 172 | |
| 173 | async *entries(): AsyncGenerator<Dirent, unknown, unknown> { |
| 174 | for (;;) { |
| 175 | const ent = await this.read(); |
| 176 | if (ent == null) return; |
| 177 | yield ent; |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | [Symbol.asyncIterator](): AsyncGenerator<Dirent, unknown, unknown> { |
| 182 | return this.entries(); |
| 183 | } |
| 184 | |
| 185 | async [Symbol.asyncDispose](): Promise<void> { |
| 186 | await this.close(); |
| 187 | } |
| 188 | |
| 189 | [Symbol.dispose](): void { |
| 190 | this.closeSync(); |
| 191 | } |
| 192 | } |