Skip to content

Commit c7944a7

Browse files
committed
fs: readdir optionally returning type information
readdir and readdirSync now have a "withFileTypes" option, which, when enabled, provides an array of DirectoryEntry objects, similar to Stats objects, which have the filename and the type information. Refs: nodejs#15699 PR-URL: nodejs#22020 Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Roman Reiss <me@silverwind.io> Reviewed-By: John-David Dalton <john.david.dalton@gmail.com>
1 parent 78584b6 commit c7944a7

8 files changed

Lines changed: 524 additions & 52 deletions

File tree

doc/api/fs.md

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,92 @@ synchronous use libuv's threadpool, which can have surprising and negative
283283
performance implications for some applications. See the
284284
[`UV_THREADPOOL_SIZE`][] documentation for more information.
285285

286+
## Class: fs.Dirent
287+
<!-- YAML
288+
added: REPLACEME
289+
-->
290+
291+
When [`fs.readdir()`][] or [`fs.readdirSync()`][] is called with the
292+
`withFileTypes` option set to `true`, the resulting array is filled with
293+
`fs.Dirent` objects, rather than strings or `Buffers`.
294+
295+
### dirent.isBlockDevice()
296+
<!-- YAML
297+
added: REPLACEME
298+
-->
299+
300+
* Returns: {boolean}
301+
302+
Returns `true` if the `fs.Dirent` object describes a block device.
303+
304+
### dirent.isCharacterDevice()
305+
<!-- YAML
306+
added: REPLACEME
307+
-->
308+
309+
* Returns: {boolean}
310+
311+
Returns `true` if the `fs.Dirent` object describes a character device.
312+
313+
### dirent.isDirectory()
314+
<!-- YAML
315+
added: REPLACEME
316+
-->
317+
318+
* Returns: {boolean}
319+
320+
Returns `true` if the `fs.Dirent` object describes a file system
321+
directory.
322+
323+
### dirent.isFIFO()
324+
<!-- YAML
325+
added: REPLACEME
326+
-->
327+
328+
* Returns: {boolean}
329+
330+
Returns `true` if the `fs.Dirent` object describes a first-in-first-out
331+
(FIFO) pipe.
332+
333+
### dirent.isFile()
334+
<!-- YAML
335+
added: REPLACEME
336+
-->
337+
338+
* Returns: {boolean}
339+
340+
Returns `true` if the `fs.Dirent` object describes a regular file.
341+
342+
### dirent.isSocket()
343+
<!-- YAML
344+
added: REPLACEME
345+
-->
346+
347+
* Returns: {boolean}
348+
349+
Returns `true` if the `fs.Dirent` object describes a socket.
350+
351+
### dirent.isSymbolicLink()
352+
<!-- YAML
353+
added: REPLACEME
354+
-->
355+
356+
* Returns: {boolean}
357+
358+
Returns `true` if the `fs.Dirent` object describes a symbolic link.
359+
360+
361+
### dirent.name
362+
<!-- YAML
363+
added: REPLACEME
364+
-->
365+
366+
* {string|Buffer}
367+
368+
The file name that this `fs.Dirent` object refers to. The type of this
369+
value is determined by the `options.encoding` passed to [`fs.readdir()`][] or
370+
[`fs.readdirSync()`][].
371+
286372
## Class: fs.FSWatcher
287373
<!-- YAML
288374
added: v0.5.8
@@ -2319,9 +2405,10 @@ changes:
23192405
* `path` {string|Buffer|URL}
23202406
* `options` {string|Object}
23212407
* `encoding` {string} **Default:** `'utf8'`
2408+
* `withFileTypes` {boolean} **Default:** `false`
23222409
* `callback` {Function}
23232410
* `err` {Error}
2324-
* `files` {string[]|Buffer[]}
2411+
* `files` {string[]|Buffer[]|fs.Dirent[]}
23252412

23262413
Asynchronous readdir(3). Reads the contents of a directory.
23272414
The callback gets two arguments `(err, files)` where `files` is an array of
@@ -2332,6 +2419,9 @@ object with an `encoding` property specifying the character encoding to use for
23322419
the filenames passed to the callback. If the `encoding` is set to `'buffer'`,
23332420
the filenames returned will be passed as `Buffer` objects.
23342421

2422+
If `options.withFileTypes` is set to `true`, the `files` array will contain
2423+
[`fs.Dirent`][] objects.
2424+
23352425
## fs.readdirSync(path[, options])
23362426
<!-- YAML
23372427
added: v0.1.21
@@ -2345,7 +2435,8 @@ changes:
23452435
* `path` {string|Buffer|URL}
23462436
* `options` {string|Object}
23472437
* `encoding` {string} **Default:** `'utf8'`
2348-
* Returns: {string[]} An array of filenames excluding `'.'` and `'..'`.
2438+
* `withFileTypes` {boolean} **Default:** `false`
2439+
* Returns: {string[]|Buffer[]|fs.Dirent[]}
23492440

