Skip to content

Commit 7273ef5

Browse files
Ethan ArrowoodMoLow
Ethan Arrowood
authored andcommittedJul 6, 2023
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 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>
1 parent cc7e5dd commit 7273ef5

File tree

7 files changed

+659
-31
lines changed

7 files changed

+659
-31
lines changed
 

‎doc/api/fs.md

+35
Original file line numberDiff line numberDiff line change
@@ -1214,6 +1214,9 @@ a colon, Node.js will open a file system stream, as described by
12141214
<!-- YAML
12151215
added: v12.12.0
12161216
changes:
1217+
- version: REPLACEME
1218+
pr-url: https://github.com/nodejs/node/pull/41439
1219+
description: Added `recursive` option.
12171220
- version:
12181221
- v13.1.0
12191222
- v12.16.0
@@ -1227,6 +1230,8 @@ changes:
12271230
* `bufferSize` {number} Number of directory entries that are buffered
12281231
internally when reading from the directory. Higher values lead to better
12291232
performance but higher memory usage. **Default:** `32`
1233+
* `recursive` {boolean} Resolved `Dir` will be an {AsyncIterable}
1234+
containing all sub files and directories. **Default:** `false`
12301235
* Returns: {Promise} Fulfills with an {fs.Dir}.
12311236
12321237
Asynchronously open a directory for iterative scanning. See the POSIX
@@ -1260,6 +1265,9 @@ closed after the iterator exits.
12601265
<!-- YAML
12611266
added: v10.0.0
12621267
changes:
1268+
- version: REPLACEME
1269+
pr-url: https://github.com/nodejs/node/pull/41439
1270+
description: Added `recursive` option.
12631271
- version: v10.11.0
12641272
pr-url: https://github.com/nodejs/node/pull/22020
12651273
description: New option `withFileTypes` was added.
@@ -1269,6 +1277,7 @@ changes:
12691277
* `options` {string|Object}
12701278
* `encoding` {string} **Default:** `'utf8'`
12711279
* `withFileTypes` {boolean} **Default:** `false`
1280+
* `recursive` {boolean} **Default:** `false`
12721281
* Returns: {Promise} Fulfills with an array of the names of the files in
12731282
the directory excluding `'.'` and `'..'`.
12741283
@@ -3344,6 +3353,9 @@ Functions based on `fs.open()` exhibit this behavior as well:
33443353
<!-- YAML
33453354
added: v12.12.0
33463355
changes:
3356+
- version: REPLACEME
3357+
pr-url: https://github.com/nodejs/node/pull/41439
3358+
description: Added `recursive` option.
33473359
- version: v18.0.0
33483360
pr-url: https://github.com/nodejs/node/pull/41678
33493361
description: Passing an invalid callback to the `callback` argument
@@ -3362,6 +3374,7 @@ changes:
33623374
* `bufferSize` {number} Number of directory entries that are buffered
33633375
internally when reading from the directory. Higher values lead to better
33643376
performance but higher memory usage. **Default:** `32`
3377+
* `recursive` {boolean} **Default:** `false`
33653378
* `callback` {Function}
33663379
* `err` {Error}
33673380
* `dir` {fs.Dir}
@@ -3478,6 +3491,9 @@ above values.
34783491
<!-- YAML
34793492
added: v0.1.8
34803493
changes:
3494+
- version: REPLACEME
3495+
pr-url: https://github.com/nodejs/node/pull/41439
3496+
description: Added `recursive` option.
34813497
- version: v18.0.0
34823498
pr-url: https://github.com/nodejs/node/pull/41678
34833499
description: Passing an invalid callback to the `callback` argument
@@ -3507,6 +3523,7 @@ changes:
35073523
* `options` {string|Object}
35083524
* `encoding` {string} **Default:** `'utf8'`
35093525
* `withFileTypes` {boolean} **Default:** `false`
3526+
* `recursive` {boolean} **Default:** `false`
35103527
* `callback` {Function}
35113528
* `err` {Error}
35123529
* `files` {string\[]|Buffer\[]|fs.Dirent\[]}
@@ -5470,6 +5487,9 @@ object with an `encoding` property specifying the character encoding to use.
54705487
<!-- YAML
54715488
added: v12.12.0
54725489
changes:
5490+
- version: REPLACEME
5491+
pr-url: https://github.com/nodejs/node/pull/41439
5492+
description: Added `recursive` option.
54735493
- version:
54745494
- v13.1.0
54755495
- v12.16.0
@@ -5483,6 +5503,7 @@ changes:
54835503
* `bufferSize` {number} Number of directory entries that are buffered
54845504
internally when reading from the directory. Higher values lead to better
54855505
performance but higher memory usage. **Default:** `32`
5506+
* `recursive` {boolean} **Default:** `false`
54865507
* Returns: {fs.Dir}
54875508
54885509
Synchronously open a directory. See opendir(3).
@@ -5526,6 +5547,9 @@ this API: [`fs.open()`][].
55265547
<!-- YAML
55275548
added: v0.1.21
55285549
changes:
5550+
- version: REPLACEME
5551+
pr-url: https://github.com/nodejs/node/pull/41439
5552+
description: Added `recursive` option.
55295553
- version: v10.10.0
55305554
pr-url: https://github.com/nodejs/node/pull/22020
55315555
description: New option `withFileTypes` was added.
@@ -5539,6 +5563,7 @@ changes:
55395563
* `options` {string|Object}
55405564
* `encoding` {string} **Default:** `'utf8'`
55415565
* `withFileTypes` {boolean} **Default:** `false`
5566+
* `recursive` {boolean} **Default:** `false`
55425567
* Returns: {string\[]|Buffer\[]|fs.Dirent\[]}
55435568
55445569
Reads the contents of the directory.
@@ -6384,6 +6409,16 @@ The file name that this {fs.Dirent} object refers to. The type of this
63846409
value is determined by the `options.encoding` passed to [`fs.readdir()`][] or
63856410
[`fs.readdirSync()`][].
63866411
6412+
#### `dirent.path`
6413+
6414+
<!-- YAML
6415+
added: REPLACEME
6416+
-->
6417+
6418+
* {string}
6419+
6420+
The base path that this {fs.Dirent} object refers to.
6421+
63876422
### Class: `fs.FSWatcher`
63886423
63896424
<!-- YAML

