Skip to content

Commit 018fc76

Browse files
committed
fs: introduce opendir() and fs.Dir
This adds long-requested methods for asynchronously interacting and iterating through directory entries by using `uv_fs_opendir`, `uv_fs_readdir`, and `uv_fs_closedir`. `fs.opendir()` and friends return an `fs.Dir`, which contains methods for doing reads and cleanup. `fs.Dir` also has the async iterator symbol exposed. The `read()` method and friends only return `fs.Dirent`s for this API. Having a entry type or doing a `stat` call is deemed to be necessary in the majority of cases, so just returning dirents seems like the logical choice for a new api. Reading when there are no more entries returns `null` instead of a dirent. However the async iterator hides that (and does automatic cleanup). The code lives in separate files from the rest of fs, this is done partially to prevent over-pollution of those (already very large) files, but also in the case of js allows loading into `fsPromises`. Due to async_hooks, this introduces a new handle type of `DIRHANDLE`. This PR does not attempt to make complete optimization of this feature. Notable future improvements include: - Moving promise work into C++ land like FileHandle. - Possibly adding `readv()` to do multi-entry directory reads. - Aliasing `fs.readdir` to `fs.scandir` and doing a deprecation. Refs: nodejs/node-v0.x-archive#388 Refs: #583 Refs: libuv/libuv#2057
1 parent 95266db commit 018fc76

File tree

19 files changed

+1165
-119
lines changed

19 files changed

+1165
-119
lines changed

doc/api/errors.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,11 @@ A signing `key` was not provided to the [`sign.sign()`][] method.
826826
[`crypto.timingSafeEqual()`][] was called with `Buffer`, `TypedArray`, or
827827
`DataView` arguments of different lengths.
828828

829+
<a id="ERR_DIR_CLOSED"></a>
830+
### ERR_DIR_CLOSED
831+
832+
The [`fs.Dir`][] was previously closed.
833+
829834
<a id="ERR_DNS_SET_SERVERS_FAILED"></a>
830835
### ERR_DNS_SET_SERVERS_FAILED
831836

@@ -2388,6 +2393,7 @@ such as `process.stdout.on('data')`.
23882393
[`dgram.disconnect()`]: dgram.html#dgram_socket_disconnect
23892394
[`dgram.remoteAddress()`]: dgram.html#dgram_socket_remoteaddress
23902395
[`errno`(3) man page]: http://man7.org/linux/man-pages/man3/errno.3.html
2396+
[`fs.Dir`]: fs.html#fs_class_fs_dir
23912397
[`fs.readFileSync`]: fs.html#fs_fs_readfilesync_path_options
23922398
[`fs.readdir`]: fs.html#fs_fs_readdir_path_options_callback
23932399
[`fs.symlink()`]: fs.html#fs_fs_symlink_target_path_type_callback

doc/api/fs.md

Lines changed: 214 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,13 +284,148 @@ synchronous use libuv's threadpool, which can have surprising and negative
284284
performance implications for some applications. See the
285285
[`UV_THREADPOOL_SIZE`][] documentation for more information.
286286

