Skip to content
File

Blob: src/node/internal/internal_fs.ts

typescript193 lines
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 
5import {
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';
14import {
15 type FilePath,
16 type ValidEncoding,
17} from 'node-internal:internal_fs_utils';
18import {
19 default as cffs,
20 type DirEntryHandle,
21} from 'cloudflare-internal:filesystem';
22import {
23 validateFunction,
24 validateObject,
25 validateString,
26} from 'node-internal:validators';
27import {
28 ERR_DIR_CLOSED,
29 ERR_MISSING_ARGS,
30} from 'node-internal:internal_errors';
31import { Buffer } from 'node-internal:internal_buffer';
32const kType = Symbol('type');
33 
34export 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 
74export interface DirOptions {
75 encoding?: ValidEncoding | undefined;
76}
77 
78export type DirentReadCallback = (
79 err: Error | null,
80 dirent: Dirent | null
81) => void;
82 
83export 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}