mirror of
https://github.com/nodejs/node.git
synced 2025-12-28 16:07:39 +00:00
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: https://github.com/nodejs/node/issues/34992 PR-URL: https://github.com/nodejs/node/pull/41439 Refs: https://github.com/nodejs/tooling/issues/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>
This commit is contained in:
parent
6675505686
commit
7b39e8099a
@ -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
|
||||
@ -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
|
||||
@ -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.
|
||||
@ -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 `'..'`.
|
||||
|
||||
@ -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
|
||||
@ -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}
|
||||
@ -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
|
||||
@ -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\[]}
|
||||
@ -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
|
||||
@ -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).
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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
|
||||
|
||||
47
lib/fs.js
47
lib/fs.js
@ -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
|
||||
@ -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) {
|
||||
@ -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,
|
||||
|
||||
@ -2,8 +2,7 @@
|
||||
|
||||
const {
|
||||
ArrayPrototypePush,
|
||||
ArrayPrototypeSlice,
|
||||
ArrayPrototypeSplice,
|
||||
ArrayPrototypeShift,
|
||||
FunctionPrototypeBind,
|
||||
ObjectDefineProperty,
|
||||
PromiseReject,
|
||||
@ -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();
|
||||
@ -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] = [];
|
||||
@ -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();
|
||||
@ -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] };
|
||||
@ -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) {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
const {
|
||||
ArrayPrototypePush,
|
||||
ArrayPrototypePop,
|
||||
Error,
|
||||
MathMax,
|
||||
MathMin,
|
||||
@ -770,13 +771,81 @@ async function mkdir(path, options) {
|
||||
kUsePromises);
|
||||
}
|
||||
|
||||
async function readdirRecursive(originalPath, options) {
|
||||
const result = [];
|
||||
const queue = [
|
||||
[
|
||||
originalPath,
|
||||
await binding.readdir(
|
||||
pathModule.toNamespacedPath(originalPath),
|
||||
options.encoding,
|
||||
!!options.withFileTypes,
|
||||
kUsePromises,
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
if (options.withFileTypes) {
|
||||
while (queue.length > 0) {
|
||||
// If we want to implement BFS make this a `shift` call instead of `pop`
|
||||
const { 0: path, 1: readdir } = ArrayPrototypePop(queue);
|
||||
for (const dirent of getDirents(path, readdir)) {
|
||||
ArrayPrototypePush(result, dirent);
|
||||
if (dirent.isDirectory()) {
|
||||
const direntPath = pathModule.join(path, dirent.name);
|
||||
ArrayPrototypePush(queue, [
|
||||
direntPath,
|
||||
await binding.readdir(
|
||||
direntPath,
|
||||
options.encoding,
|
||||
true,
|
||||
kUsePromises,
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (queue.length > 0) {
|
||||
const { 0: path, 1: readdir } = ArrayPrototypePop(queue);
|
||||
for (const ent of readdir) {
|
||||
const direntPath = pathModule.join(path, ent);
|
||||
const stat = binding.internalModuleStat(direntPath);
|
||||
ArrayPrototypePush(
|
||||
result,
|
||||
pathModule.relative(originalPath, direntPath),
|
||||
);
|
||||
if (stat === 1) {
|
||||
ArrayPrototypePush(queue, [
|
||||
direntPath,
|
||||
await binding.readdir(
|
||||
pathModule.toNamespacedPath(direntPath),
|
||||
options.encoding,
|
||||
false,
|
||||
kUsePromises,
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function readdir(path, options) {
|
||||
options = getOptions(options);
|
||||
path = getValidatedPath(path);
|
||||
const result = await binding.readdir(pathModule.toNamespacedPath(path),
|
||||
options.encoding,
|
||||
!!options.withFileTypes,
|
||||
kUsePromises);
|
||||
if (options.recursive) {
|
||||
return readdirRecursive(path, options);
|
||||
}
|
||||
const result = await binding.readdir(
|
||||
pathModule.toNamespacedPath(path),
|
||||
options.encoding,
|
||||
!!options.withFileTypes,
|
||||
kUsePromises,
|
||||
);
|
||||
return options.withFileTypes ?
|
||||
getDirectoryEntriesPromise(path, result) :
|
||||
result;
|
||||
|
||||
@ -161,8 +161,9 @@ function assertEncoding(encoding) {
|
||||
}
|
||||
|
||||
class Dirent {
|
||||
constructor(name, type) {
|
||||
constructor(name, type, path) {
|
||||
this.name = name;
|
||||
this.path = path;
|
||||
this[kType] = type;
|
||||
}
|
||||
|
||||
@ -196,8 +197,8 @@ class Dirent {
|
||||
}
|
||||
|
||||
class DirentFromStats extends Dirent {
|
||||
constructor(name, stats) {
|
||||
super(name, null);
|
||||
constructor(name, stats, path) {
|
||||
super(name, null, path);
|
||||
this[kStats] = stats;
|
||||
}
|
||||
}
|
||||
@ -232,7 +233,7 @@ function join(path, name) {
|
||||
}
|
||||
|
||||
if (typeof path === 'string' && typeof name === 'string') {
|
||||
return pathModule.join(path, name);
|
||||
return pathModule.basename(path) === name ? path : pathModule.join(path, name);
|
||||
}
|
||||
|
||||
if (isUint8Array(path) && isUint8Array(name)) {
|
||||
@ -267,13 +268,13 @@ function getDirents(path, { 0: names, 1: types }, callback) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
names[idx] = new DirentFromStats(name, stats);
|
||||
names[idx] = new DirentFromStats(name, stats, path);
|
||||
if (--toFinish === 0) {
|
||||
callback(null, names);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
names[i] = new Dirent(names[i], types[i]);
|
||||
names[i] = new Dirent(names[i], types[i], path);
|
||||
}
|
||||
}
|
||||
if (toFinish === 0) {
|
||||
@ -303,16 +304,17 @@ function getDirent(path, name, type, callback) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
callback(null, new DirentFromStats(name, stats));
|
||||
callback(null, new DirentFromStats(name, stats, filepath));
|
||||
});
|
||||
} else {
|
||||
callback(null, new Dirent(name, type));
|
||||
callback(null, new Dirent(name, type, path));
|
||||
}
|
||||
} else if (type === UV_DIRENT_UNKNOWN) {
|
||||
const stats = lazyLoadFs().lstatSync(join(path, name));
|
||||
return new DirentFromStats(name, stats);
|
||||
const filepath = join(path, name);
|
||||
const stats = lazyLoadFs().lstatSync(filepath);
|
||||
return new DirentFromStats(name, stats, path);
|
||||
} else {
|
||||
return new Dirent(name, type);
|
||||
return new Dirent(name, type, path);
|
||||
}
|
||||
}
|
||||
|
||||
@ -335,6 +337,7 @@ function getOptions(options, defaultOptions = kEmptyObject) {
|
||||
if (options.signal !== undefined) {
|
||||
validateAbortSignal(options.signal, 'options.signal');
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
220
test/sequential/test-fs-opendir-recursive.js
Normal file
220
test/sequential/test-fs-opendir-recursive.js
Normal file
@ -0,0 +1,220 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const fsPromises = fs.promises;
|
||||
const pathModule = require('path');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
|
||||
const testDir = tmpdir.path;
|
||||
|
||||
const fileStructure = [
|
||||
[ 'a', [ 'foo', 'bar' ] ],
|
||||
[ 'b', [ 'foo', 'bar' ] ],
|
||||
[ 'c', [ 'foo', 'bar' ] ],
|
||||
[ 'd', [ 'foo', 'bar' ] ],
|
||||
[ 'e', [ 'foo', 'bar' ] ],
|
||||
[ 'f', [ 'foo', 'bar' ] ],
|
||||
[ 'g', [ 'foo', 'bar' ] ],
|
||||
[ 'h', [ 'foo', 'bar' ] ],
|
||||
[ 'i', [ 'foo', 'bar' ] ],
|
||||
[ 'j', [ 'foo', 'bar' ] ],
|
||||
[ 'k', [ 'foo', 'bar' ] ],
|
||||
[ 'l', [ 'foo', 'bar' ] ],
|
||||
[ 'm', [ 'foo', 'bar' ] ],
|
||||
[ 'n', [ 'foo', 'bar' ] ],
|
||||
[ 'o', [ 'foo', 'bar' ] ],
|
||||
[ 'p', [ 'foo', 'bar' ] ],
|
||||
[ 'q', [ 'foo', 'bar' ] ],
|
||||
[ 'r', [ 'foo', 'bar' ] ],
|
||||
[ 's', [ 'foo', 'bar' ] ],
|
||||
[ 't', [ 'foo', 'bar' ] ],
|
||||
[ 'u', [ 'foo', 'bar' ] ],
|
||||
[ 'v', [ 'foo', 'bar' ] ],
|
||||
[ 'w', [ 'foo', 'bar' ] ],
|
||||
[ 'x', [ 'foo', 'bar' ] ],
|
||||
[ 'y', [ 'foo', 'bar' ] ],
|
||||
[ 'z', [ 'foo', 'bar' ] ],
|
||||
[ 'aa', [ 'foo', 'bar' ] ],
|
||||
[ 'bb', [ 'foo', 'bar' ] ],
|
||||
[ 'cc', [ 'foo', 'bar' ] ],
|
||||
[ 'dd', [ 'foo', 'bar' ] ],
|
||||
[ 'ee', [ 'foo', 'bar' ] ],
|
||||
[ 'ff', [ 'foo', 'bar' ] ],
|
||||
[ 'gg', [ 'foo', 'bar' ] ],
|
||||
[ 'hh', [ 'foo', 'bar' ] ],
|
||||
[ 'ii', [ 'foo', 'bar' ] ],
|
||||
[ 'jj', [ 'foo', 'bar' ] ],
|
||||
[ 'kk', [ 'foo', 'bar' ] ],
|
||||
[ 'll', [ 'foo', 'bar' ] ],
|
||||
[ 'mm', [ 'foo', 'bar' ] ],
|
||||
[ 'nn', [ 'foo', 'bar' ] ],
|
||||
[ 'oo', [ 'foo', 'bar' ] ],
|
||||
[ 'pp', [ 'foo', 'bar' ] ],
|
||||
[ 'qq', [ 'foo', 'bar' ] ],
|
||||
[ 'rr', [ 'foo', 'bar' ] ],
|
||||
[ 'ss', [ 'foo', 'bar' ] ],
|
||||
[ 'tt', [ 'foo', 'bar' ] ],
|
||||
[ 'uu', [ 'foo', 'bar' ] ],
|
||||
[ 'vv', [ 'foo', 'bar' ] ],
|
||||
[ 'ww', [ 'foo', 'bar' ] ],
|
||||
[ 'xx', [ 'foo', 'bar' ] ],
|
||||
[ 'yy', [ 'foo', 'bar' ] ],
|
||||
[ 'zz', [ 'foo', 'bar' ] ],
|
||||
[ 'abc', [ ['def', [ 'foo', 'bar' ] ], ['ghi', [ 'foo', 'bar' ] ] ] ],
|
||||
];
|
||||
|
||||
function createFiles(path, fileStructure) {
|
||||
for (const fileOrDir of fileStructure) {
|
||||
if (typeof fileOrDir === 'string') {
|
||||
fs.writeFileSync(pathModule.join(path, fileOrDir), '');
|
||||
} else {
|
||||
const dirPath = pathModule.join(path, fileOrDir[0]);
|
||||
fs.mkdirSync(dirPath);
|
||||
createFiles(dirPath, fileOrDir[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure tmp directory is clean
|
||||
tmpdir.refresh();
|
||||
|
||||
createFiles(testDir, fileStructure);
|
||||
const symlinksRootPath = pathModule.join(testDir, 'symlinks');
|
||||
const symlinkTargetFile = pathModule.join(symlinksRootPath, 'symlink-target-file');
|
||||
const symlinkTargetDir = pathModule.join(symlinksRootPath, 'symlink-target-dir');
|
||||
fs.mkdirSync(symlinksRootPath);
|
||||
fs.writeFileSync(symlinkTargetFile, '');
|
||||
fs.mkdirSync(symlinkTargetDir);
|
||||
fs.symlinkSync(symlinkTargetFile, pathModule.join(symlinksRootPath, 'symlink-src-file'));
|
||||
fs.symlinkSync(symlinkTargetDir, pathModule.join(symlinksRootPath, 'symlink-src-dir'));
|
||||
|
||||
const expected = [
|
||||
'a', 'a/bar', 'a/foo', 'aa', 'aa/bar', 'aa/foo',
|
||||
'abc', 'abc/def', 'abc/def/bar', 'abc/def/foo', 'abc/ghi', 'abc/ghi/bar', 'abc/ghi/foo',
|
||||
'b', 'b/bar', 'b/foo', 'bb', 'bb/bar', 'bb/foo',
|
||||
'c', 'c/bar', 'c/foo', 'cc', 'cc/bar', 'cc/foo',
|
||||
'd', 'd/bar', 'd/foo', 'dd', 'dd/bar', 'dd/foo',
|
||||
'e', 'e/bar', 'e/foo', 'ee', 'ee/bar', 'ee/foo',
|
||||
'f', 'f/bar', 'f/foo', 'ff', 'ff/bar', 'ff/foo',
|
||||
'g', 'g/bar', 'g/foo', 'gg', 'gg/bar', 'gg/foo',
|
||||
'h', 'h/bar', 'h/foo', 'hh', 'hh/bar', 'hh/foo',
|
||||
'i', 'i/bar', 'i/foo', 'ii', 'ii/bar', 'ii/foo',
|
||||
'j', 'j/bar', 'j/foo', 'jj', 'jj/bar', 'jj/foo',
|
||||
'k', 'k/bar', 'k/foo', 'kk', 'kk/bar', 'kk/foo',
|
||||
'l', 'l/bar', 'l/foo', 'll', 'll/bar', 'll/foo',
|
||||
'm', 'm/bar', 'm/foo', 'mm', 'mm/bar', 'mm/foo',
|
||||
'n', 'n/bar', 'n/foo', 'nn', 'nn/bar', 'nn/foo',
|
||||
'o', 'o/bar', 'o/foo', 'oo', 'oo/bar', 'oo/foo',
|
||||
'p', 'p/bar', 'p/foo', 'pp', 'pp/bar', 'pp/foo',
|
||||
'q', 'q/bar', 'q/foo', 'qq', 'qq/bar', 'qq/foo',
|
||||
'r', 'r/bar', 'r/foo', 'rr', 'rr/bar', 'rr/foo',
|
||||
's', 's/bar', 's/foo', 'ss', 'ss/bar', 'ss/foo',
|
||||
'symlinks', 'symlinks/symlink-src-dir', 'symlinks/symlink-src-file',
|
||||
'symlinks/symlink-target-dir', 'symlinks/symlink-target-file',
|
||||
't', 't/bar', 't/foo', 'tt', 'tt/bar', 'tt/foo',
|
||||
'u', 'u/bar', 'u/foo', 'uu', 'uu/bar', 'uu/foo',
|
||||
'v', 'v/bar', 'v/foo', 'vv', 'vv/bar', 'vv/foo',
|
||||
'w', 'w/bar', 'w/foo', 'ww', 'ww/bar', 'ww/foo',
|
||||
'x', 'x/bar', 'x/foo', 'xx', 'xx/bar', 'xx/foo',
|
||||
'y', 'y/bar', 'y/foo', 'yy', 'yy/bar', 'yy/foo',
|
||||
'z', 'z/bar', 'z/foo', 'zz', 'zz/bar', 'zz/foo',
|
||||
];
|
||||
|
||||
// Normalize paths once for non POSIX platforms
|
||||
for (let i = 0; i < expected.length; i++) {
|
||||
expected[i] = pathModule.normalize(expected[i]);
|
||||
}
|
||||
|
||||
function getDirentPath(dirent) {
|
||||
return pathModule.relative(testDir, dirent.path);
|
||||
}
|
||||
|
||||
function assertDirents(dirents) {
|
||||
dirents.sort((a, b) => (getDirentPath(a) < getDirentPath(b) ? -1 : 1));
|
||||
for (const [i, dirent] of dirents.entries()) {
|
||||
assert(dirent instanceof fs.Dirent);
|
||||
assert.strictEqual(getDirentPath(dirent), expected[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function processDirSync(dir) {
|
||||
const dirents = [];
|
||||
let dirent = dir.readSync();
|
||||
while (dirent !== null) {
|
||||
dirents.push(dirent);
|
||||
dirent = dir.readSync();
|
||||
}
|
||||
assertDirents(dirents);
|
||||
}
|
||||
|
||||
// Opendir read results sync
|
||||
|
||||
{
|
||||
const dir = fs.opendirSync(testDir, { recursive: true });
|
||||
processDirSync(dir);
|
||||
dir.closeSync();
|
||||
}
|
||||
|
||||
{
|
||||
fs.opendir(testDir, { recursive: true }, common.mustSucceed((dir) => {
|
||||
processDirSync(dir);
|
||||
dir.close(common.mustSucceed());
|
||||
}));
|
||||
}
|
||||
|
||||
// Opendir read result using callback
|
||||
|
||||
function processDirCb(dir, cb) {
|
||||
const acc = [];
|
||||
|
||||
function _process(dir, acc, cb) {
|
||||
dir.read((err, dirent) => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
if (dirent !== null) {
|
||||
acc.push(dirent);
|
||||
_process(dir, acc, cb);
|
||||
} else {
|
||||
cb(null, acc);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_process(dir, acc, cb);
|
||||
}
|
||||
|
||||
{
|
||||
const dir = fs.opendirSync(testDir, { recursive: true });
|
||||
processDirCb(dir, common.mustSucceed((dirents) => {
|
||||
assertDirents(dirents);
|
||||
dir.close(common.mustSucceed());
|
||||
}));
|
||||
}
|
||||
|
||||
{
|
||||
fs.opendir(testDir, { recursive: true }, common.mustSucceed((dir) => {
|
||||
processDirCb(dir, common.mustSucceed((dirents) => {
|
||||
assertDirents(dirents);
|
||||
dir.close(common.mustSucceed());
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
// Opendir read result using AsyncIterator
|
||||
|
||||
{
|
||||
async function test() {
|
||||
const dir = await fsPromises.opendir(testDir, { recursive: true });
|
||||
const dirents = [];
|
||||
for await (const dirent of dir) {
|
||||
dirents.push(dirent);
|
||||
}
|
||||
assertDirents(dirents);
|
||||
}
|
||||
|
||||
test().then(common.mustCall());
|
||||
}
|
||||
193
test/sequential/test-fs-readdir-recursive.js
Normal file
193
test/sequential/test-fs-readdir-recursive.js
Normal file
@ -0,0 +1,193 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const pathModule = require('path');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
|
||||
const readdirDir = tmpdir.path;
|
||||
|
||||
const fileStructure = [
|
||||
[ 'a', [ 'foo', 'bar' ] ],
|
||||
[ 'b', [ 'foo', 'bar' ] ],
|
||||
[ 'c', [ 'foo', 'bar' ] ],
|
||||
[ 'd', [ 'foo', 'bar' ] ],
|
||||
[ 'e', [ 'foo', 'bar' ] ],
|
||||
[ 'f', [ 'foo', 'bar' ] ],
|
||||
[ 'g', [ 'foo', 'bar' ] ],
|
||||
[ 'h', [ 'foo', 'bar' ] ],
|
||||
[ 'i', [ 'foo', 'bar' ] ],
|
||||
[ 'j', [ 'foo', 'bar' ] ],
|
||||
[ 'k', [ 'foo', 'bar' ] ],
|
||||
[ 'l', [ 'foo', 'bar' ] ],
|
||||
[ 'm', [ 'foo', 'bar' ] ],
|
||||
[ 'n', [ 'foo', 'bar' ] ],
|
||||
[ 'o', [ 'foo', 'bar' ] ],
|
||||
[ 'p', [ 'foo', 'bar' ] ],
|
||||
[ 'q', [ 'foo', 'bar' ] ],
|
||||
[ 'r', [ 'foo', 'bar' ] ],
|
||||
[ 's', [ 'foo', 'bar' ] ],
|
||||
[ 't', [ 'foo', 'bar' ] ],
|
||||
[ 'u', [ 'foo', 'bar' ] ],
|
||||
[ 'v', [ 'foo', 'bar' ] ],
|
||||
[ 'w', [ 'foo', 'bar' ] ],
|
||||
[ 'x', [ 'foo', 'bar' ] ],
|
||||
[ 'y', [ 'foo', 'bar' ] ],
|
||||
[ 'z', [ 'foo', 'bar' ] ],
|
||||
[ 'aa', [ 'foo', 'bar' ] ],
|
||||
[ 'bb', [ 'foo', 'bar' ] ],
|
||||
[ 'cc', [ 'foo', 'bar' ] ],
|
||||
[ 'dd', [ 'foo', 'bar' ] ],
|
||||
[ 'ee', [ 'foo', 'bar' ] ],
|
||||
[ 'ff', [ 'foo', 'bar' ] ],
|
||||
[ 'gg', [ 'foo', 'bar' ] ],
|
||||
[ 'hh', [ 'foo', 'bar' ] ],
|
||||
[ 'ii', [ 'foo', 'bar' ] ],
|
||||
[ 'jj', [ 'foo', 'bar' ] ],
|
||||
[ 'kk', [ 'foo', 'bar' ] ],
|
||||
[ 'll', [ 'foo', 'bar' ] ],
|
||||
[ 'mm', [ 'foo', 'bar' ] ],
|
||||
[ 'nn', [ 'foo', 'bar' ] ],
|
||||
[ 'oo', [ 'foo', 'bar' ] ],
|
||||
[ 'pp', [ 'foo', 'bar' ] ],
|
||||
[ 'qq', [ 'foo', 'bar' ] ],
|
||||
[ 'rr', [ 'foo', 'bar' ] ],
|
||||
[ 'ss', [ 'foo', 'bar' ] ],
|
||||
[ 'tt', [ 'foo', 'bar' ] ],
|
||||
[ 'uu', [ 'foo', 'bar' ] ],
|
||||
[ 'vv', [ 'foo', 'bar' ] ],
|
||||
[ 'ww', [ 'foo', 'bar' ] ],
|
||||
[ 'xx', [ 'foo', 'bar' ] ],
|
||||
[ 'yy', [ 'foo', 'bar' ] ],
|
||||
[ 'zz', [ 'foo', 'bar' ] ],
|
||||
[ 'abc', [ ['def', [ 'foo', 'bar' ] ], ['ghi', [ 'foo', 'bar' ] ] ] ],
|
||||
];
|
||||
|
||||
function createFiles(path, fileStructure) {
|
||||
for (const fileOrDir of fileStructure) {
|
||||
if (typeof fileOrDir === 'string') {
|
||||
fs.writeFileSync(pathModule.join(path, fileOrDir), '');
|
||||
} else {
|
||||
const dirPath = pathModule.join(path, fileOrDir[0]);
|
||||
fs.mkdirSync(dirPath);
|
||||
createFiles(dirPath, fileOrDir[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure tmp directory is clean
|
||||
tmpdir.refresh();
|
||||
|
||||
createFiles(readdirDir, fileStructure);
|
||||
const symlinksRootPath = pathModule.join(readdirDir, 'symlinks');
|
||||
const symlinkTargetFile = pathModule.join(symlinksRootPath, 'symlink-target-file');
|
||||
const symlinkTargetDir = pathModule.join(symlinksRootPath, 'symlink-target-dir');
|
||||
fs.mkdirSync(symlinksRootPath);
|
||||
fs.writeFileSync(symlinkTargetFile, '');
|
||||
fs.mkdirSync(symlinkTargetDir);
|
||||
fs.symlinkSync(symlinkTargetFile, pathModule.join(symlinksRootPath, 'symlink-src-file'));
|
||||
fs.symlinkSync(symlinkTargetDir, pathModule.join(symlinksRootPath, 'symlink-src-dir'));
|
||||
|
||||
const expected = [
|
||||
'a', 'a/bar', 'a/foo', 'aa', 'aa/bar', 'aa/foo',
|
||||
'abc', 'abc/def', 'abc/def/bar', 'abc/def/foo', 'abc/ghi', 'abc/ghi/bar', 'abc/ghi/foo',
|
||||
'b', 'b/bar', 'b/foo', 'bb', 'bb/bar', 'bb/foo',
|
||||
'c', 'c/bar', 'c/foo', 'cc', 'cc/bar', 'cc/foo',
|
||||
'd', 'd/bar', 'd/foo', 'dd', 'dd/bar', 'dd/foo',
|
||||
'e', 'e/bar', 'e/foo', 'ee', 'ee/bar', 'ee/foo',
|
||||
'f', 'f/bar', 'f/foo', 'ff', 'ff/bar', 'ff/foo',
|
||||
'g', 'g/bar', 'g/foo', 'gg', 'gg/bar', 'gg/foo',
|
||||
'h', 'h/bar', 'h/foo', 'hh', 'hh/bar', 'hh/foo',
|
||||
'i', 'i/bar', 'i/foo', 'ii', 'ii/bar', 'ii/foo',
|
||||
'j', 'j/bar', 'j/foo', 'jj', 'jj/bar', 'jj/foo',
|
||||
'k', 'k/bar', 'k/foo', 'kk', 'kk/bar', 'kk/foo',
|
||||
'l', 'l/bar', 'l/foo', 'll', 'll/bar', 'll/foo',
|
||||
'm', 'm/bar', 'm/foo', 'mm', 'mm/bar', 'mm/foo',
|
||||
'n', 'n/bar', 'n/foo', 'nn', 'nn/bar', 'nn/foo',
|
||||
'o', 'o/bar', 'o/foo', 'oo', 'oo/bar', 'oo/foo',
|
||||
'p', 'p/bar', 'p/foo', 'pp', 'pp/bar', 'pp/foo',
|
||||
'q', 'q/bar', 'q/foo', 'qq', 'qq/bar', 'qq/foo',
|
||||
'r', 'r/bar', 'r/foo', 'rr', 'rr/bar', 'rr/foo',
|
||||
's', 's/bar', 's/foo', 'ss', 'ss/bar', 'ss/foo',
|
||||
'symlinks', 'symlinks/symlink-src-dir', 'symlinks/symlink-src-file',
|
||||
'symlinks/symlink-target-dir', 'symlinks/symlink-target-file',
|
||||
't', 't/bar', 't/foo', 'tt', 'tt/bar', 'tt/foo',
|
||||
'u', 'u/bar', 'u/foo', 'uu', 'uu/bar', 'uu/foo',
|
||||
'v', 'v/bar', 'v/foo', 'vv', 'vv/bar', 'vv/foo',
|
||||
'w', 'w/bar', 'w/foo', 'ww', 'ww/bar', 'ww/foo',
|
||||
'x', 'x/bar', 'x/foo', 'xx', 'xx/bar', 'xx/foo',
|
||||
'y', 'y/bar', 'y/foo', 'yy', 'yy/bar', 'yy/foo',
|
||||
'z', 'z/bar', 'z/foo', 'zz', 'zz/bar', 'zz/foo',
|
||||
];
|
||||
|
||||
// Normalize paths once for non POSIX platforms
|
||||
for (let i = 0; i < expected.length; i++) {
|
||||
expected[i] = pathModule.normalize(expected[i]);
|
||||
}
|
||||
|
||||
function getDirentPath(dirent) {
|
||||
return pathModule.relative(readdirDir, pathModule.join(dirent.path, dirent.name));
|
||||
}
|
||||
|
||||
function assertDirents(dirents) {
|
||||
dirents.sort((a, b) => (getDirentPath(a) < getDirentPath(b) ? -1 : 1));
|
||||
for (const [i, dirent] of dirents.entries()) {
|
||||
assert(dirent instanceof fs.Dirent);
|
||||
assert.strictEqual(getDirentPath(dirent), expected[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// readdirSync
|
||||
|
||||
// readdirSync { recursive }
|
||||
{
|
||||
const result = fs.readdirSync(readdirDir, { recursive: true });
|
||||
assert.deepStrictEqual(result.sort(), expected);
|
||||
}
|
||||
|
||||
// readdirSync { recursive, withFileTypes }
|
||||
{
|
||||
const result = fs.readdirSync(readdirDir, { recursive: true, withFileTypes: true });
|
||||
assertDirents(result);
|
||||
}
|
||||
|
||||
// readdir
|
||||
|
||||
// readdir { recursive } callback
|
||||
{
|
||||
fs.readdir(readdirDir, { recursive: true },
|
||||
common.mustSucceed((result) => {
|
||||
assert.deepStrictEqual(result.sort(), expected);
|
||||
}));
|
||||
}
|
||||
|
||||
// Readdir { recursive, withFileTypes } callback
|
||||
{
|
||||
fs.readdir(readdirDir, { recursive: true, withFileTypes: true },
|
||||
common.mustSucceed((result) => {
|
||||
assertDirents(result);
|
||||
}));
|
||||
}
|
||||
|
||||
// fs.promises.readdir
|
||||
|
||||
// fs.promises.readdir { recursive }
|
||||
{
|
||||
async function test() {
|
||||
const result = await fs.promises.readdir(readdirDir, { recursive: true });
|
||||
assert.deepStrictEqual(result.sort(), expected);
|
||||
}
|
||||
|
||||
test().then(common.mustCall());
|
||||
}
|
||||
|
||||
// fs.promises.readdir { recursive, withFileTypes }
|
||||
{
|
||||
async function test() {
|
||||
const result = await fs.promises.readdir(readdirDir, { recursive: true, withFileTypes: true });
|
||||
assertDirents(result);
|
||||
}
|
||||
|
||||
test().then(common.mustCall());
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user