287+
## Class fs.Dir
288+
<!-- YAML
289+
added: REPLACEME
290+
-->
291+
292+
A class representing a directory stream.
293+
294+
Created by [`fs.opendir()`][], [`fs.opendirSync()`][], or [`fsPromises.opendir()`][].
295+
296+
Example using async interation:
297+
298+
```js
299+
const fs = require('fs');
300+
301+
async function print(path) {
302+
const dir = await fs.promises.opendir(path);
303+
for await (const dirent of dir) {
304+
console.log(dirent.name);
305+
}
306+
}
307+
print('./').catch(console.error);
308+
```
309+
310+
### dir.path
311+
<!-- YAML
312+
added: REPLACEME
313+
-->
314+
315+
* {string}
316+
317+
The read-only path of this directory as was provided to [`fs.opendir()`][],
318+
[`fs.opendirSync()`][], or [`fsPromises.opendir()`][].
319+
320+
### dir.close()
321+
<!-- YAML
322+
added: REPLACEME
323+
-->
324+
325+
* Returns: {Promise}
326+
327+
Asynchronously close the directory's underlying resource handle.
328+
Subsequent reads will result in errors.
329+
330+
A `Promise` is returned that will be resolved after the resource has been
331+
closed.
332+
333+
### dir.close(callback)
334+
<!-- YAML
335+
added: REPLACEME
336+
-->
337+
338+
* `callback` {Function}
339+
* `err` {Error}
340+
341+
Asynchronously close the directory's underlying resource handle.
342+
Subsequent reads will result in errors.
343+
344+
The `callback` will be called after the resource handle has been closed.
345+
346+
### dir.closeSync()
347+
<!-- YAML
348+
added: REPLACEME
349+
-->
350+
351+
Synchronously close the directory's underlying resource handle.
352+
Subsequent reads will result in errors.
353+
354+
### dir.read([options])
355+
<!-- YAML
356+
added: REPLACEME
357+
-->
358+
359+
* `options` {Object}
360+
* `encoding` {string|null} **Default:** `'utf8'`
361+
* Returns: {Promise} containing {fs.Dirent}
362+
363+
Asynchronously read the next directory entry via readdir(3) as an
364+
[`fs.Dirent`][].
365+
366+
A `Promise` is returned that will be resolved with a [Dirent][] after the read
367+
is completed.
368+
369+
_Directory entries returned by this function are in no particular order as
370+
provided by the operating system's underlying directory mechanisms._
371+
372+
### dir.read([options, ]callback)
373+
<!-- YAML
374+
added: REPLACEME
375+
-->
376+
377+
* `options` {Object}
378+
* `encoding` {string|null} **Default:** `'utf8'`
379+
* `callback` {Function}
380+
* `err` {Error}
381+
* `dirent` {fs.Dirent}
382+
383+
Asynchronously read the next directory entry via readdir(3) as an
384+
[`fs.Dirent`][].
385+
386+
The `callback` will be called with a [Dirent][] after the read is completed.
387+
388+
The `encoding` option sets the encoding of the `name` in the `dirent`.
389+
390+
_Directory entries returned by this function are in no particular order as
391+
provided by the operating system's underlying directory mechanisms._
392+
393+
### dir.readSync([options])
394+
<!-- YAML
395+
added: REPLACEME
396+
-->
397+
398+
* `options` {Object}
399+
* `encoding` {string|null} **Default:** `'utf8'`
400+
* Returns: {fs.Dirent}
401+
402+
Synchronously read the next directory entry via readdir(3) as an
403+
[`fs.Dirent`][].
404+
405+
The `encoding` option sets the encoding of the `name` in the `dirent`.
406+
407+
_Directory entries returned by this function are in no particular order as
408+
provided by the operating system's underlying directory mechanisms._
409+
410+
### dir\[Symbol.asyncIterator\]()
411+
<!-- YAML
412+
added: REPLACEME
413+
-->
414+
415+
* Returns: {AsyncIterator} to fully iterate over all entries in the directory.
416+
417+
_Directory entries returned by this iterator are in no particular order as
418+
provided by the operating system's underlying directory mechanisms._
419+
287420
## Class: fs.Dirent
288421
<!-- YAML
289422
added: v10.10.0
290423
-->
291424

292-
When [`fs.readdir()`][] or [`fs.readdirSync()`][] is called with the
293-
`withFileTypes` option set to `true`, the resulting array is filled with
425+
A representation of a directory entry, as returned by reading from an [`fs.Dir`][].
426+
427+
Additionally, when [`fs.readdir()`][] or [`fs.readdirSync()`][] is called with
428+
the `withFileTypes` option set to `true`, the resulting array is filled with
294429
`fs.Dirent` objects, rather than strings or `Buffers`.
295430

296431
### dirent.isBlockDevice()
@@ -2505,6 +2640,46 @@ Returns an integer representing the file descriptor.
25052640
For detailed information, see the documentation of the asynchronous version of
25062641
this API: [`fs.open()`][].
25072642

