Skip to content

Commit

Permalink
fs: readdir optionally returning type information
Browse files Browse the repository at this point in the history
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: #15699

PR-URL: #22020
Reviewed-By: Ruben Bridgewater <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Anna Henningsen <[email protected]>
Reviewed-By: Benjamin Gruenbaum <[email protected]>
Reviewed-By: Roman Reiss <[email protected]>
Reviewed-By: John-David Dalton <[email protected]>
  • Loading branch information
bengl authored and targos committed Aug 19, 2018
1 parent 4a56313 commit 549d0c2
Show file tree
Hide file tree
Showing 8 changed files with 524 additions and 52 deletions.
101 changes: 99 additions & 2 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,92 @@ synchronous use libuv's threadpool, which can have surprising and negative
performance implications for some applications. See the
[`UV_THREADPOOL_SIZE`][] documentation for more information.

## Class: fs.Dirent
<!-- YAML
added: REPLACEME
-->

When [`fs.readdir()`][] or [`fs.readdirSync()`][] is called with the
`withFileTypes` option set to `true`, the resulting array is filled with
`fs.Dirent` objects, rather than strings or `Buffers`.

### dirent.isBlockDevice()
<!-- YAML
added: REPLACEME
-->

* Returns: {boolean}

Returns `true` if the `fs.Dirent` object describes a block device.

### dirent.isCharacterDevice()
<!-- YAML
added: REPLACEME
-->

* Returns: {boolean}

Returns `true` if the `fs.Dirent` object describes a character device.

### dirent.isDirectory()
<!-- YAML
added: REPLACEME
-->

* Returns: {boolean}

Returns `true` if the `fs.Dirent` object describes a file system
directory.

### dirent.isFIFO()
<!-- YAML
added: REPLACEME
-->

* Returns: {boolean}

Returns `true` if the `fs.Dirent` object describes a first-in-first-out
(FIFO) pipe.

### dirent.isFile()
<!-- YAML
added: REPLACEME
-->

* Returns: {boolean}

Returns `true` if the `fs.Dirent` object describes a regular file.

### dirent.isSocket()
<!-- YAML
added: REPLACEME
-->

* Returns: {boolean}

Returns `true` if the `fs.Dirent` object describes a socket.

### dirent.isSymbolicLink()
<!-- YAML
added: REPLACEME
-->

* Returns: {boolean}

Returns `true` if the `fs.Dirent` object describes a symbolic link.


### dirent.name
<!-- YAML
added: REPLACEME
-->