‎lib/fs.js

+47
Original file line numberDiff line numberDiff line change
@@ -1399,6 +1399,36 @@ function mkdirSync(path, options) {
13991399
}
14001400
}
14011401

1402+
// TODO(Ethan-Arrowood): Make this iterative too
1403+
function readdirSyncRecursive(path, origPath, options) {
1404+
nullCheck(path, 'path', true);
1405+
const ctx = { path };
1406+
const result = binding.readdir(pathModule.toNamespacedPath(path),
1407+
options.encoding, !!options.withFileTypes, undefined, ctx);
1408+
handleErrorFromBinding(ctx);
1409+
return options.withFileTypes ?
1410+
getDirents(path, result).flatMap((dirent) => {
1411+
return [
1412+
dirent,
1413+
...(dirent.isDirectory() ?
1414+
readdirSyncRecursive(
1415+
pathModule.join(path, dirent.name),
1416+
origPath,
1417+
options,
1418+
) : []),
1419+
];
1420+
}) :
1421+
result.flatMap((ent) => {
1422+
const innerPath = pathModule.join(path, ent);
1423+
const relativePath = pathModule.relative(origPath, innerPath);
1424+
const stat = binding.internalModuleStat(innerPath);
1425+
return [
1426+
relativePath,
1427+
...(stat === 1 ? readdirSyncRecursive(innerPath, origPath, options) : []),
1428+
];
1429+
});
1430+
}
1431+
14021432
/**
14031433
* Reads the contents of a directory.
14041434
* @param {string | Buffer | URL} path
@@ -1416,6 +1446,14 @@ function readdir(path, options, callback) {
14161446
callback = makeCallback(typeof options === 'function' ? options : callback);
14171447
options = getOptions(options);
14181448
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+
}
14191457

14201458
const req = new FSReqCallback();
14211459
if (!options.withFileTypes) {
@@ -1439,12 +1477,21 @@ function readdir(path, options, callback) {
14391477
* @param {string | {
14401478
* encoding?: string;
14411479
* withFileTypes?: boolean;
1480+
* recursive?: boolean;
14421481
* }} [options]
14431482
* @returns {string | Buffer[] | Dirent[]}
14441483
*/
14451484
function readdirSync(path, options) {
14461485
options = getOptions(options);
14471486
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+
14481495
const ctx = { path };
14491496
const result = binding.readdir(pathModule.toNamespacedPath(path),
14501497
options.encoding, !!options.withFileTypes,

‎lib/internal/fs/dir.js

+77-16
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) {

‎lib/internal/fs/promises.js

+73-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const {
44
ArrayPrototypePush,
5+
ArrayPrototypePop,
56
Error,
67
MathMax,
78
MathMin,
@@ -770,13 +771,81 @@ async function mkdir(path, options) {
770771
kUsePromises);
771772
}
772773

774+
async function readdirRecursive(originalPath, options) {
775+
const result = [];
776+
const queue = [
777+
[
778+
originalPath,
779+
await binding.readdir(
780+
pathModule.toNamespacedPath(originalPath),
781+
options.encoding,
782+
!!options.withFileTypes,
783+
kUsePromises,
784+
),
785+
],
786+
];
787+
788+
789+
if (options.withFileTypes) {
790+
while (queue.length > 0) {
791+
// If we want to implement BFS make this a `shift` call instead of `pop`
792+
const { 0: path, 1: readdir } = ArrayPrototypePop(queue);
793+
for (const dirent of getDirents(path, readdir)) {
794+
ArrayPrototypePush(result, dirent);
795+
if (dirent.isDirectory()) {
796+
const direntPath = pathModule.join(path, dirent.name);
797+
ArrayPrototypePush(queue, [
798+
direntPath,
799+
await binding.readdir(
800+
direntPath,
801+
options.encoding,
802+
true,
803+
kUsePromises,
804+
),
805+
]);
806+
}
807+
}
808+
}
809+
} else {
810+
while (queue.length > 0) {
811+
const { 0: path, 1: readdir } = ArrayPrototypePop(queue);
812+
for (const ent of readdir) {
813+
const direntPath = pathModule.join(path, ent);
814+
const stat = binding.internalModuleStat(direntPath);
815+
ArrayPrototypePush(
816+
result,
817+
pathModule.relative(originalPath, direntPath),
818+
);
819+
if (stat === 1) {
820+
ArrayPrototypePush(queue, [
821+
direntPath,
822+
await binding.readdir(
823+
pathModule.toNamespacedPath(direntPath),
824+
options.encoding,
825+
false,
826+
kUsePromises,
827+
),
828+
]);
829+
}
830+
}
831+
}
832+
}
833+
834+
return result;
835+
}
836+
773837
async function readdir(path, options) {
774838
options = getOptions(options);
775839
path = getValidatedPath(path);
776-
const result = await binding.readdir(pathModule.toNamespacedPath(path),
777-
options.encoding,
778-
!!options.withFileTypes,
779-
kUsePromises);
840+
if (options.recursive) {
841+
return readdirRecursive(path, options);
842+
}
843+
const result = await binding.readdir(
844+
pathModule.toNamespacedPath(path),
845+
options.encoding,
846+
!!options.withFileTypes,
847+
kUsePromises,
848+
);
780849
return options.withFileTypes ?
781850
getDirectoryEntriesPromise(path, result) :
782851
result;

‎lib/internal/fs/utils.js

+14-11
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,9 @@ function assertEncoding(encoding) {
160160
}
161161

162162
class Dirent {
163-
constructor(name, type) {
163+
constructor(name, type, path) {
164164
this.name = name;
165+
this.path = path;
165166
this[kType] = type;
166167
}
167168

@@ -195,8 +196,8 @@ class Dirent {
195196
}
196197

197198
class DirentFromStats extends Dirent {
198-
constructor(name, stats) {
199-
super(name, null);
199+
constructor(name, stats, path) {
200+
super(name, null, path);
200201
this[kStats] = stats;
201202
}
202203
}
@@ -231,7 +232,7 @@ function join(path, name) {
231232
}
232233

233234
if (typeof path === 'string' && typeof name === 'string') {
234-
return pathModule.join(path, name);
235+
return pathModule.basename(path) === name ? path : pathModule.join(path, name);
235236
}
236237

237238
if (isUint8Array(path) && isUint8Array(name)) {
@@ -266,13 +267,13 @@ function getDirents(path, { 0: names, 1: types }, callback) {
266267
callback(err);
267268
return;
268269
}
269-
names[idx] = new DirentFromStats(name, stats);
270+
names[idx] = new DirentFromStats(name, stats, path);
270271
if (--toFinish === 0) {
271272
callback(null, names);
272273
}
273274
});
274275
} else {
275-
names[i] = new Dirent(names[i], types[i]);
276+
names[i] = new Dirent(names[i], types[i], path);
276277
}
277278
}
278279
if (toFinish === 0) {
@@ -302,16 +303,17 @@ function getDirent(path, name, type, callback) {
302303
callback(err);
303304
return;
304305
}
305-
callback(null, new DirentFromStats(name, stats));
306+
callback(null, new DirentFromStats(name, stats, filepath));
306307
});
307308
} else {
308-
callback(null, new Dirent(name, type));
309+
callback(null, new Dirent(name, type, path));
309310
}
310311
} else if (type === UV_DIRENT_UNKNOWN) {
311-
const stats = lazyLoadFs().lstatSync(join(path, name));
312-
return new DirentFromStats(name, stats);
312+
const filepath = join(path, name);
313+
const stats = lazyLoadFs().lstatSync(filepath);
314+
return new DirentFromStats(name, stats, path);
313315
} else {
314-
return new Dirent(name, type);
316+
return new Dirent(name, type, path);
315317
}
316318
}
317319