2643+
## fs.opendir(path[, options], callback)
2644+
<!-- YAML
2645+
added: REPLACEME
2646+
-->
2647+
2648+
* `path` {string|Buffer|URL}
2649+
* `options` {Object}
2650+
* `encoding` {string|null} **Default:** `'utf8'`
2651+
* `callback` {Function}
2652+
* `err` {Error}
2653+
* `dir` {fs.Dir}
2654+
2655+
Asynchronously open a directory. See opendir(3).
2656+
2657+
Creates an [`fs.Dir`][], which contains all further functions for reading from
2658+
and cleaning up the directory.
2659+
2660+
The `encoding` option sets the encoding for the `path` while opening the
2661+
directory and subsequent read operations (unless otherwise overriden during
2662+
reads from the directory).
2663+
2664+
## fs.opendirSync(path[, options])
2665+
<!-- YAML
2666+
added: REPLACEME
2667+
-->
2668+
2669+
* `path` {string|Buffer|URL}
2670+
* `options` {Object}
2671+
* `encoding` {string|null} **Default:** `'utf8'`
2672+
* Returns: {fs.Dir}
2673+
2674+
Synchronously open a directory. See opendir(3).
2675+
2676+
Creates an [`fs.Dir`][], which contains all further functions for reading from
2677+
and cleaning up the directory.
2678+
2679+
The `encoding` option sets the encoding for the `path` while opening the
2680+
directory and subsequent read operations (unless otherwise overriden during
2681+
reads from the directory).
2682+
25082683
## fs.read(fd, buffer, offset, length, position, callback)
25092684
<!-- YAML
25102685
added: v0.0.2
@@ -4644,6 +4819,39 @@ by [Naming Files, Paths, and Namespaces][]. Under NTFS, if the filename contains
46444819
a colon, Node.js will open a file system stream, as described by
46454820
[this MSDN page][MSDN-Using-Streams].
46464821

4822+
## fsPromises.opendir(path[, options])
4823+
<!-- YAML
4824+
added: REPLACEME
4825+
-->
4826+
4827+
* `path` {string|Buffer|URL}
4828+
* `options` {Object}
4829+
* `encoding` {string|null} **Default:** `'utf8'`
4830+
* Returns: {Promise} containing {fs.Dir}
4831+
4832+
Asynchronously open a directory. See opendir(3).
4833+
4834+
Creates an [`fs.Dir`][], which contains all further functions for reading from
4835+
and cleaning up the directory.
4836+
4837+
The `encoding` option sets the encoding for the `path` while opening the
4838+
directory and subsequent read operations (unless otherwise overriden during
4839+
reads from the directory).
4840+
4841+
Example using async interation:
4842+
4843+
```js
4844+
const fs = require('fs');
4845+
4846+
async function print(path) {
4847+
const dir = await fs.promises.opendir(path);
4848+
for await (const dirent of dir) {
4849+
console.log(dirent.name);
4850+
}
4851+
}
4852+
print('./').catch(console.error);
4853+
```
4854+
46474855
### fsPromises.readdir(path[, options])
46484856
<!-- YAML
46494857
added: v10.0.0
@@ -5253,6 +5461,7 @@ the file contents.
52535461
[`UV_THREADPOOL_SIZE`]: cli.html#cli_uv_threadpool_size_size
52545462
[`WriteStream`]: #fs_class_fs_writestream
52555463
[`event ports`]: https://illumos.org/man/port_create
5464+
[`fs.Dir`]: #fs_class_fs_dir
52565465
[`fs.Dirent`]: #fs_class_fs_dirent
52575466
[`fs.FSWatcher`]: #fs_class_fs_fswatcher
52585467
[`fs.Stats`]: #fs_class_fs_stats
@@ -5269,6 +5478,8 @@ the file contents.
52695478
[`fs.mkdir()`]: #fs_fs_mkdir_path_options_callback
52705479
[`fs.mkdtemp()`]: #fs_fs_mkdtemp_prefix_options_callback
52715480
[`fs.open()`]: #fs_fs_open_path_flags_mode_callback
5481+
[`fs.opendir()`]: #fs_fs_opendir_path_options_callback
5482+
[`fs.opendirSync()`]: #fs_fs_opendirsync_path_options
52725483
[`fs.read()`]: #fs_fs_read_fd_buffer_offset_length_position_callback
52735484
[`fs.readFile()`]: #fs_fs_readfile_path_options_callback
52745485
[`fs.readFileSync()`]: #fs_fs_readfilesync_path_options
@@ -5284,6 +5495,7 @@ the file contents.
52845495
[`fs.write(fd, string...)`]: #fs_fs_write_fd_string_position_encoding_callback
52855496
[`fs.writeFile()`]: #fs_fs_writefile_file_data_options_callback
52865497
[`fs.writev()`]: #fs_fs_writev_fd_buffers_position_callback
5498+
[`fsPromises.opendir()`]: #fs_fspromises_opendir_path_options
52875499
[`inotify(7)`]: http://man7.org/linux/man-pages/man7/inotify.7.html
52885500
[`kqueue(2)`]: https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2
52895501
[`net.Socket`]: net.html#net_class_net_socket

