Skip to content

Commit 611a616

Browse files
author
Ethan Arrowood
committed
fs: add recursive option to readdir and opendir
Adds a naive, linear recursive algorithm for the following methods: readdir, readdirSync, opendir, opendirSync, and the promise based equivalents. Fixes: #34992
1 parent 3abbc38 commit 611a616

File tree

7 files changed

+643
-30
lines changed

7 files changed

+643
-30
lines changed

doc/api/fs.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,6 +1204,9 @@ a colon, Node.js will open a file system stream, as described by
12041204
<!-- YAML
12051205
added: v12.12.0
12061206
changes:
1207+
- version: REPLACEME
1208+
pr-url: https://github.com/nodejs/node/pull/41439
1209+
description: Added `recursive` option.
12071210
- version:
12081211
- v13.1.0
12091212
- v12.16.0
@@ -1217,6 +1220,8 @@ changes:
12171220
* `bufferSize` {number} Number of directory entries that are buffered
12181221
internally when reading from the directory. Higher values lead to better
12191222
performance but higher memory usage. **Default:** `32`
1223+
* `recursive` {boolean} Resolved `Dir` will be an {AsyncIterable}
1224+
containing all sub files and directories. **Default:** `false`
12201225
* Returns: {Promise} Fulfills with an {fs.Dir}.
12211226
12221227
Asynchronously open a directory for iterative scanning. See the POSIX
@@ -1250,6 +1255,9 @@ closed after the iterator exits.
12501255
<!-- YAML
12511256
added: v10.0.0
12521257
changes:
1258+
- version: REPLACEME
1259+
pr-url: https://github.com/nodejs/node/pull/41439
1260+
description: Added `recursive` option.
12531261
- version: v10.11.0
12541262
pr-url: https://github.com/nodejs/node/pull/22020
12551263
description: New option `withFileTypes` was added.
@@ -1259,6 +1267,7 @@ changes:
12591267
* `options` {string|Object}
12601268
* `encoding` {string} **Default:** `'utf8'`
12611269
* `withFileTypes` {boolean} **Default:** `false`
1270+
* `recursive` {boolean} **Default:** `false`
12621271
* Returns: {Promise} Fulfills with an array of the names of the files in
12631272
the directory excluding `'.'` and `'..'`.
12641273
@@ -3368,6 +3377,9 @@ const { openAsBlob } = require('node:fs');
33683377
<!-- YAML
33693378
added: v12.12.0
33703379
changes:
3380+
- version: REPLACEME
3381+
pr-url: https://github.com/nodejs/node/pull/41439
3382+
description: Added `recursive` option.
33713383
- version: v18.0.0
33723384
pr-url: https://github.com/nodejs/node/pull/41678
33733385
description: Passing an invalid callback to the `callback` argument
@@ -3386,6 +3398,7 @@ changes:
33863398
* `bufferSize` {number} Number of directory entries that are buffered
33873399
internally when reading from the directory. Higher values lead to better
33883400
performance but higher memory usage. **Default:** `32`
3401+
* `recursive` {boolean} **Default:** `false`
33893402
* `callback` {Function}
33903403
* `err` {Error}
33913404
* `dir` {fs.Dir}
@@ -3504,6 +3517,9 @@ above values.
35043517
<!-- YAML
35053518
added: v0.1.8
35063519
changes:
3520+
- version: REPLACEME
3521+
pr-url: https://github.com/nodejs/node/pull/41439
3522+
description: Added `recursive` option.
35073523
- version: v18.0.0
35083524
pr-url: https://github.com/nodejs/node/pull/41678
35093525
description: Passing an invalid callback to the `callback` argument
@@ -3533,6 +3549,7 @@ changes:
35333549
* `options` {string|Object}
35343550
* `encoding` {string} **Default:** `'utf8'`
35353551
* `withFileTypes` {boolean} **Default:** `false`
3552+
* `recursive` {boolean} **Default:** `false`
35363553
* `callback` {Function}
35373554
* `err` {Error}
35383555
* `files` {string\[]|Buffer\[]|fs.Dirent\[]}
@@ -5501,6 +5518,9 @@ object with an `encoding` property specifying the character encoding to use.
55015518
<!-- YAML
55025519
added: v12.12.0
55035520
changes:
5521+
- version: REPLACEME
5522+
pr-url: https://github.com/nodejs/node/pull/41439
5523+
description: Added `recursive` option.
55045524
- version:
55055525
- v13.1.0
55065526
- v12.16.0
@@ -5514,6 +5534,7 @@ changes:
55145534
* `bufferSize` {number} Number of directory entries that are buffered
55155535
internally when reading from the directory. Higher values lead to better
55165536
performance but higher memory usage. **Default:** `32`
5537+
* `recursive` {boolean} **Default:** `false`
55175538
* Returns: {fs.Dir}
55185539
55195540
Synchronously open a directory. See opendir(3).
@@ -5557,6 +5578,9 @@ this API: [`fs.open()`][].
55575578
<!-- YAML
55585579
added: v0.1.21
55595580
changes:
5581+
- version: REPLACEME
5582+
pr-url: https://github.com/nodejs/node/pull/41439
5583+
description: Added `recursive` option.
55605584
- version: v10.10.0
55615585
pr-url: https://github.com/nodejs/node/pull/22020
55625586
description: New option `withFileTypes` was added.
@@ -5570,6 +5594,7 @@ changes:
55705594
* `options` {string|Object}
55715595
* `encoding` {string} **Default:** `'utf8'`
55725596
* `withFileTypes` {boolean} **Default:** `false`
5597+
* `recursive` {boolean} **Default:** `false`
55735598
* Returns: {string\[]|Buffer\[]|fs.Dirent\[]}
55745599
55755600
Reads the contents of the directory.
@@ -6421,6 +6446,16 @@ The file name that this {fs.Dirent} object refers to. The type of this
64216446
value is determined by the `options.encoding` passed to [`fs.readdir()`][] or
64226447
[`fs.readdirSync()`][].
64236448
6449+
#### `dirent.path`
6450+
6451+
<!-- YAML
6452+
added: REPLACEME
6453+
-->
6454+
6455+
* {string}
6456+
6457+
The base path that this {fs.Dirent} object refers to.
6458+
64246459
### Class: `fs.FSWatcher`
64256460
64266461
<!-- YAML

