Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
  • Loading branch information
Fishrock123 committed Oct 7, 2019
commit 018fc768afe6f28266d31e111affa0495c3eeca4
6 changes: 6 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,11 @@ A signing `key` was not provided to the [`sign.sign()`][] method.
[`crypto.timingSafeEqual()`][] was called with `Buffer`, `TypedArray`, or
`DataView` arguments of different lengths.

<a id="ERR_DIR_CLOSED"></a>
### ERR_DIR_CLOSED

The [`fs.Dir`][] was previously closed.

<a id="ERR_DNS_SET_SERVERS_FAILED"></a>
### ERR_DNS_SET_SERVERS_FAILED

Expand Down Expand Up @@ -2388,6 +2393,7 @@ such as `process.stdout.on('data')`.
[`dgram.disconnect()`]: dgram.html#dgram_socket_disconnect
[`dgram.remoteAddress()`]: dgram.html#dgram_socket_remoteaddress
[`errno`(3) man page]: http://man7.org/linux/man-pages/man3/errno.3.html
[`fs.Dir`]: fs.html#fs_class_fs_dir
[`fs.readFileSync`]: fs.html#fs_fs_readfilesync_path_options
[`fs.readdir`]: fs.html#fs_fs_readdir_path_options_callback
[`fs.symlink()`]: fs.html#fs_fs_symlink_target_path_type_callback
Expand Down
216 changes: 214 additions & 2 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,13 +284,148 @@ synchronous use libuv's threadpool, which can have surprising and negative
performance implications for some applications. See the
[`UV_THREADPOOL_SIZE`][] documentation for more information.

## Class fs.Dir
<!-- YAML
added: REPLACEME
-->

A class representing a directory stream.

Created by [`fs.opendir()`][], [`fs.opendirSync()`][], or [`fsPromises.opendir()`][].
Comment thread
Fishrock123 marked this conversation as resolved.
Outdated

Example using async interation:

```js
const fs = require('fs');

async function print(path) {
const dir = await fs.promises.opendir(path);
for await (const dirent of dir) {
console.log(dirent.name);
}
}
print('./').catch(console.error);
```

### dir.path
<!-- YAML
added: REPLACEME
-->

* {string}

The read-only path of this directory as was provided to [`fs.opendir()`][],
[`fs.opendirSync()`][], or [`fsPromises.opendir()`][].

### dir.close()
<!-- YAML
added: REPLACEME
-->

* Returns: {Promise}

Asynchronously close the directory's underlying resource handle.
Subsequent reads will result in errors.

A `Promise` is returned that will be resolved after the resource has been
closed.

### dir.close(callback)
<!-- YAML
added: REPLACEME
-->

* `callback` {Function}
* `err` {Error}

Asynchronously close the directory's underlying resource handle.
Subsequent reads will result in errors.

Comment thread
Fishrock123 marked this conversation as resolved.
Outdated
The `callback` will be called after the resource handle has been closed.

### dir.closeSync()
<!-- YAML
added: REPLACEME
-->

Synchronously close the directory's underlying resource handle.
Subsequent reads will result in errors.

### dir.read([options])
<!-- YAML
added: REPLACEME
-->

* `options` {Object}
* `encoding` {string|null} **Default:** `'utf8'`
* Returns: {Promise} containing {fs.Dirent}

Asynchronously read the next directory entry via readdir(3) as an
[`fs.Dirent`][].

A `Promise` is returned that will be resolved with a [Dirent][] after the read
is completed.

_Directory entries returned by this function are in no particular order as
provided by the operating system's underlying directory mechanisms._

### dir.read([options, ]callback)
<!-- YAML
added: REPLACEME
-->

* `options` {Object}
* `encoding` {string|null} **Default:** `'utf8'`
* `callback` {Function}
* `err` {Error}
* `dirent` {fs.Dirent}

Asynchronously read the next directory entry via readdir(3) as an
[`fs.Dirent`][].

The `callback` will be called with a [Dirent][] after the read is completed.

The `encoding` option sets the encoding of the `name` in the `dirent`.

