Skip to content

Commit b76a2e5

Browse files
Fishrock123BridgeAR
authored andcommittedOct 9, 2019
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 PR-URL: #29349 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: David Carlier <[email protected]>
1 parent f566cd5 commit b76a2e5

19 files changed

+1165
-119
lines changed
 

‎doc/api/errors.md

+6
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,11 @@ A signing `key` was not provided to the [`sign.sign()`][] method.
824824
[`crypto.timingSafeEqual()`][] was called with `Buffer`, `TypedArray`, or
825825
`DataView` arguments of different lengths.
826826

827+
<a id="ERR_DIR_CLOSED"></a>
828+
### ERR_DIR_CLOSED
829+
830+
The [`fs.Dir`][] was previously closed.
831+
827832
<a id="ERR_DNS_SET_SERVERS_FAILED"></a>
828833
### ERR_DNS_SET_SERVERS_FAILED
829834

@@ -2380,6 +2385,7 @@ such as `process.stdout.on('data')`.
23802385
[`dgram.disconnect()`]: dgram.html#dgram_socket_disconnect
23812386
[`dgram.remoteAddress()`]: dgram.html#dgram_socket_remoteaddress
23822387
[`errno`(3) man page]: http://man7.org/linux/man-pages/man3/errno.3.html
2388+
[`fs.Dir`]: fs.html#fs_class_fs_dir
23832389
[`fs.readFileSync`]: fs.html#fs_fs_readfilesync_path_options
23842390
[`fs.readdir`]: fs.html#fs_fs_readdir_path_options_callback
23852391
[`fs.symlink()`]: fs.html#fs_fs_symlink_target_path_type_callback

‎doc/api/fs.md

+214-2
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

+9-18
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;
@@ -1818,7 +1807,6 @@ function createWriteStream(path, options) {
18181807
return new WriteStream(path, options);
18191808
}
18201809

1821-
18221810
module.exports = fs = {
18231811
appendFile,
18241812
appendFileSync,
@@ -1864,6 +1852,8 @@ module.exports = fs = {
18641852
mkdtempSync,
18651853
open,
18661854
openSync,
1855+
opendir,
1856+
opendirSync,
18671857
readdir,
18681858
readdirSync,
18691859
read,
@@ -1897,6 +1887,7 @@ module.exports = fs = {
18971887
writeSync,
18981888
writev,
18991889
writevSync,
1890+
Dir,
19001891
Dirent,
19011892
Stats,
19021893

‎lib/internal/errors.js

+1
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,7 @@ E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error);
759759
E('ERR_CRYPTO_SIGN_KEY_REQUIRED', 'No key provided to sign', Error);
760760
E('ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH',
761761
'Input buffers must have the same byte length', RangeError);
762+
E('ERR_DIR_CLOSED', 'Directory handle was closed', Error);
762763
E('ERR_DNS_SET_SERVERS_FAILED', 'c-ares failed to set servers: "%s" [%s]',
763764
Error);
764765
E('ERR_DOMAIN_CALLBACK_NOT_AVAILABLE',

0 commit comments

Comments
 (0)
Please sign in to comment.