lib/fs.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,6 +1404,31 @@ function mkdirSync(path, options) {
14041404
}
14051405
}
14061406

1407+
// TODO(Ethan-Arrowood): Make this iterative too
1408+
function readdirSyncRecursive(path, origPath, options) {
1409+
nullCheck(path, 'path', true);
1410+
const ctx = { path };
1411+
const result = binding.readdir(pathModule.toNamespacedPath(path),
1412+
options.encoding, !!options.withFileTypes, undefined, ctx);
1413+
handleErrorFromBinding(ctx);
1414+
return options.withFileTypes ?
1415+
getDirents(path, result).flatMap((dirent) => {
1416+
return [
1417+
dirent,
1418+
...(dirent.isDirectory() ? readdirSyncRecursive(pathModule.join(path, dirent.name), origPath, options) : []),
1419+
];
1420+
}) :
1421+
result.flatMap((ent) => {
1422+
const innerPath = pathModule.join(path, ent);
1423+
const relativePath = pathModule.relative(origPath, innerPath);
1424+
const stat = fs.lstatSync(innerPath);
1425+
return [
1426+
relativePath,
1427+
...(stat.isDirectory() ? readdirSyncRecursive(innerPath, origPath, options) : []),
1428+
];
1429+
});
1430+
}
1431+
14071432
/**
14081433
* Reads the contents of a directory.
14091434
* @param {string | Buffer | URL} path
@@ -1421,6 +1446,14 @@ function readdir(path, options, callback) {
14211446
callback = makeCallback(typeof options === 'function' ? options : callback);
14221447
options = getOptions(options);
14231448
path = getValidatedPath(path);
1449+
if (options.recursive != null) {
1450+
validateBoolean(options.recursive, 'options.recursive');
1451+
}
1452+
1453+
if (options.recursive) {
1454+
callback(null, readdirSyncRecursive(path, path, options));
1455+
return;
1456+
}
14241457

14251458
const req = new FSReqCallback();
14261459
if (!options.withFileTypes) {
@@ -1444,12 +1477,21 @@ function readdir(path, options, callback) {
14441477
* @param {string | {
14451478
* encoding?: string;
14461479
* withFileTypes?: boolean;
1480+
* recursive?: boolean;
14471481
* }} [options]
14481482
* @returns {string | Buffer[] | Dirent[]}
14491483
*/
14501484
function readdirSync(path, options) {
14511485
options = getOptions(options);
14521486
path = getValidatedPath(path);
1487+
if (options.recursive != null) {
1488+
validateBoolean(options.recursive, 'options.recursive');
1489+
}
1490+
1491+
if (options.recursive) {
1492+
return readdirSyncRecursive(path, path, options);
1493+
}
1494+
14531495
const ctx = { path };
14541496
const result = binding.readdir(pathModule.toNamespacedPath(path),
14551497
options.encoding, !!options.withFileTypes,

lib/internal/fs/dir.js

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
const {
44
ArrayPrototypePush,
5-
ArrayPrototypeSlice,
6-
ArrayPrototypeSplice,
5+
ArrayPrototypeShift,
76
FunctionPrototypeBind,
87
ObjectDefineProperty,
98
PromiseReject,
@@ -99,13 +98,21 @@ class Dir {
9998
}
10099

101100
if (this[kDirBufferedEntries].length > 0) {
102-
const { 0: name, 1: type } =
103-
ArrayPrototypeSplice(this[kDirBufferedEntries], 0, 2);
104-
if (maybeSync)
105-
process.nextTick(getDirent, this[kDirPath], name, type, callback);
106-
else
107-
getDirent(this[kDirPath], name, type, callback);
108-
return;
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+
}
109116
}
110117

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

123-
this[kDirBufferedEntries] = ArrayPrototypeSlice(result, 2);
124-
getDirent(this[kDirPath], result[0], result[1], callback);
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+
}
125140
};
126141