23502441
Synchronous readdir(3).
23512442

@@ -2354,6 +2445,9 @@ object with an `encoding` property specifying the character encoding to use for
23542445
the filenames returned. If the `encoding` is set to `'buffer'`,
23552446
the filenames returned will be passed as `Buffer` objects.
23562447

2448+
If `options.withFileTypes` is set to `true`, the result will contain
2449+
[`fs.Dirent`][] objects.
2450+
23572451
## fs.readFile(path[, options], callback)
23582452
<!-- YAML
23592453
added: v0.1.29
@@ -4637,6 +4731,7 @@ the file contents.
46374731
[`WriteStream`]: #fs_class_fs_writestream
46384732
[`EventEmitter`]: events.html
46394733
[`event ports`]: http://illumos.org/man/port_create
4734+
[`fs.Dirent`]: #fs_class_fs_dirent
46404735
[`fs.FSWatcher`]: #fs_class_fs_fswatcher
46414736
[`fs.Stats`]: #fs_class_fs_stats
46424737
[`fs.access()`]: #fs_fs_access_path_mode_callback
@@ -4652,6 +4747,8 @@ the file contents.
46524747
[`fs.mkdtemp()`]: #fs_fs_mkdtemp_prefix_options_callback
46534748
[`fs.open()`]: #fs_fs_open_path_flags_mode_callback
46544749
[`fs.read()`]: #fs_fs_read_fd_buffer_offset_length_position_callback
4750+
[`fs.readdir()`]: #fs_fs_readdir_path_options_callback
4751+
[`fs.readdirSync()`]: #fs_fs_readdirsync_path_options
46554752
[`fs.readFile()`]: #fs_fs_readfile_path_options_callback
46564753
[`fs.readFileSync()`]: #fs_fs_readfilesync_path_options
46574754
[`fs.realpath()`]: #fs_fs_realpath_path_options_callback

