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 pathModule.join(path, result[i]), 156 result[i], 157 result[i + 1], 158 ), 159 ); 160 } 161 } 162 163 readSyncRecursive(dirent) { 164 const ctx = { path: dirent.path }; 165 const handle = dirBinding.opendir( 166 pathModule.toNamespacedPath(dirent.path), 167 this[kDirOptions].encoding, 168 undefined, 169 ctx, 170 ); 171 handleErrorFromBinding(ctx); 172 const result = handle.read( 173 this[kDirOptions].encoding, 174 this[kDirOptions].bufferSize, 175 undefined, 176 ctx, 177 ); 178 179 if (result) { 180 this.processReadResult(dirent.path, result); 181 } 182 183 handle.close(undefined, ctx); 184 handleErrorFromBinding(ctx); 185 } 186 187 readSync() { 188 if (this[kDirClosed] === true) { 189 throw new ERR_DIR_CLOSED(); 190 } 191 192 if (this[kDirOperationQueue] !== null) { 193 throw new ERR_DIR_CONCURRENT_OPERATION(); 194 } 195 196 if (this[kDirBufferedEntries].length > 0) { 197 const dirent = ArrayPrototypeShift(this[kDirBufferedEntries]); 198 if (this[kDirOptions].recursive && dirent.isDirectory()) { 199 this.readSyncRecursive(dirent); 200 } 201 return dirent; 202 } 203 204 const ctx = { path: this[kDirPath] }; 205 const result = this[kDirHandle].read( 206 this[kDirOptions].encoding, 207 this[kDirOptions].bufferSize, 208 undefined, 209 ctx, 210 ); 211 handleErrorFromBinding(ctx); 212 213 if (result === null) { 214 return result; 215 } 216 217 this.processReadResult(this[kDirPath], result); 218 219 const dirent = ArrayPrototypeShift(this[kDirBufferedEntries]); 220 if (this[kDirOptions].recursive && dirent.isDirectory()) { 221 this.readSyncRecursive(dirent); 222 } 223 return dirent; 224 } 225 226 close(callback) { 227 // Promise 228 if (callback === undefined) { 229 if (this[kDirClosed] === true) { 230 return PromiseReject(new ERR_DIR_CLOSED()); 231 } 232 return this[kDirClosePromisified](); 233 } 234 235 // callback 236 validateFunction(callback, 'callback'); 237 238 if (this[kDirClosed] === true) { 239 process.nextTick(callback, new ERR_DIR_CLOSED()); 240 return; 241 } 242 243 if (this[kDirOperationQueue] !== null) { 244 ArrayPrototypePush(this[kDirOperationQueue], () => { 245 this.close(callback); 246 }); 247 return; 248 } 249 250 this[kDirClosed] = true; 251 const req = new FSReqCallback(); 252 req.oncomplete = callback; 253 this[kDirHandle].close(req); 254 } 255 256 closeSync() { 257 if (this[kDirClosed] === true) { 258 throw new ERR_DIR_CLOSED(); 259 } 260 261 if (this[kDirOperationQueue] !== null) { 262 throw new ERR_DIR_CONCURRENT_OPERATION(); 263 } 264 265 this[kDirClosed] = true; 266 const ctx = { path: this[kDirPath] }; 267 const result = this[kDirHandle].close(undefined, ctx); 268 handleErrorFromBinding(ctx); 269 return result; 270 } 271 272 async* entries() { 273 try { 274 while (true) { 275 const result = await this[kDirReadPromisified](); 276 if (result === null) { 277 break; 278 } 279 yield result; 280 } 281 } finally { 282 await this[kDirClosePromisified](); 283 } 284 } 285} 286 287ObjectDefineProperty(Dir.prototype, SymbolAsyncIterator, { 288 __proto__: null, 289 value: Dir.prototype.entries, 290 enumerable: false, 291 writable: true, 292 configurable: true, 293}); 294 295function opendir(path, options, callback) { 296 callback = typeof options === 'function' ? options : callback; 297 validateFunction(callback, 'callback'); 298 299 path = getValidatedPath(path); 300 options = getOptions(options, { 301 encoding: 'utf8', 302 }); 303 304 function opendirCallback(error, handle) { 305 if (error) { 306 callback(error); 307 } else { 308 callback(null, new Dir(handle, path, options)); 309 } 310 } 311 312 const req = new FSReqCallback(); 313 req.oncomplete = opendirCallback; 314 315 dirBinding.opendir( 316 pathModule.toNamespacedPath(path), 317 options.encoding, 318 req, 319 ); 320} 321 322function opendirSync(path, options) { 323 path = getValidatedPath(path); 324 options = getOptions(options, { 325 encoding: 'utf8', 326 }); 327 328 const ctx = { path }; 329 const handle = dirBinding.opendir( 330 pathModule.toNamespacedPath(path), 331 options.encoding, 332 undefined, 333 ctx, 334 ); 335 handleErrorFromBinding(ctx); 336 337 return new Dir(handle, path, options); 338} 339 340module.exports = { 341 Dir, 342 opendir, 343 opendirSync, 344}; 345