xref: /third_party/node/lib/internal/fs/dir.js (revision 1cb0ef41)
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