@@ -334,6 +336,7 @@ function getOptions(options, defaultOptions = kEmptyObject) {
334336
if (options.signal !== undefined) {
335337
validateAbortSignal(options.signal, 'options.signal');
336338
}
339+
337340
return options;
338341
}
339342

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const fs = require('fs');
6+
const fsPromises = fs.promises;
7+
const pathModule = require('path');
8+
const tmpdir = require('../common/tmpdir');
9+
10+
const testDir = tmpdir.path;
11+
12+
const fileStructure = [
13+
[ 'a', [ 'foo', 'bar' ] ],
14+
[ 'b', [ 'foo', 'bar' ] ],
15+
[ 'c', [ 'foo', 'bar' ] ],
16+
[ 'd', [ 'foo', 'bar' ] ],
17+
[ 'e', [ 'foo', 'bar' ] ],
18+
[ 'f', [ 'foo', 'bar' ] ],
19+
[ 'g', [ 'foo', 'bar' ] ],
20+
[ 'h', [ 'foo', 'bar' ] ],
21+
[ 'i', [ 'foo', 'bar' ] ],
22+
[ 'j', [ 'foo', 'bar' ] ],
23+
[ 'k', [ 'foo', 'bar' ] ],
24+
[ 'l', [ 'foo', 'bar' ] ],
25+
[ 'm', [ 'foo', 'bar' ] ],
26+
[ 'n', [ 'foo', 'bar' ] ],
27+
[ 'o', [ 'foo', 'bar' ] ],
28+
[ 'p', [ 'foo', 'bar' ] ],
29+
[ 'q', [ 'foo', 'bar' ] ],
30+
[ 'r', [ 'foo', 'bar' ] ],
31+
[ 's', [ 'foo', 'bar' ] ],
32+
[ 't', [ 'foo', 'bar' ] ],
33+
[ 'u', [ 'foo', 'bar' ] ],
34+
[ 'v', [ 'foo', 'bar' ] ],
35+
[ 'w', [ 'foo', 'bar' ] ],
36+
[ 'x', [ 'foo', 'bar' ] ],
37+
[ 'y', [ 'foo', 'bar' ] ],
38+
[ 'z', [ 'foo', 'bar' ] ],
39+
[ 'aa', [ 'foo', 'bar' ] ],
40+
[ 'bb', [ 'foo', 'bar' ] ],
41+
[ 'cc', [ 'foo', 'bar' ] ],
42+
[ 'dd', [ 'foo', 'bar' ] ],
43+
[ 'ee', [ 'foo', 'bar' ] ],
44+
[ 'ff', [ 'foo', 'bar' ] ],
45+
[ 'gg', [ 'foo', 'bar' ] ],
46+
[ 'hh', [ 'foo', 'bar' ] ],
47+
[ 'ii', [ 'foo', 'bar' ] ],
48+
[ 'jj', [ 'foo', 'bar' ] ],
49+
[ 'kk', [ 'foo', 'bar' ] ],
50+
[ 'll', [ 'foo', 'bar' ] ],
51+
[ 'mm', [ 'foo', 'bar' ] ],
52+
[ 'nn', [ 'foo', 'bar' ] ],
53+
[ 'oo', [ 'foo', 'bar' ] ],
54+
[ 'pp', [ 'foo', 'bar' ] ],
55+
[ 'qq', [ 'foo', 'bar' ] ],
56+
[ 'rr', [ 'foo', 'bar' ] ],
57+
[ 'ss', [ 'foo', 'bar' ] ],
58+
[ 'tt', [ 'foo', 'bar' ] ],
59+
[ 'uu', [ 'foo', 'bar' ] ],
60+
[ 'vv', [ 'foo', 'bar' ] ],
61+
[ 'ww', [ 'foo', 'bar' ] ],
62+
[ 'xx', [ 'foo', 'bar' ] ],
63+
[ 'yy', [ 'foo', 'bar' ] ],
64+
[ 'zz', [ 'foo', 'bar' ] ],
65+
[ 'abc', [ ['def', [ 'foo', 'bar' ] ], ['ghi', [ 'foo', 'bar' ] ] ] ],
66+
];
67+
68+
function createFiles(path, fileStructure) {
69+
for (const fileOrDir of fileStructure) {
70+
if (typeof fileOrDir === 'string') {
71+
fs.writeFileSync(pathModule.join(path, fileOrDir), '');
72+
} else {
73+
const dirPath = pathModule.join(path, fileOrDir[0]);
74+
fs.mkdirSync(dirPath);
75+
createFiles(dirPath, fileOrDir[1]);
76+
}
77+
}
78+
}
79+
80+
// Make sure tmp directory is clean
81+
tmpdir.refresh();
82+
83+
createFiles(testDir, fileStructure);
84+
const symlinksRootPath = pathModule.join(testDir, 'symlinks');
85+
const symlinkTargetFile = pathModule.join(symlinksRootPath, 'symlink-target-file');
86+
const symlinkTargetDir = pathModule.join(symlinksRootPath, 'symlink-target-dir');
87+
fs.mkdirSync(symlinksRootPath);
88+
fs.writeFileSync(symlinkTargetFile, '');
89+
fs.mkdirSync(symlinkTargetDir);
90+
fs.symlinkSync(symlinkTargetFile, pathModule.join(symlinksRootPath, 'symlink-src-file'));
91+
fs.symlinkSync(symlinkTargetDir, pathModule.join(symlinksRootPath, 'symlink-src-dir'));
92+
93+
const expected = [
94+
'a', 'a/bar', 'a/foo', 'aa', 'aa/bar', 'aa/foo',
95+
'abc', 'abc/def', 'abc/def/bar', 'abc/def/foo', 'abc/ghi', 'abc/ghi/bar', 'abc/ghi/foo',
96+
'b', 'b/bar', 'b/foo', 'bb', 'bb/bar', 'bb/foo',
97+
'c', 'c/bar', 'c/foo', 'cc', 'cc/bar', 'cc/foo',
98+
'd', 'd/bar', 'd/foo', 'dd', 'dd/bar', 'dd/foo',
99+
'e', 'e/bar', 'e/foo', 'ee', 'ee/bar', 'ee/foo',
100+
'f', 'f/bar', 'f/foo', 'ff', 'ff/bar', 'ff/foo',
101+
'g', 'g/bar', 'g/foo', 'gg', 'gg/bar', 'gg/foo',
102+
'h', 'h/bar', 'h/foo', 'hh', 'hh/bar', 'hh/foo',
103+
'i', 'i/bar', 'i/foo', 'ii', 'ii/bar', 'ii/foo',
104+
'j', 'j/bar', 'j/foo', 'jj', 'jj/bar', 'jj/foo',
105+
'k', 'k/bar', 'k/foo', 'kk', 'kk/bar', 'kk/foo',
106+
'l', 'l/bar', 'l/foo', 'll', 'll/bar', 'll/foo',
107+
'm', 'm/bar', 'm/foo', 'mm', 'mm/bar', 'mm/foo',
108+
'n', 'n/bar', 'n/foo', 'nn', 'nn/bar', 'nn/foo',
109+
'o', 'o/bar', 'o/foo', 'oo', 'oo/bar', 'oo/foo',
110+
'p', 'p/bar', 'p/foo', 'pp', 'pp/bar', 'pp/foo',
111+
'q', 'q/bar', 'q/foo', 'qq', 'qq/bar', 'qq/foo',
112+
'r', 'r/bar', 'r/foo', 'rr', 'rr/bar', 'rr/foo',
113+
's', 's/bar', 's/foo', 'ss', 'ss/bar', 'ss/foo',
114+
'symlinks', 'symlinks/symlink-src-dir', 'symlinks/symlink-src-file',
115+
'symlinks/symlink-target-dir', 'symlinks/symlink-target-file',
116+
't', 't/bar', 't/foo', 'tt', 'tt/bar', 'tt/foo',
117+
'u', 'u/bar', 'u/foo', 'uu', 'uu/bar', 'uu/foo',
118+
'v', 'v/bar', 'v/foo', 'vv', 'vv/bar', 'vv/foo',
119+
'w', 'w/bar', 'w/foo', 'ww', 'ww/bar', 'ww/foo',
120+
'x', 'x/bar', 'x/foo', 'xx', 'xx/bar', 'xx/foo',
121+
'y', 'y/bar', 'y/foo', 'yy', 'yy/bar', 'yy/foo',
122+
'z', 'z/bar', 'z/foo', 'zz', 'zz/bar', 'zz/foo',
123+
];
124+
125+
// Normalize paths once for non POSIX platforms
126+
for (let i = 0; i < expected.length; i++) {
127+
expected[i] = pathModule.normalize(expected[i]);
128+
}
129+
130+
function getDirentPath(dirent) {
131+
return pathModule.relative(testDir, dirent.path);
132+
}
133+
134+
function assertDirents(dirents) {
135+
dirents.sort((a, b) => (getDirentPath(a) < getDirentPath(b) ? -1 : 1));
136+
for (const [i, dirent] of dirents.entries()) {
137+
assert(dirent instanceof fs.Dirent);
138+
assert.strictEqual(getDirentPath(dirent), expected[i]);
139+
}
140+
}
141+
142+
function processDirSync(dir) {
143+
const dirents = [];
144+
let dirent = dir.readSync();
145+
while (dirent !== null) {
146+
dirents.push(dirent);
147+
dirent = dir.readSync();
148+
}
149+
assertDirents(dirents);
150+
}
151+
152+
// Opendir read results sync
153+
154+
{
155+
const dir = fs.opendirSync(testDir, { recursive: true });
156+
processDirSync(dir);
157+
dir.closeSync();
158+
}
159+
160+
{
161+
fs.opendir(testDir, { recursive: true }, common.mustSucceed((dir) => {
162+
processDirSync(dir);
163+
dir.close(common.mustSucceed());
164+
}));
165+
}
166+
167+
// Opendir read result using callback
168+
169+
function processDirCb(dir, cb) {
170+
const acc = [];
171+
172+
function _process(dir, acc, cb) {
173+
dir.read((err, dirent) => {
174+
if (err) {
175+
return cb(err);
176+
}
177+
178+
if (dirent !== null) {
179+
acc.push(dirent);
180+
_process(dir, acc, cb);
181+
} else {
182+
cb(null, acc);
183+
}
184+
});
185+
}
186+
187+
_process(dir, acc, cb);
188+
}
189+
190+
{
191+
const dir = fs.opendirSync(testDir, { recursive: true });
192+
processDirCb(dir, common.mustSucceed((dirents) => {
193+
assertDirents(dirents);
194+
dir.close(common.mustSucceed());
195+
}));
196+
}
197+
198+
{
199+
fs.opendir(testDir, { recursive: true }, common.mustSucceed((dir) => {
200+
processDirCb(dir, common.mustSucceed((dirents) => {
201+
assertDirents(dirents);
202+
dir.close(common.mustSucceed());
203+
}));
204+
}));
205+
}
206+
207+
// Opendir read result using AsyncIterator
208+
209+
{
210+
async function test() {
211+
const dir = await fsPromises.opendir(testDir, { recursive: true });
212+
const dirents = [];
213+
for await (const dirent of dir) {
214+
dirents.push(dirent);
215+
}
216+
assertDirents(dirents);
217+
}
218+
219+
test().then(common.mustCall());
220+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const fs = require('fs');
6+
const pathModule = require('path');
7+
const tmpdir = require('../common/tmpdir');
8+
9+
const readdirDir = tmpdir.path;
10+
11+
const fileStructure = [
12+
[ 'a', [ 'foo', 'bar' ] ],
13+
[ 'b', [ 'foo', 'bar' ] ],
14+
[ 'c', [ 'foo', 'bar' ] ],
15+
[ 'd', [ 'foo', 'bar' ] ],
16+
[ 'e', [ 'foo', 'bar' ] ],
17+
[ 'f', [ 'foo', 'bar' ] ],
18+
[ 'g', [ 'foo', 'bar' ] ],
19+
[ 'h', [ 'foo', 'bar' ] ],
20+
[ 'i', [ 'foo', 'bar' ] ],
21+
[ 'j', [ 'foo', 'bar' ] ],
22+
[ 'k', [ 'foo', 'bar' ] ],
23+
[ 'l', [ 'foo', 'bar' ] ],
24+
[ 'm', [ 'foo', 'bar' ] ],
25+
[ 'n', [ 'foo', 'bar' ] ],
26+
[ 'o', [ 'foo', 'bar' ] ],
27+
[ 'p', [ 'foo', 'bar' ] ],
28+
[ 'q', [ 'foo', 'bar' ] ],
29+
[ 'r', [ 'foo', 'bar' ] ],
30+
[ 's', [ 'foo', 'bar' ] ],
31+
[ 't', [ 'foo', 'bar' ] ],
32+
[ 'u', [ 'foo', 'bar' ] ],
33+
[ 'v', [ 'foo', 'bar' ] ],
34+
[ 'w', [ 'foo', 'bar' ] ],
35+
[ 'x', [ 'foo', 'bar' ] ],
36+
[ 'y', [ 'foo', 'bar' ] ],
37+
[ 'z', [ 'foo', 'bar' ] ],
38+
[ 'aa', [ 'foo', 'bar' ] ],
39+
[ 'bb', [ 'foo', 'bar' ] ],
40+
[ 'cc', [ 'foo', 'bar' ] ],
41+
[ 'dd', [ 'foo', 'bar' ] ],
42+
[ 'ee', [ 'foo', 'bar' ] ],
43+
[ 'ff', [ 'foo', 'bar' ] ],
44+
[ 'gg', [ 'foo', 'bar' ] ],
45+
[ 'hh', [ 'foo', 'bar' ] ],
46+
[ 'ii', [ 'foo', 'bar' ] ],
47+
[ 'jj', [ 'foo', 'bar' ] ],
48+
[ 'kk', [ 'foo', 'bar' ] ],
49+
[ 'll', [ 'foo', 'bar' ] ],
50+
[ 'mm', [ 'foo', 'bar' ] ],
51+
[ 'nn', [ 'foo', 'bar' ] ],
52+
[ 'oo', [ 'foo', 'bar' ] ],
53+
[ 'pp', [ 'foo', 'bar' ] ],
54+
[ 'qq', [ 'foo', 'bar' ] ],
55+
[ 'rr', [ 'foo', 'bar' ] ],
56+
[ 'ss', [ 'foo', 'bar' ] ],
57+
[ 'tt', [ 'foo', 'bar' ] ],
58+
[ 'uu', [ 'foo', 'bar' ] ],
59+
[ 'vv', [ 'foo', 'bar' ] ],
60+
[ 'ww', [ 'foo', 'bar' ] ],
61+
[ 'xx', [ 'foo', 'bar' ] ],
62+
[ 'yy', [ 'foo', 'bar' ] ],
63+
[ 'zz', [ 'foo', 'bar' ] ],
64+
[ 'abc', [ ['def', [ 'foo', 'bar' ] ], ['ghi', [ 'foo', 'bar' ] ] ] ],
65+
];
66+
67+
function createFiles(path, fileStructure) {
68+
for (const fileOrDir of fileStructure) {
69+
if (typeof fileOrDir === 'string') {
70+
fs.writeFileSync(pathModule.join(path, fileOrDir), '');
71+
} else {
72+
const dirPath = pathModule.join(path, fileOrDir[0]);
73+
fs.mkdirSync(dirPath);
74+
createFiles(dirPath, fileOrDir[1]);
75+
}
76+
}
77+
}
78+
79+
// Make sure tmp directory is clean
80+
tmpdir.refresh();
81+
82+
createFiles(readdirDir, fileStructure);
83+
const symlinksRootPath = pathModule.join(readdirDir, 'symlinks');
84+
const symlinkTargetFile = pathModule.join(symlinksRootPath, 'symlink-target-file');
85+
const symlinkTargetDir = pathModule.join(symlinksRootPath, 'symlink-target-dir');
86+
fs.mkdirSync(symlinksRootPath);
87+
fs.writeFileSync(symlinkTargetFile, '');
88+
fs.mkdirSync(symlinkTargetDir);
89+
fs.symlinkSync(symlinkTargetFile, pathModule.join(symlinksRootPath, 'symlink-src-file'));
90+
fs.symlinkSync(symlinkTargetDir, pathModule.join(symlinksRootPath, 'symlink-src-dir'));
91+
92+
const expected = [
93+
'a', 'a/bar', 'a/foo', 'aa', 'aa/bar', 'aa/foo',
94+
'abc', 'abc/def', 'abc/def/bar', 'abc/def/foo', 'abc/ghi', 'abc/ghi/bar', 'abc/ghi/foo',
95+
'b', 'b/bar', 'b/foo', 'bb', 'bb/bar', 'bb/foo',
96+
'c', 'c/bar', 'c/foo', 'cc', 'cc/bar', 'cc/foo',
97+
'd', 'd/bar', 'd/foo', 'dd', 'dd/bar', 'dd/foo',
98+
'e', 'e/bar', 'e/foo', 'ee', 'ee/bar', 'ee/foo',
99+
'f', 'f/bar', 'f/foo', 'ff', 'ff/bar', 'ff/foo',
100+
'g', 'g/bar', 'g/foo', 'gg', 'gg/bar', 'gg/foo',
101+
'h', 'h/bar', 'h/foo', 'hh', 'hh/bar', 'hh/foo',
102+
'i', 'i/bar', 'i/foo', 'ii', 'ii/bar', 'ii/foo',
103+
'j', 'j/bar', 'j/foo', 'jj', 'jj/bar', 'jj/foo',
104+
'k', 'k/bar', 'k/foo', 'kk', 'kk/bar', 'kk/foo',
105+
'l', 'l/bar', 'l/foo', 'll', 'll/bar', 'll/foo',
106+
'm', 'm/bar', 'm/foo', 'mm', 'mm/bar', 'mm/foo',
107+
'n', 'n/bar', 'n/foo', 'nn', 'nn/bar', 'nn/foo',
108+
'o', 'o/bar', 'o/foo', 'oo', 'oo/bar', 'oo/foo',
109+
'p', 'p/bar', 'p/foo', 'pp', 'pp/bar', 'pp/foo',
110+
'q', 'q/bar', 'q/foo', 'qq', 'qq/bar', 'qq/foo',
111+
'r', 'r/bar', 'r/foo', 'rr', 'rr/bar', 'rr/foo',
112+
's', 's/bar', 's/foo', 'ss', 'ss/bar', 'ss/foo',
113+
'symlinks', 'symlinks/symlink-src-dir', 'symlinks/symlink-src-file',
114+
'symlinks/symlink-target-dir', 'symlinks/symlink-target-file',
115+
't', 't/bar', 't/foo', 'tt', 'tt/bar', 'tt/foo',
116+
'u', 'u/bar', 'u/foo', 'uu', 'uu/bar', 'uu/foo',
117+
'v', 'v/bar', 'v/foo', 'vv', 'vv/bar', 'vv/foo',
118+
'w', 'w/bar', 'w/foo', 'ww', 'ww/bar', 'ww/foo',
119+
'x', 'x/bar', 'x/foo', 'xx', 'xx/bar', 'xx/foo',
120+
'y', 'y/bar', 'y/foo', 'yy', 'yy/bar', 'yy/foo',
121+
'z', 'z/bar', 'z/foo', 'zz', 'zz/bar', 'zz/foo',
122+
];
123+
124+
// Normalize paths once for non POSIX platforms
125+
for (let i = 0; i < expected.length; i++) {
126+
expected[i] = pathModule.normalize(expected[i]);
127+
}
128+
129+
function getDirentPath(dirent) {
130+
return pathModule.relative(readdirDir, pathModule.join(dirent.path, dirent.name));
131+
}
132+
133+
function assertDirents(dirents) {
134+
dirents.sort((a, b) => (getDirentPath(a) < getDirentPath(b) ? -1 : 1));
135+
for (const [i, dirent] of dirents.entries()) {
136+
assert(dirent instanceof fs.Dirent);
137+
assert.strictEqual(getDirentPath(dirent), expected[i]);
138+
}
139+
}
140+
141+
// readdirSync
142+
143+
// readdirSync { recursive }
144+
{
145+
const result = fs.readdirSync(readdirDir, { recursive: true });
146+
assert.deepStrictEqual(result.sort(), expected);
147+
}
148+
149+
// readdirSync { recursive, withFileTypes }
150+
{
151+
const result = fs.readdirSync(readdirDir, { recursive: true, withFileTypes: true });
152+
assertDirents(result);
153+
}
154+
155+
// readdir
156+
157+
// readdir { recursive } callback
158+
{
159+
fs.readdir(readdirDir, { recursive: true },
160+
common.mustSucceed((result) => {
161+
assert.deepStrictEqual(result.sort(), expected);
162+
}));
163+
}
164+
165+
// Readdir { recursive, withFileTypes } callback
166+
{
167+
fs.readdir(readdirDir, { recursive: true, withFileTypes: true },
168+
common.mustSucceed((result) => {
169+
assertDirents(result);
170+
}));
171+
}
172+
173+
// fs.promises.readdir
174+
175+
// fs.promises.readdir { recursive }
176+
{
177+
async function test() {
178+
const result = await fs.promises.readdir(readdirDir, { recursive: true });
179+
assert.deepStrictEqual(result.sort(), expected);
180+
}
181+
182+
test().then(common.mustCall());
183+
}
184+
185+
// fs.promises.readdir { recursive, withFileTypes }
186+
{
187+
async function test() {
188+
const result = await fs.promises.readdir(readdirDir, { recursive: true, withFileTypes: true });
189+
assertDirents(result);
190+
}
191+
192+
test().then(common.mustCall());
193+
}

0 commit comments

Comments
 (0)
Please sign in to comment.