Skip to content

Commit

Permalink
fs: add recursive option to readdir and opendir
Browse files Browse the repository at this point in the history
Adds a naive, linear recursive algorithm for the following methods:
readdir, readdirSync, opendir, opendirSync, and the promise based
equivalents.

Fixes: #34992
PR-URL: #41439
Refs: nodejs/tooling#130
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
  • Loading branch information
Ethan Arrowood authored Apr 20, 2023
1 parent 6675505 commit 7b39e80
Show file tree
Hide file tree
Showing 7 changed files with 659 additions and 31 deletions.
35 changes: 35 additions & 0 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,9 @@ a colon, Node.js will open a file system stream, as described by
<!-- YAML
added: v12.12.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/41439
description: Added `recursive` option.
- version:
- v13.1.0
- v12.16.0
Expand All @@ -1233,6 +1236,8 @@ changes:
* `bufferSize` {number} Number of directory entries that are buffered
internally when reading from the directory. Higher values lead to better
performance but higher memory usage. **Default:** `32`
* `recursive` {boolean} Resolved `Dir` will be an {AsyncIterable}
containing all sub files and directories. **Default:** `false`
* Returns: {Promise} Fulfills with an {fs.Dir}.
Asynchronously open a directory for iterative scanning. See the POSIX
Expand Down Expand Up @@ -1266,6 +1271,9 @@ closed after the iterator exits.
<!-- YAML
added: v10.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/41439
description: Added `recursive` option.
- version: v10.11.0
pr-url: https://github.com/nodejs/node/pull/22020
description: New option `withFileTypes` was added.
Expand All @@ -1275,6 +1283,7 @@ changes:
* `options` {string|Object}
* `encoding` {string} **Default:** `'utf8'`
* `withFileTypes` {boolean} **Default:** `false`
* `recursive` {boolean} **Default:** `false`
* Returns: {Promise} Fulfills with an array of the names of the files in
the directory excluding `'.'` and `'..'`.
Expand Down Expand Up @@ -3402,6 +3411,9 @@ const { openAsBlob } = require('node:fs');
<!-- YAML
added: v12.12.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/41439
description: Added `recursive` option.
- version: v18.0.0
pr-url: https://github.com/nodejs/node/pull/41678
description: Passing an invalid callback to the `callback` argument
Expand All @@ -3420,6 +3432,7 @@ changes:
* `bufferSize` {number} Number of directory entries that are buffered
internally when reading from the directory. Higher values lead to better
performance but higher memory usage. **Default:** `32`
* `recursive` {boolean} **Default:** `false`
* `callback` {Function}
* `err` {Error}
* `dir` {fs.Dir}
Expand Down Expand Up @@ -3538,6 +3551,9 @@ above values.
<!-- YAML
added: v0.1.8
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/41439
description: Added `recursive` option.
- version: v18.0.0
pr-url: https://github.com/nodejs/node/pull/41678
description: Passing an invalid callback to the `callback` argument
Expand Down Expand Up @@ -3567,6 +3583,7 @@ changes:
* `options` {string|Object}
* `encoding` {string} **Default:** `'utf8'`
* `withFileTypes` {boolean} **Default:** `false`
* `recursive` {boolean} **Default:** `false`
* `callback` {Function}
* `err` {Error}
* `files` {string\[]|Buffer\[]|fs.Dirent\[]}
Expand Down Expand Up @@ -5543,6 +5560,9 @@ object with an `encoding` property specifying the character encoding to use.
<!-- YAML
added: v12.12.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/41439
description: Added `recursive` option.
- version:
- v13.1.0
- v12.16.0
Expand All @@ -5556,6 +5576,7 @@ changes:
* `bufferSize` {number} Number of directory entries that are buffered
internally when reading from the directory. Higher values lead to better
performance but higher memory usage. **Default:** `32`
* `recursive` {boolean} **Default:** `false`
* Returns: {fs.Dir}
Synchronously open a directory. See opendir(3).
Expand Down Expand Up @@ -5599,6 +5620,9 @@ this API: [`fs.open()`][].
<!-- YAML
added: v0.1.21
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/41439
description: Added `recursive` option.
- version: v10.10.0
pr-url: https://github.com/nodejs/node/pull/22020
description: New option `withFileTypes` was added.
Expand All @@ -5612,6 +5636,7 @@ changes:
* `options` {string|Object}
* `encoding` {string} **Default:** `'utf8'`
* `withFileTypes` {boolean} **Default:** `false`
* `recursive` {boolean} **Default:** `false`
* Returns: {string\[]|Buffer\[]|fs.Dirent\[]}
Reads the contents of the directory.
Expand Down Expand Up @@ -6465,6 +6490,16 @@ The file name that this {fs.Dirent} object refers to. The type of this
value is determined by the `options.encoding` passed to [`fs.readdir()`][] or
[`fs.readdirSync()`][].
#### `dirent.path`
<!-- YAML
added: REPLACEME
-->
* {string}
The base path that this {fs.Dirent} object refers to.
### Class: `fs.FSWatcher`
<!-- YAML
Expand Down
47 changes: 47 additions & 0 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -1404,6 +1404,36 @@ function mkdirSync(path, options) {
}
}

