1'use strict'; 2 3const { 4 ArrayPrototypePush, 5 ArrayPrototypeShift, 6 FunctionPrototypeBind, 7 ObjectDefineProperty, 8 PromiseReject, 9 Symbol, 10 SymbolAsyncIterator, 11} = primordials; 12 13const pathModule = require('path'); 14const binding = internalBinding('fs'); 15const dirBinding = internalBinding('fs_dir'); 16const { 17 codes: { 18 ERR_DIR_CLOSED, 19 ERR_DIR_CONCURRENT_OPERATION, 20 ERR_MISSING_ARGS, 21 }, 22} = require('internal/errors'); 23 24const { FSReqCallback } = binding; 25const internalUtil = require('internal/util'); 26const { 27 getDirent, 28 getOptions, 29 getValidatedPath, 30 handleErrorFromBinding, 31} = require('internal/fs/utils'); 32const { 33 validateFunction, 34 validateUint32, 35} = require('internal/validators'); 36 37const kDirHandle = Symbol('kDirHandle'); 38const kDirPath = Symbol('kDirPath'); 39const kDirBufferedEntries = Symbol('kDirBufferedEntries'); 40const kDirClosed = Symbol('kDirClosed'); 41const kDirOptions = Symbol('kDirOptions'); 42const kDirReadImpl = Symbol('kDirReadImpl'); 43const kDirReadPromisified = Symbol('kDirReadPromisified'); 44const kDirClosePromisified = Symbol('kDirClosePromisified'); 45const kDirOperationQueue = Symbol('kDirOperationQueue'); 46 47class Dir { 48 constructor(handle, path, options) { 49 if (handle == null) throw new ERR_MISSING_ARGS('handle'); 50 this[kDirHandle] = handle; 51 this[kDirBufferedEntries] = []; 52 this[kDirPath] = path; 53 this[kDirClosed] = false; 54 55 // Either `null` or an Array of pending operations (= functions to be called 56 // once the current operation is done). 57 this[kDirOperationQueue] = null; 58 59 this[kDirOptions] = { 60 bufferSize: 32, 61 ...getOptions(options, { 62 encoding: 'utf8', 63 }), 64 }; 65 66 validateUint32(this[kDirOptions].bufferSize, 'options.bufferSize', true); 67 68 this[kDirReadPromisified] = FunctionPrototypeBind( 69 internalUtil.promisify(this[kDirReadImpl]), this, false); 70 this[kDirClosePromisified] = FunctionPrototypeBind( 71 internalUtil.promisify(this.close), this); 72 } 73 74 get path() { 75 return this[kDirPath]; 76 } 77 78 read(callback) { 79 return this[kDirReadImpl](true, callback); 80 } 81 82 [kDirReadImpl](maybeSync, callback) { 83 if (this[kDirClosed] === true) { 84 throw new ERR_DIR_CLOSED(); 85 } 86 87 if (callback === undefined) { 88 return this[kDirReadPromisified](); 89 } 90 91 validateFunction(callback, 'callback'); 92 93 if (this[kDirOperationQueue] !== null) { 94 ArrayPrototypePush(this[kDirOperationQueue], () => { 95 this[kDirReadImpl](maybeSync, callback); 96 }); 97 return; 98 } 99 100 if (this[kDirBufferedEntries].length > 0) { 101 try { 102 const dirent = ArrayPrototypeShift(this[kDirBufferedEntries]); 103 104 if (this[kDirOptions].recursive && dirent.isDirectory()) { 105 this.readSyncRecursive(dirent); 106 } 107 108 if (maybeSync) 109 process.nextTick(callback, null, dirent); 110 else 111 callback(null, dirent); 112 return; 113 } catch (error) { 114 return callback(error); 115 } 116 } 117 118 const req = new FSReqCallback(); 119 req.oncomplete = (err, result) => { 120 process.nextTick(() => { 121 const queue = this[kDirOperationQueue]; 122 this[kDirOperationQueue] = null; 123 for (const op of queue) op(); 124 }); 125 126 if (err || result === null) { 127 return callback(err, result); 128 } 129 130 try { 131 this.processReadResult(this[kDirPath], result); 132 const dirent = ArrayPrototypeShift(this[kDirBufferedEntries]); 133 if (this[kDirOptions].recursive && dirent.isDirectory()) { 134 this.readSyncRecursive(dirent); 135 } 136 callback(null, dirent); 137 } catch (error) { 138 callback(error); 139 } 140 }; 141 142 this[kDirOperationQueue] = []; 143 this[kDirHandle].read( 144 this[kDirOptions].encoding, 145 this[kDirOptions].bufferSize, 146 req, 147 ); 148 } 149 150 processReadResult(path, result) { 151 for (let i = 0; i < result.length; i += 2) { 152 ArrayPrototypePush( 153 this[kDirBufferedEntries], 154 getDirent( 155 path, 156 result[i], 157 result[i + 1], 158 true, // Quirk to not introduce a breaking change. 159 ), 160 ); 161 } 162 } 163 164 readSyncRecursive(dirent) { 165 const ctx = { path: dirent.path }; 166 const handle = dirBinding.opendir( 167 pathModule.toNamespacedPath(dirent.path), 168 this[kDirOptions].encoding, 169 undefined, 170 ctx, 171 ); 172 handleErrorFromBinding(ctx); 173 const result = handle.read( 174 this[kDirOptions].encoding, 175 this[kDirOptions].bufferSize, 176 undefined, 177 ctx, 178 ); 179 180 if (result) { 181 this.processReadResult(dirent.path, result); 182 } 183 184 handle.close(undefined, ctx); 185 handleErrorFromBinding(ctx); 186 } 187 188 readSync() { 189 if (this[kDirClosed] === true) { 190 throw new ERR_DIR_CLOSED(); 191 } 192 193 if (this[kDirOperationQueue] !== null) { 194 throw new ERR_DIR_CONCURRENT_OPERATION(); 195 } 196 197 if (this[kDirBufferedEntries].length > 0) { 198 const dirent = ArrayPrototypeShift(this[kDirBufferedEntries]); 199 if (this[kDirOptions].recursive && dirent.isDirectory()) { 200 this.readSyncRecursive(dirent); 201 } 202 return dirent; 203 } 204 205 const ctx = { path: this[kDirPath] }; 206 const result = this[kDirHandle].read( 207 this[kDirOptions].encoding, 208 this[kDirOptions].bufferSize, 209 undefined, 210 ctx, 211 ); 212 handleErrorFromBinding(ctx); 213 214 if (result === null) { 215 return result; 216 } 217 218 this.processReadResult(this[kDirPath], result); 219 220 const dirent = ArrayPrototypeShift(this[kDirBufferedEntries]); 221 if (this[kDirOptions].recursive && dirent.isDirectory()) { 222 this.readSyncRecursive(dirent); 223 } 224 return dirent; 225 } 226 227 close(callback) { 228 // Promise 229 if (callback === undefined) { 230 if (this[kDirClosed] === true) { 231 return PromiseReject(new ERR_DIR_CLOSED()); 232 } 233 return this[kDirClosePromisified](); 234 } 235 236 // callback 237 validateFunction(callback, 'callback'); 238 239 if (this[kDirClosed] === true) { 240 process.nextTick(callback, new ERR_DIR_CLOSED()); 241 return; 242 } 243 244 if (this[kDirOperationQueue] !== null) { 245 ArrayPrototypePush(this[kDirOperationQueue], () => { 246 this.close(callback); 247 }); 248 return; 249 } 250 251 this[kDirClosed] = true; 252 const req = new FSReqCallback(); 253 req.oncomplete = callback; 254 this[kDirHandle].close(req); 255 } 256 257 closeSync() { 258 if (this[kDirClosed] === true) { 259 throw new ERR_DIR_CLOSED(); 260 } 261 262 if (this[kDirOperationQueue] !== null) { 263 throw new ERR_DIR_CONCURRENT_OPERATION(); 264 } 265 266 this[kDirClosed] = true; 267 const ctx = { path: this[kDirPath] }; 268 const result = this[kDirHandle].close(undefined, ctx); 269 handleErrorFromBinding(ctx); 270 return result; 271 } 272 273 async* entries() { 274 try { 275 while (true) { 276 const result = await this[kDirReadPromisified](); 277 if (result === null) { 278 break; 279 } 280 yield result; 281 } 282 } finally { 283 await this[kDirClosePromisified](); 284 } 285 } 286} 287 288ObjectDefineProperty(Dir.prototype, SymbolAsyncIterator, { 289 __proto__: null, 290 value: Dir.prototype.entries, 291 enumerable: false, 292 writable: true, 293 configurable: true, 294}); 295 296function opendir(path, options, callback) { 297 callback = typeof options === 'function' ? options : callback; 298 validateFunction(callback, 'callback'); 299 300 path = getValidatedPath(path); 301 options = getOptions(options, { 302 encoding: 'utf8', 303 }); 304 305 function opendirCallback(error, handle) { 306 if (error) { 307 callback(error); 308 } else { 309 callback(null, new Dir(handle, path, options)); 310 } 311 } 312 313 const req = new FSReqCallback(); 314 req.oncomplete = opendirCallback; 315 316 dirBinding.opendir( 317 pathModule.toNamespacedPath(path), 318 options.encoding, 319 req, 320 ); 321} 322 323function opendirSync(path, options) { 324 path = getValidatedPath(path); 325 options = getOptions(options, { 326 encoding: 'utf8', 327 }); 328 329 const ctx = { path }; 330 const handle = dirBinding.opendir( 331 pathModule.toNamespacedPath(path), 332 options.encoding, 333 undefined, 334 ctx, 335 ); 336 handleErrorFromBinding(ctx); 337 338 return new Dir(handle, path, options); 339} 340 341module.exports = { 342 Dir, 343 opendir, 344 opendirSync, 345}; 346