lib/fs.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ const { getPathFromURL } = require('internal/url');
5858
const internalUtil = require('internal/util');
5959
const {
6060
copyObject,
61+
Dirent,
62+
getDirents,
6163
getOptions,
6264
nullCheck,
6365
preprocessSymlinkDestination,
@@ -773,8 +775,19 @@ function readdir(path, options, callback) {
773775
validatePath(path);
774776

775777
const req = new FSReqCallback();
776-
req.oncomplete = callback;
777-
binding.readdir(pathModule.toNamespacedPath(path), options.encoding, req);
778+
if (!options.withFileTypes) {
779+
req.oncomplete = callback;
780+
} else {
781+
req.oncomplete = (err, result) => {
782+
if (err) {
783+
callback(err);
784+
return;
785+
}
786+
getDirents(path, result, callback);
787+
};
788+
}
789+
binding.readdir(pathModule.toNamespacedPath(path), options.encoding,
790+
!!options.withFileTypes, req);
778791
}
779792

780793
function readdirSync(path, options) {
@@ -783,9 +796,10 @@ function readdirSync(path, options) {
783796
validatePath(path);
784797
const ctx = { path };
785798
const result = binding.readdir(pathModule.toNamespacedPath(path),
786-
options.encoding, undefined, ctx);
799+
options.encoding, !!options.withFileTypes,
800+
undefined, ctx);
787801
handleErrorFromBinding(ctx);
788-
return result;
802+
return options.withFileTypes ? getDirents(path, result) : result;
789803
}
790804

791805
function fstat(fd, options, callback) {
@@ -1819,6 +1833,7 @@ module.exports = fs = {
18191833
writeFileSync,
18201834
write,
18211835
writeSync,
1836+
Dirent,
18221837
Stats,
18231838

18241839
get ReadStream() {

lib/internal/fs/promises.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const { getPathFromURL } = require('internal/url');
1919
const { isUint8Array } = require('internal/util/types');
2020
const {
2121
copyObject,
22+
getDirents,
2223
getOptions,
2324
getStatsFromBinding,
2425
nullCheck,
@@ -37,10 +38,13 @@ const {
3738
validateUint32
3839
} = require('internal/validators');
3940
const pathModule = require('path');
41+
const { promisify } = require('internal/util');
4042

4143
const kHandle = Symbol('handle');
4244
const { kUsePromises } = binding;
4345

46+
const getDirectoryEntriesPromise = promisify(getDirents);
47+
4448
class FileHandle {
4549
constructor(filehandle) {
4650
this[kHandle] = filehandle;
@@ -312,8 +316,12 @@ async function readdir(path, options) {
312316
options = getOptions(options, {});
313317
path = getPathFromURL(path);
314318
validatePath(path);
315-
return binding.readdir(pathModule.toNamespacedPath(path),
316-
options.encoding, kUsePromises);
319+
const result = await binding.readdir(pathModule.toNamespacedPath(path),
320+
options.encoding, !!options.withTypes,
321+
kUsePromises);
322+
return options.withFileTypes ?
323+
getDirectoryEntriesPromise(path, result) :
324+
result;
317325
}
318326

319327
async function readlink(path, options) {

lib/internal/fs/utils.js

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const {
1212
const { isUint8Array } = require('internal/util/types');
1313
const pathModule = require('path');
1414
const util = require('util');
15+
const kType = Symbol('type');
16+
const kStats = Symbol('stats');
1517

1618
const {
1719
O_APPEND,
@@ -31,24 +33,135 @@ const {
3133
S_IFREG,
3234
S_IFSOCK,
3335
UV_FS_SYMLINK_DIR,
34-
UV_FS_SYMLINK_JUNCTION
36+
UV_FS_SYMLINK_JUNCTION,
37+
UV_DIRENT_UNKNOWN,
38+
UV_DIRENT_FILE,
39+
UV_DIRENT_DIR,
40+
UV_DIRENT_LINK,
41+
UV_DIRENT_FIFO,
42+
UV_DIRENT_SOCKET,
43+
UV_DIRENT_CHAR,
44+
UV_DIRENT_BLOCK
3545
} = process.binding('constants').fs;
3646

3747
const isWindows = process.platform === 'win32';
3848

49+
let fs;
50+
function lazyLoadFs() {
51+
if (!fs) {
52+
fs = require('fs');
53+
}
54+
return fs;
55+
}
56+
3957
function assertEncoding(encoding) {
4058
if (encoding && !Buffer.isEncoding(encoding)) {
4159
throw new ERR_INVALID_OPT_VALUE_ENCODING(encoding);
4260
}
4361
}
4462

63+
class Dirent {
64+
constructor(name, type) {
65+
this.name = name;
66+
this[kType] = type;
67+
}
68+
69+
isDirectory() {
70+
return this[kType] === UV_DIRENT_DIR;
71+
}
72+
73+
isFile() {
74+
return this[kType] === UV_DIRENT_FILE;
75+
}
76+
77+
isBlockDevice() {
78+
return this[kType] === UV_DIRENT_BLOCK;
79+
}
80+
81+
isCharacterDevice() {
82+
return this[kType] === UV_DIRENT_CHAR;
83+
}
84+
85+
isSymbolicLink() {
86+
return this[kType] === UV_DIRENT_LINK;
87+
}
88+
89+
isFIFO() {
90+
return this[kType] === UV_DIRENT_FIFO;
91+
}
92+
93+
isSocket() {
94+
return this[kType] === UV_DIRENT_SOCKET;
95+
}
96+
}
97+
98+
class DirentFromStats extends Dirent {
99+
constructor(name, stats) {
100+
super(name, null);
101+
this[kStats] = stats;
102+
}
103+
}
104+
105+
for (const name of Reflect.ownKeys(Dirent.prototype)) {
106+
if (name === 'constructor') {
107+
continue;
108+
}
109+
DirentFromStats.prototype[name] = function() {
110+
return this[kStats][name]();
111+
};
112+
}
113+
45114
function copyObject(source) {
46115
var target = {};
47116
for (var key in source)
48117
target[key] = source[key];
49118
return target;
50119
}
51120

121+
function getDirents(path, [names, types], callback) {
122+
var i;
123+
if (typeof callback == 'function') {
124+
const len = names.length;
125+
let toFinish = 0;
126+
for (i = 0; i < len; i++) {
127+
const type = types[i];
128+
if (type === UV_DIRENT_UNKNOWN) {
129+
const name = names[i];
130+
const idx = i;
131+
toFinish++;
132+
lazyLoadFs().stat(pathModule.resolve(path, name), (err, stats) => {
133+
if (err) {
134+
callback(err);
135+
return;
136+
}
137+
names[idx] = new DirentFromStats(name, stats);
138+
if (--toFinish === 0) {
139+
callback(null, names);
140+
}
141+
});
142+
} else {
143+
names[i] = new Dirent(names[i], types[i]);
144+
}
145+
}
146+
if (toFinish === 0) {
147+
callback(null, names);
148+
}
149+
} else {
150+
const len = names.length;
151+
for (i = 0; i < len; i++) {
152+
const type = types[i];
153+
if (type === UV_DIRENT_UNKNOWN) {
154+
const name = names[i];
155+
const stats = lazyLoadFs().statSync(pathModule.resolve(path, name));
156+
names[i] = new DirentFromStats(name, stats);
157+
} else {
158+
names[i] = new Dirent(names[i], types[i]);
159+
}
160+
}
161+
return names;
162+
}
163+
}
164+
52165
function getOptions(options, defaultOptions) {
53166
if (options === null || options === undefined ||
54167
typeof options === 'function') {
@@ -342,6 +455,8 @@ function validatePath(path, propName = 'path') {
342455
module.exports = {
343456
assertEncoding,
344457
copyObject,
458+
Dirent,
459+
getDirents,
345460
getOptions,
346461
nullCheck,
347462
preprocessSymlinkDestination,

0 commit comments

Comments
 (0)