* {string|Buffer}

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()`][].

## Class: fs.FSWatcher
<!-- YAML
added: v0.5.8
Expand Down Expand Up @@ -2314,9 +2400,10 @@ changes:
* `path` {string|Buffer|URL}
* `options` {string|Object}
* `encoding` {string} **Default:** `'utf8'`
* `withFileTypes` {boolean} **Default:** `false`
* `callback` {Function}
* `err` {Error}
* `files` {string[]|Buffer[]}
* `files` {string[]|Buffer[]|fs.Dirent[]}

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

If `options.withFileTypes` is set to `true`, the `files` array will contain
[`fs.Dirent`][] objects.

## fs.readdirSync(path[, options])
<!-- YAML
added: v0.1.21
Expand All @@ -2340,7 +2430,8 @@ changes:
* `path` {string|Buffer|URL}
* `options` {string|Object}
* `encoding` {string} **Default:** `'utf8'`
* Returns: {string[]} An array of filenames excluding `'.'` and `'..'`.
* `withFileTypes` {boolean} **Default:** `false`
* Returns: {string[]|Buffer[]|fs.Dirent[]}

Synchronous readdir(3).

Expand All @@ -2349,6 +2440,9 @@ object with an `encoding` property specifying the character encoding to use for
the filenames returned. If the `encoding` is set to `'buffer'`,
the filenames returned will be passed as `Buffer` objects.

If `options.withFileTypes` is set to `true`, the result will contain
[`fs.Dirent`][] objects.

## fs.readFile(path[, options], callback)
<!-- YAML
added: v0.1.29
Expand Down Expand Up @@ -4632,6 +4726,7 @@ the file contents.
[`WriteStream`]: #fs_class_fs_writestream
[`EventEmitter`]: events.html
[`event ports`]: http://illumos.org/man/port_create
[`fs.Dirent`]: #fs_class_fs_dirent
[`fs.FSWatcher`]: #fs_class_fs_fswatcher
[`fs.Stats`]: #fs_class_fs_stats
[`fs.access()`]: #fs_fs_access_path_mode_callback
Expand All @@ -4647,6 +4742,8 @@ the file contents.
[`fs.mkdtemp()`]: #fs_fs_mkdtemp_prefix_options_callback
[`fs.open()`]: #fs_fs_open_path_flags_mode_callback
[`fs.read()`]: #fs_fs_read_fd_buffer_offset_length_position_callback
[`fs.readdir()`]: #fs_fs_readdir_path_options_callback
[`fs.readdirSync()`]: #fs_fs_readdirsync_path_options
[`fs.readFile()`]: #fs_fs_readfile_path_options_callback
[`fs.readFileSync()`]: #fs_fs_readfilesync_path_options
[`fs.realpath()`]: #fs_fs_realpath_path_options_callback
Expand Down
23 changes: 19 additions & 4 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ const { getPathFromURL } = require('internal/url');
const internalUtil = require('internal/util');
const {
copyObject,
Dirent,
getDirents,
getOptions,
nullCheck,
preprocessSymlinkDestination,
Expand Down Expand Up @@ -755,8 +757,19 @@ function readdir(path, options, callback) {
validatePath(path);

const req = new FSReqWrap();
req.oncomplete = callback;
binding.readdir(pathModule.toNamespacedPath(path), options.encoding, req);
if (!options.withFileTypes) {
req.oncomplete = callback;
} else {
req.oncomplete = (err, result) => {
if (err) {
callback(err);
return;
}
getDirents(path, result, callback);
};
}
binding.readdir(pathModule.toNamespacedPath(path), options.encoding,
!!options.withFileTypes, req);
}

function readdirSync(path, options) {
Expand All @@ -765,9 +778,10 @@ function readdirSync(path, options) {
validatePath(path);
const ctx = { path };
const result = binding.readdir(pathModule.toNamespacedPath(path),
options.encoding, undefined, ctx);
options.encoding, !!options.withFileTypes,
undefined, ctx);
handleErrorFromBinding(ctx);
return result;
return options.withFileTypes ? getDirents(path, result) : result;
}

function fstat(fd, options, callback) {
Expand Down Expand Up @@ -1792,6 +1806,7 @@ module.exports = fs = {
writeFileSync,
write,
writeSync,
Dirent,
Stats,

// Stream constructors
Expand Down
12 changes: 10 additions & 2 deletions lib/internal/fs/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const { getPathFromURL } = require('internal/url');
const { isUint8Array } = require('internal/util/types');
const {
copyObject,
getDirents,
getOptions,
getStatsFromBinding,
nullCheck,
Expand All @@ -36,10 +37,13 @@ const {
validateUint32
} = require('internal/validators');
const pathModule = require('path');
const { promisify } = require('internal/util');

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

const getDirectoryEntriesPromise = promisify(getDirents);

class FileHandle {
constructor(filehandle) {
this[kHandle] = filehandle;
Expand Down Expand Up @@ -306,8 +310,12 @@ async function readdir(path, options) {
options = getOptions(options, {});
path = getPathFromURL(path);
validatePath(path);
return binding.readdir(pathModule.toNamespacedPath(path),
options.encoding, kUsePromises);
const result = await binding.readdir(pathModule.toNamespacedPath(path),
options.encoding, !!options.withTypes,
kUsePromises);
return options.withFileTypes ?
getDirectoryEntriesPromise(path, result) :
result;
}

async function readlink(path, options) {
Expand Down
117 changes: 116 additions & 1 deletion lib/internal/fs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const {
const { isUint8Array } = require('internal/util/types');
const pathModule = require('path');
const util = require('util');
const kType = Symbol('type');
const kStats = Symbol('stats');

const {
O_APPEND,
Expand All @@ -31,24 +33,135 @@ const {
S_IFREG,
S_IFSOCK,
UV_FS_SYMLINK_DIR,
UV_FS_SYMLINK_JUNCTION
UV_FS_SYMLINK_JUNCTION,
UV_DIRENT_UNKNOWN,
UV_DIRENT_FILE,
UV_DIRENT_DIR,
UV_DIRENT_LINK,
UV_DIRENT_FIFO,
UV_DIRENT_SOCKET,
UV_DIRENT_CHAR,
UV_DIRENT_BLOCK
} = process.binding('constants').fs;

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

let fs;
function lazyLoadFs() {
if (!fs) {
fs = require('fs');
}
return fs;
}

function assertEncoding(encoding) {
if (encoding && !Buffer.isEncoding(encoding)) {
throw new ERR_INVALID_OPT_VALUE_ENCODING(encoding);
}
}

class Dirent {
constructor(name, type) {
this.name = name;
this[kType] = type;
}

isDirectory() {
return this[kType] === UV_DIRENT_DIR;
}

isFile() {
return this[kType] === UV_DIRENT_FILE;
}

isBlockDevice() {
return this[kType] === UV_DIRENT_BLOCK;
}

isCharacterDevice() {
return this[kType] === UV_DIRENT_CHAR;
}

isSymbolicLink() {
return this[kType] === UV_DIRENT_LINK;
}

isFIFO() {
return this[kType] === UV_DIRENT_FIFO;
}

isSocket() {
return this[kType] === UV_DIRENT_SOCKET;
}
}

class DirentFromStats extends Dirent {
constructor(name, stats) {
super(name, null);
this[kStats] = stats;
}
}

for (const name of Reflect.ownKeys(Dirent.prototype)) {
if (name === 'constructor') {
continue;
}
DirentFromStats.prototype[name] = function() {
return this[kStats][name]();
};
}

function copyObject(source) {
var target = {};
for (var key in source)
target[key] = source[key];
return target;
}

function getDirents(path, [names, types], callback) {
var i;
if (typeof callback == 'function') {
const len = names.length;
let toFinish = 0;
for (i = 0; i < len; i++) {
const type = types[i];
if (type === UV_DIRENT_UNKNOWN) {
const name = names[i];
const idx = i;
toFinish++;
lazyLoadFs().stat(pathModule.resolve(path, name), (err, stats) => {
if (err) {
callback(err);
return;
}
names[idx] = new DirentFromStats(name, stats);
if (--toFinish === 0) {
callback(null, names);
}
});
} else {
names[i] = new Dirent(names[i], types[i]);
}
}
if (toFinish === 0) {
callback(null, names);
}
} else {
const len = names.length;
for (i = 0; i < len; i++) {
const type = types[i];
if (type === UV_DIRENT_UNKNOWN) {
const name = names[i];
const stats = lazyLoadFs().statSync(pathModule.resolve(path, name));
names[i] = new DirentFromStats(name, stats);
} else {
names[i] = new Dirent(names[i], types[i]);
}
}
return names;
}
}

function getOptions(options, defaultOptions) {
if (options === null || options === undefined ||
typeof options === 'function') {
Expand Down Expand Up @@ -341,6 +454,8 @@ function validatePath(path, propName = 'path') {
module.exports = {
assertEncoding,
copyObject,
Dirent,
getDirents,
getOptions,
nullCheck,
preprocessSymlinkDestination,
Expand Down
Loading

0 comments on commit 549d0c2

Please sign in to comment.