127142
this[kDirOperationQueue] = [];
@@ -132,6 +147,45 @@ class Dir {
132147
);
133148
}
134149

150+
processReadResult(path, result) {
151+
for (let i = 0; i < result.length; i += 2) {
152+
ArrayPrototypePush(
153+
this[kDirBufferedEntries],
154+
getDirent(
155+
pathModule.join(path, result[i]),
156+
result[i],
157+
result[i + 1],
158+
),
159+
);
160+
}
161+
}
162+
163+
// TODO(Ethan-Arrowood): Review this implementation. Make it iterative.
164+
// Can we better leverage the `kDirOperationQueue`?
165+
readSyncRecursive(dirent) {
166+
const ctx = { path: dirent.path };
167+
const handle = dirBinding.opendir(
168+
pathModule.toNamespacedPath(dirent.path),
169+
this[kDirOptions].encoding,
170+
undefined,
171+
ctx,
172+
);
173+
handleErrorFromBinding(ctx);
174+
const result = handle.read(
175+
this[kDirOptions].encoding,
176+
this[kDirOptions].bufferSize,
177+
undefined,
178+
ctx,
179+
);
180+
181+
if (result) {
182+
this.processReadResult(dirent.path, result);
183+
}
184+
185+
handle.close(undefined, ctx);
186+
handleErrorFromBinding(ctx);
187+
}
188+
135189
readSync() {
136190
if (this[kDirClosed] === true) {
137191
throw new ERR_DIR_CLOSED();
@@ -142,9 +196,11 @@ class Dir {
142196
}
143197

144198
if (this[kDirBufferedEntries].length > 0) {
145-
const { 0: name, 1: type } =
146-
ArrayPrototypeSplice(this[kDirBufferedEntries], 0, 2);
147-
return getDirent(this[kDirPath], name, type);
199+
const dirent = ArrayPrototypeShift(this[kDirBufferedEntries]);
200+
if (this[kDirOptions].recursive && dirent.isDirectory()) {
201+
this.readSyncRecursive(dirent);
202+
}
203+
return dirent;
148204
}
149205

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

163-
this[kDirBufferedEntries] = ArrayPrototypeSlice(result, 2);
164-
return getDirent(this[kDirPath], result[0], result[1]);
219+
this.processReadResult(this[kDirPath], result);
220+
221+
const dirent = ArrayPrototypeShift(this[kDirBufferedEntries]);
222+
if (this[kDirOptions].recursive && dirent.isDirectory()) {
223+
this.readSyncRecursive(dirent);
224+
}
225+
return dirent;
165226
}
166227

167228
close(callback) {

0 commit comments

Comments
 (0)