_Directory entries returned by this function are in no particular order as
provided by the operating system's underlying directory mechanisms._

### dir.readSync([options])
<!-- YAML
added: REPLACEME
-->

* `options` {Object}
* `encoding` {string|null} **Default:** `'utf8'`
* Returns: {fs.Dirent}

Synchronously read the next directory entry via readdir(3) as an
[`fs.Dirent`][].

The `encoding` option sets the encoding of the `name` in the `dirent`.

_Directory entries returned by this function are in no particular order as
provided by the operating system's underlying directory mechanisms._

### dir\[Symbol.asyncIterator\]()
<!-- YAML
added: REPLACEME
-->

* Returns: {AsyncIterator} to fully iterate over all entries in the directory.

_Directory entries returned by this iterator are in no particular order as
provided by the operating system's underlying directory mechanisms._

## Class: fs.Dirent
<!-- YAML
added: v10.10.0
-->

When [`fs.readdir()`][] or [`fs.readdirSync()`][] is called with the
`withFileTypes` option set to `true`, the resulting array is filled with
A representation of a directory entry, as returned by reading from an [`fs.Dir`][].

Additionally, when [`fs.readdir()`][] or [`fs.readdirSync()`][] is called with
the `withFileTypes` option set to `true`, the resulting array is filled with
`fs.Dirent` objects, rather than strings or `Buffers`.

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

## fs.opendir(path[, options], callback)
<!-- YAML
added: REPLACEME
-->

* `path` {string|Buffer|URL}
* `options` {Object}
* `encoding` {string|null} **Default:** `'utf8'`
Comment thread
Fishrock123 marked this conversation as resolved.
Outdated
* `callback` {Function}
* `err` {Error}
* `dir` {fs.Dir}

Asynchronously open a directory. See opendir(3).

Creates an [`fs.Dir`][], which contains all further functions for reading from
and cleaning up the directory.

The `encoding` option sets the encoding for the `path` while opening the
directory and subsequent read operations (unless otherwise overriden during
reads from the directory).

## fs.opendirSync(path[, options])
<!-- YAML
added: REPLACEME
-->

* `path` {string|Buffer|URL}
* `options` {Object}
* `encoding` {string|null} **Default:** `'utf8'`
* Returns: {fs.Dir}

Synchronously open a directory. See opendir(3).

Creates an [`fs.Dir`][], which contains all further functions for reading from
and cleaning up the directory.

The `encoding` option sets the encoding for the `path` while opening the
directory and subsequent read operations (unless otherwise overriden during
reads from the directory).

## fs.read(fd, buffer, offset, length, position, callback)
<!-- YAML
added: v0.0.2
Expand Down Expand Up @@ -4644,6 +4819,39 @@ by [Naming Files, Paths, and Namespaces][]. Under NTFS, if the filename contains
a colon, Node.js will open a file system stream, as described by
[this MSDN page][MSDN-Using-Streams].

## fsPromises.opendir(path[, options])
<!-- YAML
added: REPLACEME
-->

* `path` {string|Buffer|URL}
* `options` {Object}
* `encoding` {string|null} **Default:** `'utf8'`
* Returns: {Promise} containing {fs.Dir}

Asynchronously open a directory. See opendir(3).

Creates an [`fs.Dir`][], which contains all further functions for reading from
and cleaning up the directory.
Comment thread
Fishrock123 marked this conversation as resolved.
Outdated

The `encoding` option sets the encoding for the `path` while opening the
directory and subsequent read operations (unless otherwise overriden during
reads from the directory).

Example using async interation:

```js
const fs = require('fs');

async function print(path) {
const dir = await fs.promises.opendir(path);
for await (const dirent of dir) {
console.log(dirent.name);
}
}
print('./').catch(console.error);
```