lib/fs.js

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const {
6464
getDirents,
6565
getOptions,
6666
getValidatedPath,
67+
handleErrorFromBinding,
6768
nullCheck,
6869
preprocessSymlinkDestination,
6970
Stats,
@@ -79,6 +80,11 @@ const {
7980
validateRmdirOptions,
8081
warnOnNonPortableTemplate
8182
} = require('internal/fs/utils');
83+
const {
84+
Dir,
85+
opendir,
86+
opendirSync
87+
} = require('internal/fs/dir');
8288
const {
8389
CHAR_FORWARD_SLASH,
8490
CHAR_BACKWARD_SLASH,
@@ -122,23 +128,6 @@ function showTruncateDeprecation() {
122128
}
123129
}
124130

125-
function handleErrorFromBinding(ctx) {
126-
if (ctx.errno !== undefined) { // libuv error numbers
127-
const err = uvException(ctx);
128-
// eslint-disable-next-line no-restricted-syntax
129-
Error.captureStackTrace(err, handleErrorFromBinding);
130-
throw err;
131-
}
132-
if (ctx.error !== undefined) { // Errors created in C++ land.
133-
// TODO(joyeecheung): currently, ctx.error are encoding errors
134-
// usually caused by memory problems. We need to figure out proper error
135-
// code(s) for this.
136-
// eslint-disable-next-line no-restricted-syntax
137-
Error.captureStackTrace(ctx.error, handleErrorFromBinding);
138-
throw ctx.error;
139-
}
140-
}
141-
142131
function maybeCallback(cb) {
143132
if (typeof cb === 'function')
144133
return cb;
@@ -1834,7 +1823,6 @@ function createWriteStream(path, options) {
18341823
return new WriteStream(path, options);
18351824
}
18361825

1837-
18381826
module.exports = fs = {
18391827
appendFile,
18401828
appendFileSync,
@@ -1880,6 +1868,8 @@ module.exports = fs = {
18801868
mkdtempSync,
18811869
open,
18821870
openSync,
1871+
opendir,
1872+
opendirSync,
18831873
readdir,
18841874
readdirSync,
18851875
read,
@@ -1913,6 +1903,7 @@ module.exports = fs = {
19131903
writeSync,
19141904
writev,
19151905
writevSync,
1906+
Dir,
19161907
Dirent,
19171908
Stats,
19181909

lib/internal/errors.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,7 @@ E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error);
764764
E('ERR_CRYPTO_SIGN_KEY_REQUIRED', 'No key provided to sign', Error);
765765
E('ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH',
766766
'Input buffers must have the same byte length', RangeError);
767+
E('ERR_DIR_CLOSED', 'Directory handle was closed', Error);
767768
E('ERR_DNS_SET_SERVERS_FAILED', 'c-ares failed to set servers: "%s" [%s]',
768769
Error);
769770
E('ERR_DOMAIN_CALLBACK_NOT_AVAILABLE',

0 commit comments

Comments
 (0)