// TODO(Ethan-Arrowood): Make this iterative too
function readdirSyncRecursive(path, origPath, options) {
nullCheck(path, 'path', true);
const ctx = { path };
const result = binding.readdir(pathModule.toNamespacedPath(path),
options.encoding, !!options.withFileTypes, undefined, ctx);
handleErrorFromBinding(ctx);
return options.withFileTypes ?
getDirents(path, result).flatMap((dirent) => {
return [
dirent,
...(dirent.isDirectory() ?
readdirSyncRecursive(
pathModule.join(path, dirent.name),
origPath,
options,
) : []),
];
}) :
result.flatMap((ent) => {
const innerPath = pathModule.join(path, ent);
const relativePath = pathModule.relative(origPath, innerPath);
const stat = binding.internalModuleStat(innerPath);
return [
relativePath,
...(stat === 1 ? readdirSyncRecursive(innerPath, origPath, options) : []),
];
});
}

/**
* Reads the contents of a directory.
* @param {string | Buffer | URL} path
Expand All @@ -1421,6 +1451,14 @@ function readdir(path, options, callback) {
callback = makeCallback(typeof options === 'function' ? options : callback);
options = getOptions(options);
path = getValidatedPath(path);
if (options.recursive != null) {
validateBoolean(options.recursive, 'options.recursive');
}

if (options.recursive) {
callback(null, readdirSyncRecursive(path, path, options));
return;
}

const req = new FSReqCallback();
if (!options.withFileTypes) {
Expand All @@ -1444,12 +1482,21 @@ function readdir(path, options, callback) {
* @param {string | {
* encoding?: string;
* withFileTypes?: boolean;
* recursive?: boolean;
* }} [options]
* @returns {string | Buffer[] | Dirent[]}
*/
function readdirSync(path, options) {
options = getOptions(options);
path = getValidatedPath(path);
if (options.recursive != null) {
validateBoolean(options.recursive, 'options.recursive');
}

if (options.recursive) {
return readdirSyncRecursive(path, path, options);
}

const ctx = { path };
const result = binding.readdir(pathModule.toNamespacedPath(path),
options.encoding, !!options.withFileTypes,
Expand Down
93 changes: 77 additions & 16 deletions lib/internal/fs/dir.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

const {
ArrayPrototypePush,
ArrayPrototypeSlice,
ArrayPrototypeSplice,
ArrayPrototypeShift,
FunctionPrototypeBind,
ObjectDefineProperty,
PromiseReject,
Expand Down Expand Up @@ -99,13 +98,21 @@ class Dir {
}

if (this[kDirBufferedEntries].length > 0) {
const { 0: name, 1: type } =
ArrayPrototypeSplice(this[kDirBufferedEntries], 0, 2);
if (maybeSync)
process.nextTick(getDirent, this[kDirPath], name, type, callback);
else
getDirent(this[kDirPath], name, type, callback);
return;
try {
const dirent = ArrayPrototypeShift(this[kDirBufferedEntries]);

if (this[kDirOptions].recursive && dirent.isDirectory()) {
this.readSyncRecursive(dirent);
}

if (maybeSync)
process.nextTick(callback, null, dirent);
else
callback(null, dirent);
return;
} catch (error) {
return callback(error);
}
}

const req = new FSReqCallback();
Expand All @@ -120,8 +127,16 @@ class Dir {
return callback(err, result);
}

this[kDirBufferedEntries] = ArrayPrototypeSlice(result, 2);
getDirent(this[kDirPath], result[0], result[1], callback);
try {
this.processReadResult(this[kDirPath], result);
const dirent = ArrayPrototypeShift(this[kDirBufferedEntries]);
if (this[kDirOptions].recursive && dirent.isDirectory()) {
this.readSyncRecursive(dirent);
}
callback(null, dirent);
} catch (error) {
callback(error);
}
};

this[kDirOperationQueue] = [];
Expand All @@ -132,6 +147,45 @@ class Dir {
);
}

processReadResult(path, result) {
for (let i = 0; i < result.length; i += 2) {
ArrayPrototypePush(
this[kDirBufferedEntries],
getDirent(
pathModule.join(path, result[i]),
result[i],
result[i + 1],
),
);
}
}

// TODO(Ethan-Arrowood): Review this implementation. Make it iterative.
// Can we better leverage the `kDirOperationQueue`?
readSyncRecursive(dirent) {
const ctx = { path: dirent.path };
const handle = dirBinding.opendir(
pathModule.toNamespacedPath(dirent.path),
this[kDirOptions].encoding,
undefined,
ctx,
);
handleErrorFromBinding(ctx);
const result = handle.read(
this[kDirOptions].encoding,
this[kDirOptions].bufferSize,
undefined,
ctx,
);

if (result) {
this.processReadResult(dirent.path, result);
}

handle.close(undefined, ctx);
handleErrorFromBinding(ctx);
}

readSync() {
if (this[kDirClosed] === true) {
throw new ERR_DIR_CLOSED();
Expand All @@ -142,9 +196,11 @@ class Dir {
}

if (this[kDirBufferedEntries].length > 0) {
const { 0: name, 1: type } =
ArrayPrototypeSplice(this[kDirBufferedEntries], 0, 2);
return getDirent(this[kDirPath], name, type);
const dirent = ArrayPrototypeShift(this[kDirBufferedEntries]);
if (this[kDirOptions].recursive && dirent.isDirectory()) {
this.readSyncRecursive(dirent);
}
return dirent;
}

const ctx = { path: this[kDirPath] };
Expand All @@ -160,8 +216,13 @@ class Dir {
return result;
}

this[kDirBufferedEntries] = ArrayPrototypeSlice(result, 2);
return getDirent(this[kDirPath], result[0], result[1]);
this.processReadResult(this[kDirPath], result);

const dirent = ArrayPrototypeShift(this[kDirBufferedEntries]);
if (this[kDirOptions].recursive && dirent.isDirectory()) {
this.readSyncRecursive(dirent);
}
return dirent;
}

close(callback) {
Expand Down
Loading

0 comments on commit 7b39e80

Please sign in to comment.