### fsPromises.readdir(path[, options])
<!-- YAML
added: v10.0.0
Expand Down Expand Up @@ -5253,6 +5461,7 @@ the file contents.
[`UV_THREADPOOL_SIZE`]: cli.html#cli_uv_threadpool_size_size
[`WriteStream`]: #fs_class_fs_writestream
[`event ports`]: https://illumos.org/man/port_create
[`fs.Dir`]: #fs_class_fs_dir
[`fs.Dirent`]: #fs_class_fs_dirent
[`fs.FSWatcher`]: #fs_class_fs_fswatcher
[`fs.Stats`]: #fs_class_fs_stats
Expand All @@ -5269,6 +5478,8 @@ the file contents.
[`fs.mkdir()`]: #fs_fs_mkdir_path_options_callback
[`fs.mkdtemp()`]: #fs_fs_mkdtemp_prefix_options_callback
[`fs.open()`]: #fs_fs_open_path_flags_mode_callback
[`fs.opendir()`]: #fs_fs_opendir_path_options_callback
[`fs.opendirSync()`]: #fs_fs_opendirsync_path_options
[`fs.read()`]: #fs_fs_read_fd_buffer_offset_length_position_callback
[`fs.readFile()`]: #fs_fs_readfile_path_options_callback
[`fs.readFileSync()`]: #fs_fs_readfilesync_path_options
Expand All @@ -5284,6 +5495,7 @@ the file contents.
[`fs.write(fd, string...)`]: #fs_fs_write_fd_string_position_encoding_callback
[`fs.writeFile()`]: #fs_fs_writefile_file_data_options_callback
[`fs.writev()`]: #fs_fs_writev_fd_buffers_position_callback
[`fsPromises.opendir()`]: #fs_fspromises_opendir_path_options
[`inotify(7)`]: http://man7.org/linux/man-pages/man7/inotify.7.html
[`kqueue(2)`]: https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2
[`net.Socket`]: net.html#net_class_net_socket
Expand Down
27 changes: 9 additions & 18 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const {
getDirents,
getOptions,
getValidatedPath,
handleErrorFromBinding,
nullCheck,
preprocessSymlinkDestination,
Stats,
Expand All @@ -79,6 +80,11 @@ const {
validateRmdirOptions,
warnOnNonPortableTemplate
} = require('internal/fs/utils');
const {
Dir,
opendir,
opendirSync
} = require('internal/fs/dir');
const {
CHAR_FORWARD_SLASH,
CHAR_BACKWARD_SLASH,
Expand Down Expand Up @@ -122,23 +128,6 @@ function showTruncateDeprecation() {
}
}

function handleErrorFromBinding(ctx) {
if (ctx.errno !== undefined) { // libuv error numbers
const err = uvException(ctx);
// eslint-disable-next-line no-restricted-syntax
Error.captureStackTrace(err, handleErrorFromBinding);
throw err;
}
if (ctx.error !== undefined) { // Errors created in C++ land.
// TODO(joyeecheung): currently, ctx.error are encoding errors
// usually caused by memory problems. We need to figure out proper error
// code(s) for this.
// eslint-disable-next-line no-restricted-syntax
Error.captureStackTrace(ctx.error, handleErrorFromBinding);
throw ctx.error;
}
}

function maybeCallback(cb) {
if (typeof cb === 'function')
return cb;
Expand Down Expand Up @@ -1834,7 +1823,6 @@ function createWriteStream(path, options) {
return new WriteStream(path, options);
}


module.exports = fs = {
appendFile,
appendFileSync,
Expand Down Expand Up @@ -1880,6 +1868,8 @@ module.exports = fs = {
mkdtempSync,
open,
openSync,
opendir,
opendirSync,
readdir,
readdirSync,
read,
Expand Down Expand Up @@ -1913,6 +1903,7 @@ module.exports = fs = {
writeSync,
writev,
writevSync,
Dir,
Dirent,
Stats,

Expand Down
1 change: 1 addition & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,7 @@ E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error);
E('ERR_CRYPTO_SIGN_KEY_REQUIRED', 'No key provided to sign', Error);
E('ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH',
'Input buffers must have the same byte length', RangeError);
E('ERR_DIR_CLOSED', 'Directory handle was closed', Error);
E('ERR_DNS_SET_SERVERS_FAILED', 'c-ares failed to set servers: "%s" [%s]',
Error);
E('ERR_DOMAIN_CALLBACK_NOT_AVAILABLE',
Expand Down
Loading