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:
Ethan Arrowood 2023-04-20 11:50:27 -06:00 committed by GitHub
parent 6675505686
commit 7b39e8099a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 659 additions and 31 deletions

View File

@ -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

View File

@ -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,

View File

@ -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) {

View File

@ -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;

View File

@ -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;
}

View 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());
}

View 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());
}