diff --git a/benchmark/fs/bench-stat-promise.js b/benchmark/fs/bench-stat-promise.js index adc0ed4965fdfd..b0317455728b46 100644 --- a/benchmark/fs/bench-stat-promise.js +++ b/benchmark/fs/bench-stat-promise.js @@ -1,7 +1,7 @@ 'use strict'; const common = require('../common'); -const fs = require('fs'); +const fsPromises = require('fs/promises'); const bench = common.createBenchmark(main, { n: [20e4], @@ -10,11 +10,11 @@ const bench = common.createBenchmark(main, { async function run(n, statType) { const arg = statType === 'fstat' ? - await fs.promises.open(__filename, 'r') : __filename; + await fsPromises.open(__filename, 'r') : __filename; let remaining = n; bench.start(); while (remaining-- > 0) - await fs.promises[statType](arg); + await fsPromises[statType](arg); bench.end(n); if (typeof arg.close === 'function') diff --git a/doc/api/fs.md b/doc/api/fs.md index 15afc61e567efc..a4a9e5fd3f2096 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -3231,9 +3231,9 @@ Synchronous versions of [`fs.write()`][]. Returns the number of bytes written. > Stability: 1 - Experimental -The `fs.promises` API provides an alternative set of asynchronous file system +The `fs/promises` API provides an alternative set of asynchronous file system methods that return `Promise` objects rather than using callbacks. The -API is accessible via `fs.promises`. +API is accessible via `require('fs/promises)`. ### class: FileHandle @@ -3547,18 +3547,18 @@ with an `Error` object. The following example checks if the file `/etc/passwd` can be read and written by the current process. ```js -fs.promises.access('/etc/passwd', fs.constants.R_OK | fs.constants.W_OK) +fsPromises.access('/etc/passwd', fs.constants.R_OK | fs.constants.W_OK) .then(() => console.log('can access')) .catch(() => console.error('cannot access')); ``` -Using `fs.promises.access()` to check for the accessibility of a file before -calling `fs.promises.open()` is not recommended. Doing so introduces a race +Using `fsPromises.access()` to check for the accessibility of a file before +calling `fsPromises.open()` is not recommended. Doing so introduces a race condition, since other processes may change the file's state between the two calls. Instead, user code should open/read/write the file directly and handle the error raised if the file is not accessible. -### fs.promises.appendFile(file, data[, options]) +### fsPromises.appendFile(file, data[, options]) @@ -3578,9 +3578,9 @@ resolved with no arguments upon success. If `options` is a string, then it specifies the encoding. The `file` may be specified as a `FileHandle` that has been opened -for appending (using `fs.promises.open()`). +for appending (using `fsPromises.open()`). -### fs.promises.chmod(path, mode) +### fsPromises.chmod(path, mode) @@ -3592,7 +3592,7 @@ added: REPLACEME Changes the permissions of a file then resolves the `Promise` with no arguments upon succces. -### fs.promises.chown(path, uid, gid) +### fsPromises.chown(path, uid, gid) @@ -3605,7 +3605,7 @@ added: REPLACEME Changes the ownership of a file then resolves the `Promise` with no arguments upon success. -### fs.promises.copyFile(src, dest[, flags]) +### fsPromises.copyFile(src, dest[, flags]) @@ -3632,7 +3632,7 @@ Example: const fs = require('fs'); // destination.txt will be created or overwritten by default. -fs.promises.copyFile('source.txt', 'destination.txt') +fsPromises.copyFile('source.txt', 'destination.txt') .then(() => console.log('source.txt was copied to destination.txt')) .catch(() => console.log('The file could not be copied')); ``` @@ -3645,12 +3645,12 @@ const fs = require('fs'); const { COPYFILE_EXCL } = fs.constants; // By using COPYFILE_EXCL, the operation will fail if destination.txt exists. -fs.promises.copyFile('source.txt', 'destination.txt', COPYFILE_EXCL) +fsPromises.copyFile('source.txt', 'destination.txt', COPYFILE_EXCL) .then(() => console.log('source.txt was copied to destination.txt')) .catch(() => console.log('The file could not be copied')); ``` -### fs.promises.fchmod(filehandle, mode) +### fsPromises.fchmod(filehandle, mode) @@ -3662,7 +3662,7 @@ added: REPLACEME Asynchronous fchmod(2). The `Promise` is resolved with no arguments upon success. -### fs.promises.fchown(filehandle, uid, gid) +### fsPromises.fchown(filehandle, uid, gid) @@ -3675,7 +3675,7 @@ added: REPLACEME Changes the ownership of the file represented by `filehandle` then resolves the `Promise` with no arguments upon success. -### fs.promises.fdatasync(filehandle) +### fsPromises.fdatasync(filehandle) @@ -3686,7 +3686,7 @@ added: REPLACEME Asynchronous fdatasync(2). The `Promise` is resolved with no arguments upon success. -### fs.promises.fstat(filehandle) +### fsPromises.fstat(filehandle) @@ -3696,7 +3696,7 @@ added: REPLACEME Retrieves the [`fs.Stats`][] for the given `filehandle`. -### fs.promises.fsync(filehandle) +### fsPromises.fsync(filehandle) @@ -3707,7 +3707,7 @@ added: REPLACEME Asynchronous fsync(2). The `Promise` is resolved with no arguments upon success. -### fs.promises.ftruncate(filehandle[, len]) +### fsPromises.ftruncate(filehandle[, len]) @@ -3730,8 +3730,8 @@ console.log(fs.readFileSync('temp.txt', 'utf8')); // Prints: Node.js async function doTruncate() { - const fd = await fs.promises.open('temp.txt', 'r+'); - await fs.promises.ftruncate(fd, 4); + const fd = await fsPromises.open('temp.txt', 'r+'); + await fsPromises.ftruncate(fd, 4); console.log(fs.readFileSync('temp.txt', 'utf8')); // Prints: Node } @@ -3746,8 +3746,8 @@ console.log(fs.readFileSync('temp.txt', 'utf8')); // Prints: Node.js async function doTruncate() { - const fd = await fs.promises.open('temp.txt', 'r+'); - await fs.promises.ftruncate(fd, 10); + const fd = await fsPromises.open('temp.txt', 'r+'); + await fsPromises.ftruncate(fd, 10); console.log(fs.readFileSync('temp.txt', 'utf8')); // Prints Node.js\0\0\0 } @@ -3756,7 +3756,7 @@ doTruncate().catch(console.error); The last three bytes are null bytes ('\0'), to compensate the over-truncation. -### fs.promises.futimes(filehandle, atime, mtime) +### fsPromises.futimes(filehandle, atime, mtime) @@ -3772,7 +3772,7 @@ Change the file system timestamps of the object referenced by the supplied This function does not work on AIX versions before 7.1, it will resolve the `Promise` with an error using code `UV_ENOSYS`. -### fs.promises.lchmod(path, mode) +### fsPromises.lchmod(path, mode) @@ -3784,7 +3784,7 @@ deprecated: REPLACEME Changes the permissions on a symbolic link then resolves the `Promise` with no arguments upon success. This method is only implemented on macOS. -### fs.promises.lchown(path, uid, gid) +### fsPromises.lchown(path, uid, gid) @@ -3797,7 +3797,7 @@ deprecated: REPLACEME Changes the ownership on a symbolic link then resolves the `Promise` with no arguments upon success. This method is only implemented on macOS. -### fs.promises.link(existingPath, newPath) +### fsPromises.link(existingPath, newPath) @@ -3808,7 +3808,7 @@ added: REPLACEME Asynchronous link(2). The `Promise` is resolved with no arguments upon success. -### fs.promises.lstat(path) +### fsPromises.lstat(path) @@ -3819,7 +3819,7 @@ added: REPLACEME Asynchronous lstat(2). The `Promise` is resolved with the [`fs.Stats`][] object for the given symbolic link `path`. -### fs.promises.mkdir(path[, mode]) +### fsPromises.mkdir(path[, mode]) @@ -3831,7 +3831,7 @@ added: REPLACEME Asynchronously creates a directory then resolves the `Promise` with no arguments upon success. -### fs.promises.mkdtemp(prefix[, options]) +### fsPromises.mkdtemp(prefix[, options]) @@ -3851,7 +3851,7 @@ object with an `encoding` property specifying the character encoding to use. Example: ```js -fs.promises.mkdtemp(path.join(os.tmpdir(), 'foo-')) +fsPromises.mkdtemp(path.join(os.tmpdir(), 'foo-')) .catch(console.error); ``` @@ -3861,7 +3861,7 @@ intention is to create a temporary directory *within* `/tmp`, the `prefix` *must* end with a trailing platform-specific path separator (`require('path').sep`). -### fs.promises.open(path, flags[, mode]) +### fsPromises.open(path, flags[, mode]) @@ -3889,7 +3889,7 @@ An exception occurs if the file does not exist. the potentially stale local cache. It has a very real impact on I/O performance so using this flag is not recommended unless it is needed. - Note that this does not turn `fs.promises.open()` into a synchronous blocking + Note that this does not turn `fsPromises.open()` into a synchronous blocking call. * `'w'` - Open file for writing. @@ -3929,7 +3929,7 @@ On Linux, positional writes don't work when the file is opened in append mode. The kernel ignores the position argument and always appends the data to the end of the file. -The behavior of `fs.promises.open()` is platform-specific for some +The behavior of `fsPromises.open()` is platform-specific for some flags. As such, opening a directory on macOS and Linux with the `'a+'` flag will return an error. In contrast, on Windows and FreeBSD, a `FileHandle` will be returned. @@ -3940,11 +3940,11 @@ a colon, Node.js will open a file system stream, as described by [this MSDN page][MSDN-Using-Streams]. *Note:* On Windows, opening an existing hidden file using the `w` flag (e.g. -using `fs.promises.open()`) will fail with `EPERM`. Existing hidden +using `fsPromises.open()`) will fail with `EPERM`. Existing hidden files can be opened for writing with the `r+` flag. A call to -`fs.promises.ftruncate()` can be used to reset the file contents. +`fsPromises.ftruncate()` can be used to reset the file contents. -### fs.promises.read(filehandle, buffer, offset, length, position) +### fsPromises.read(filehandle, buffer, offset, length, position) @@ -3973,7 +3973,7 @@ Following successful read, the `Promise` is resolved with an object with a `bytesRead` property specifying the number of bytes read, and a `buffer` property that is a reference to the passed in `buffer` argument. -### fs.promises.readdir(path[, options]) +### fsPromises.readdir(path[, options]) @@ -3991,7 +3991,7 @@ object with an `encoding` property specifying the character encoding to use for the filenames. If the `encoding` is set to `'buffer'`, the filenames returned will be passed as `Buffer` objects. -### fs.promises.readFile(path[, options]) +### fsPromises.readFile(path[, options]) @@ -4010,14 +4010,14 @@ object. Otherwise, the data will be a string. If `options` is a string, then it specifies the encoding. -When the `path` is a directory, the behavior of `fs.promises.readFile()` is +When the `path` is a directory, the behavior of `fsPromises.readFile()` is platform-specific. On macOS, Linux, and Windows, the promise will be rejected with an error. On FreeBSD, a representation of the directory's contents will be returned. Any specified `FileHandle` has to support reading. -### fs.promises.readlink(path[, options]) +### fsPromises.readlink(path[, options]) @@ -4035,7 +4035,7 @@ object with an `encoding` property specifying the character encoding to use for the link path returned. If the `encoding` is set to `'buffer'`, the link path returned will be passed as a `Buffer` object. -### fs.promises.realpath(path[, options]) +### fsPromises.realpath(path[, options]) @@ -4060,7 +4060,7 @@ On Linux, when Node.js is linked against musl libc, the procfs file system must be mounted on `/proc` in order for this function to work. Glibc does not have this restriction. -### fs.promises.rename(oldPath, newPath) +### fsPromises.rename(oldPath, newPath) @@ -4072,7 +4072,7 @@ added: REPLACEME Renames `oldPath` to `newPath` and resolves the `Promise` with no arguments upon success. -### fs.promises.rmdir(path) +### fsPromises.rmdir(path) @@ -4083,11 +4083,11 @@ added: REPLACEME Removes the directory identified by `path` then resolves the `Promise` with no arguments upon success. -Using `fs.promises.rmdir()` on a file (not a directory) results in the +Using `fsPromises.rmdir()` on a file (not a directory) results in the `Promise` being rejected with an `ENOENT` error on Windows and an `ENOTDIR` error on POSIX. -### fs.promises.stat(path) +### fsPromises.stat(path) @@ -4097,7 +4097,7 @@ added: REPLACEME The `Promise` is resolved with the [`fs.Stats`][] object for the given `path`. -### fs.promises.symlink(target, path[, type]) +### fsPromises.symlink(target, path[, type]) @@ -4115,7 +4115,7 @@ The `type` argument is only used on Windows platforms and can be one of `'dir'`, points require the destination path to be absolute. When using `'junction'`, the `target` argument will automatically be normalized to absolute path. -### fs.promises.truncate(path[, len]) +### fsPromises.truncate(path[, len]) @@ -4127,7 +4127,7 @@ added: REPLACEME Truncates the `path` then resolves the `Promise` with no arguments upon success. The `path` *must* be a string or `Buffer`. -### fs.promises.unlink(path) +### fsPromises.unlink(path) @@ -4138,7 +4138,7 @@ added: REPLACEME Asynchronous unlink(2). The `Promise` is resolved with no arguments upon success. -### fs.promises.utimes(path, atime, mtime) +### fsPromises.utimes(path, atime, mtime) @@ -4157,7 +4157,7 @@ The `atime` and `mtime` arguments follow these rules: - If the value can not be converted to a number, or is `NaN`, `Infinity` or `-Infinity`, an `Error` will be thrown. -### fs.promises.write(filehandle, buffer[, offset[, length[, position]]]) +### fsPromises.write(filehandle, buffer[, offset[, length[, position]]]) @@ -4182,7 +4182,7 @@ an integer specifying the number of bytes to write. should be written. If `typeof position !== 'number'`, the data will be written at the current position. See pwrite(2). -It is unsafe to use `fs.promises.write()` multiple times on the same file +It is unsafe to use `fsPromises.write()` multiple times on the same file without waiting for the `Promise` to be resolved (or rejected). For this scenario, `fs.createWriteStream` is strongly recommended. @@ -4190,7 +4190,7 @@ On Linux, positional writes do not work when the file is opened in append mode. The kernel ignores the position argument and always appends the data to the end of the file. -### fs.promises.writeFile(file, data[, options]) +### fsPromises.writeFile(file, data[, options]) @@ -4214,7 +4214,7 @@ If `options` is a string, then it specifies the encoding. Any specified `FileHandle` has to support writing. -It is unsafe to use `fs.promises.writeFile()` multiple times on the same file +It is unsafe to use `fsPromises.writeFile()` multiple times on the same file without waiting for the `Promise` to be resolved (or rejected). diff --git a/lib/fs.js b/lib/fs.js index f78703762975b9..92e8d4fe3e2d43 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -72,9 +72,6 @@ Object.defineProperty(exports, 'constants', { value: constants }); -const kHandle = Symbol('handle'); -const { kUsePromises } = binding; - const kMinPoolSpace = 128; const { kMaxLength } = require('buffer'); @@ -2284,459 +2281,3 @@ Object.defineProperty(fs, 'SyncWriteStream', { set: internalUtil.deprecate((val) => { SyncWriteStream = val; }, 'fs.SyncWriteStream is deprecated.', 'DEP0061') }); - -// Promises API - -class FileHandle { - constructor(filehandle) { - this[kHandle] = filehandle; - } - - getAsyncId() { - return this[kHandle].getAsyncId(); - } - - get fd() { - return this[kHandle].fd; - } - - appendFile(data, options) { - return promises.appendFile(this, data, options); - } - - chmod(mode) { - return promises.fchmod(this, mode); - } - - chown(uid, gid) { - return promises.fchown(this, uid, gid); - } - - datasync() { - return promises.fdatasync(this); - } - - sync() { - return promises.fsync(this); - } - - - read(buffer, offset, length, position) { - return promises.read(this, buffer, offset, length, position); - } - - readFile(options) { - return promises.readFile(this, options); - } - - stat() { - return promises.fstat(this); - } - - truncate(len = 0) { - return promises.ftruncate(this, len); - } - - utimes(atime, mtime) { - return promises.futimes(this, atime, mtime); - } - - write(buffer, offset, length, position) { - return promises.write(this, buffer, offset, length, position); - } - - writeFile(data, options) { - return promises.writeFile(this, data, options); - } - - close() { - return this[kHandle].close(); - } -} - - -function validateFileHandle(handle) { - if (!(handle instanceof FileHandle)) - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', - 'filehandle', 'FileHandle'); -} - -async function writeFileHandle(filehandle, data, options) { - let buffer = isUint8Array(data) ? - data : Buffer.from('' + data, options.encoding || 'utf8'); - let remaining = buffer.length; - if (remaining === 0) return; - do { - const { bytesWritten } = - await promises.write(filehandle, buffer, 0, - Math.min(16384, buffer.length)); - remaining -= bytesWritten; - buffer = buffer.slice(bytesWritten); - } while (remaining > 0); -} - -async function readFileHandle(filehandle, options) { - const statFields = await binding.fstat(filehandle.fd, kUsePromises); - - let size; - if ((statFields[1/*mode*/] & S_IFMT) === S_IFREG) { - size = statFields[8/*size*/]; - } else { - size = 0; - } - - if (size === 0) - return Buffer.alloc(0); - - if (size > kMaxLength) - throw new errors.RangeError('ERR_BUFFER_TOO_LARGE'); - - const chunks = []; - const chunkSize = Math.min(size, 16384); - const buf = Buffer.alloc(chunkSize); - let read = 0; - do { - const { bytesRead, buffer } = - await promises.read(filehandle, buf, 0, buf.length); - read = bytesRead; - if (read > 0) - chunks.push(buffer.slice(0, read)); - } while (read === chunkSize); - - return Buffer.concat(chunks); -} - -// All of the functions in fs.promises are defined as async in order to -// ensure that errors thrown cause promise rejections rather than being -// thrown synchronously -const promises = { - async access(path, mode = fs.F_OK) { - path = getPathFromURL(path); - validatePath(path); - - mode = mode | 0; - return binding.access(pathModule.toNamespacedPath(path), mode, - kUsePromises); - }, - - async copyFile(src, dest, flags) { - src = getPathFromURL(src); - dest = getPathFromURL(dest); - validatePath(src, 'src'); - validatePath(dest, 'dest'); - flags = flags | 0; - return binding.copyFile(pathModule.toNamespacedPath(src), - pathModule.toNamespacedPath(dest), - flags, kUsePromises); - }, - - // Note that unlike fs.open() which uses numeric file descriptors, - // promises.open() uses the fs.FileHandle class. - async open(path, flags, mode) { - mode = modeNum(mode, 0o666); - path = getPathFromURL(path); - validatePath(path); - validateUint32(mode, 'mode'); - return new FileHandle( - await binding.openFileHandle(pathModule.toNamespacedPath(path), - stringToFlags(flags), - mode, kUsePromises)); - }, - - async read(handle, buffer, offset, length, position) { - validateFileHandle(handle); - validateBuffer(buffer); - - offset |= 0; - length |= 0; - - if (length === 0) - return { bytesRead: length, buffer }; - - validateOffsetLengthRead(offset, length, buffer.length); - - if (!isUint32(position)) - position = -1; - - const bytesRead = (await binding.read(handle.fd, buffer, offset, length, - position, kUsePromises)) || 0; - - return { bytesRead, buffer }; - }, - - async write(handle, buffer, offset, length, position) { - validateFileHandle(handle); - - if (buffer.length === 0) - return { bytesWritten: 0, buffer }; - - if (isUint8Array(buffer)) { - if (typeof offset !== 'number') - offset = 0; - if (typeof length !== 'number') - length = buffer.length - offset; - if (typeof position !== 'number') - position = null; - validateOffsetLengthWrite(offset, length, buffer.byteLength); - const bytesWritten = - (await binding.writeBuffer(handle.fd, buffer, offset, - length, position, kUsePromises)) || 0; - return { bytesWritten, buffer }; - } - - if (typeof buffer !== 'string') - buffer += ''; - if (typeof position !== 'function') { - if (typeof offset === 'function') { - position = offset; - offset = null; - } else { - position = length; - } - length = 'utf8'; - } - const bytesWritten = (await binding.writeString(handle.fd, buffer, offset, - length, kUsePromises)) || 0; - return { bytesWritten, buffer }; - }, - - async rename(oldPath, newPath) { - oldPath = getPathFromURL(oldPath); - newPath = getPathFromURL(newPath); - validatePath(oldPath, 'oldPath'); - validatePath(newPath, 'newPath'); - return binding.rename(pathModule.toNamespacedPath(oldPath), - pathModule.toNamespacedPath(newPath), - kUsePromises); - }, - - async truncate(path, len = 0) { - return promises.ftruncate(await promises.open(path, 'r+'), len); - }, - - async ftruncate(handle, len = 0) { - validateFileHandle(handle); - validateLen(len); - len = Math.max(0, len); - return binding.ftruncate(handle.fd, len, kUsePromises); - }, - - async rmdir(path) { - path = getPathFromURL(path); - validatePath(path); - return binding.rmdir(pathModule.toNamespacedPath(path), kUsePromises); - }, - - async fdatasync(handle) { - validateFileHandle(handle); - return binding.fdatasync(handle.fd, kUsePromises); - }, - - async fsync(handle) { - validateFileHandle(handle); - return binding.fsync(handle.fd, kUsePromises); - }, - - async mkdir(path, mode) { - mode = modeNum(mode, 0o777); - path = getPathFromURL(path); - validatePath(path); - validateUint32(mode, 'mode'); - return binding.mkdir(pathModule.toNamespacedPath(path), mode, kUsePromises); - }, - - async readdir(path, options) { - options = getOptions(options, {}); - path = getPathFromURL(path); - validatePath(path); - return binding.readdir(pathModule.toNamespacedPath(path), - options.encoding, kUsePromises); - }, - - async readlink(path, options) { - options = getOptions(options, {}); - path = getPathFromURL(path); - validatePath(path, 'oldPath'); - return binding.readlink(pathModule.toNamespacedPath(path), - options.encoding, kUsePromises); - }, - - async symlink(target, path, type_) { - const type = (typeof type_ === 'string' ? type_ : null); - target = getPathFromURL(target); - path = getPathFromURL(path); - validatePath(target, 'target'); - validatePath(path); - return binding.symlink(preprocessSymlinkDestination(target, type, path), - pathModule.toNamespacedPath(path), - stringToSymlinkType(type), - kUsePromises); - }, - - async fstat(handle) { - validateFileHandle(handle); - return statsFromValues(await binding.fstat(handle.fd, kUsePromises)); - }, - - async lstat(path) { - path = getPathFromURL(path); - validatePath(path); - return statsFromValues( - await binding.lstat(pathModule.toNamespacedPath(path), kUsePromises)); - }, - - async stat(path) { - path = getPathFromURL(path); - validatePath(path); - return statsFromValues( - await binding.stat(pathModule.toNamespacedPath(path), kUsePromises)); - }, - - async link(existingPath, newPath) { - existingPath = getPathFromURL(existingPath); - newPath = getPathFromURL(newPath); - validatePath(existingPath, 'existingPath'); - validatePath(newPath, 'newPath'); - return binding.link(pathModule.toNamespacedPath(existingPath), - pathModule.toNamespacedPath(newPath), - kUsePromises); - }, - - async unlink(path) { - path = getPathFromURL(path); - validatePath(path); - return binding.unlink(pathModule.toNamespacedPath(path), kUsePromises); - }, - - async fchmod(handle, mode) { - mode = modeNum(mode); - validateFileHandle(handle); - validateUint32(mode, 'mode'); - if (mode < 0 || mode > 0o777) - throw new errors.RangeError('ERR_OUT_OF_RANGE', 'mode'); - return binding.fchmod(handle.fd, mode, kUsePromises); - }, - - async chmod(path, mode) { - path = getPathFromURL(path); - validatePath(path); - mode = modeNum(mode); - validateUint32(mode, 'mode'); - return binding.chmod(pathModule.toNamespacedPath(path), mode, kUsePromises); - }, - - async lchmod(path, mode) { - if (constants.O_SYMLINK !== undefined) { - const fd = await promises.open(path, - constants.O_WRONLY | constants.O_SYMLINK); - return promises.fchmod(fd, mode).finally(fd.close.bind(fd)); - } - throw new errors.Error('ERR_METHOD_NOT_IMPLEMENTED'); - }, - - async lchown(path, uid, gid) { - if (constants.O_SYMLINK !== undefined) { - const fd = await promises.open(path, - constants.O_WRONLY | constants.O_SYMLINK); - return promises.fchown(fd, uid, gid).finally(fd.close.bind(fd)); - } - throw new errors.Error('ERR_METHOD_NOT_IMPLEMENTED'); - }, - - async fchown(handle, uid, gid) { - validateFileHandle(handle); - validateUint32(uid, 'uid'); - validateUint32(gid, 'gid'); - return binding.fchown(handle.fd, uid, gid, kUsePromises); - }, - - async chown(path, uid, gid) { - path = getPathFromURL(path); - validatePath(path); - validateUint32(uid, 'uid'); - validateUint32(gid, 'gid'); - return binding.chown(pathModule.toNamespacedPath(path), - uid, gid, kUsePromises); - }, - - async utimes(path, atime, mtime) { - path = getPathFromURL(path); - validatePath(path); - return binding.utimes(pathModule.toNamespacedPath(path), - toUnixTimestamp(atime), - toUnixTimestamp(mtime), - kUsePromises); - }, - - async futimes(handle, atime, mtime) { - validateFileHandle(handle); - atime = toUnixTimestamp(atime, 'atime'); - mtime = toUnixTimestamp(mtime, 'mtime'); - return binding.futimes(handle.fd, atime, mtime, kUsePromises); - }, - - async realpath(path, options) { - options = getOptions(options, {}); - path = getPathFromURL(path); - validatePath(path); - return binding.realpath(path, options.encoding, kUsePromises); - }, - - async mkdtemp(prefix, options) { - options = getOptions(options, {}); - if (!prefix || typeof prefix !== 'string') { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', - 'prefix', - 'string', - prefix); - } - nullCheck(prefix); - return binding.mkdtemp(`${prefix}XXXXXX`, options.encoding, kUsePromises); - }, - - async writeFile(path, data, options) { - options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' }); - const flag = options.flag || 'w'; - - if (path instanceof FileHandle) - return writeFileHandle(path, data, options); - - const fd = await promises.open(path, flag, options.mode); - return writeFileHandle(fd, data, options).finally(fd.close.bind(fd)); - }, - - async appendFile(path, data, options) { - options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' }); - options = copyObject(options); - options.flag = options.flag || 'a'; - return promises.writeFile(path, data, options); - }, - - async readFile(path, options) { - options = getOptions(options, { flag: 'r' }); - - if (path instanceof FileHandle) - return readFileHandle(path, options); - - const fd = await promises.open(path, options.flag, 0o666); - return readFileHandle(fd, options).finally(fd.close.bind(fd)); - } -}; - -let warn = true; - -// TODO(jasnell): Exposing this as a property with a getter works fine with -// commonjs but is going to be problematic for named imports support under -// ESM. A different approach will have to be followed there. -Object.defineProperty(fs, 'promises', { - configurable: true, - enumerable: true, - get() { - if (warn) { - warn = false; - process.emitWarning('The fs.promises API is experimental', - 'ExperimentalWarning'); - } - return promises; - } -}); diff --git a/lib/fs/promises.js b/lib/fs/promises.js new file mode 100644 index 00000000000000..8cbcd963c772c8 --- /dev/null +++ b/lib/fs/promises.js @@ -0,0 +1,507 @@ +'use strict'; + +process.emitWarning('The fs/promises API is experimental', + 'ExperimentalWarning'); + +const { + F_OK, + O_SYMLINK, + O_WRONLY, + S_IFMT, + S_IFREG +} = process.binding('constants').fs; +const binding = process.binding('fs'); +const { Buffer, kMaxLength } = require('buffer'); +const errors = require('internal/errors'); +const { getPathFromURL } = require('internal/url'); +const { isUint8Array } = require('internal/util/types'); +const { + copyObject, + getOptions, + isUint32, + modeNum, + nullCheck, + preprocessSymlinkDestination, + statsFromValues, + stringToFlags, + stringToSymlinkType, + toUnixTimestamp, + validateBuffer, + validateLen, + validateOffsetLengthRead, + validateOffsetLengthWrite, + validatePath, + validateUint32 +} = require('internal/fs'); +const pathModule = require('path'); + +const kHandle = Symbol('handle'); +const { kUsePromises } = binding; + +class FileHandle { + constructor(filehandle) { + this[kHandle] = filehandle; + } + + getAsyncId() { + return this[kHandle].getAsyncId(); + } + + get fd() { + return this[kHandle].fd; + } + + appendFile(data, options) { + return appendFile(this, data, options); + } + + chmod(mode) { + return fchmod(this, mode); + } + + chown(uid, gid) { + return fchown(this, uid, gid); + } + + datasync() { + return fdatasync(this); + } + + sync() { + return fsync(this); + } + + read(buffer, offset, length, position) { + return read(this, buffer, offset, length, position); + } + + readFile(options) { + return readFile(this, options); + } + + stat() { + return fstat(this); + } + + truncate(len = 0) { + return ftruncate(this, len); + } + + utimes(atime, mtime) { + return futimes(this, atime, mtime); + } + + write(buffer, offset, length, position) { + return write(this, buffer, offset, length, position); + } + + writeFile(data, options) { + return writeFile(this, data, options); + } + + close() { + return this[kHandle].close(); + } +} + + +function validateFileHandle(handle) { + if (!(handle instanceof FileHandle)) + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', + 'filehandle', 'FileHandle'); +} + +async function writeFileHandle(filehandle, data, options) { + let buffer = isUint8Array(data) ? + data : Buffer.from('' + data, options.encoding || 'utf8'); + let remaining = buffer.length; + if (remaining === 0) return; + do { + const { bytesWritten } = + await write(filehandle, buffer, 0, + Math.min(16384, buffer.length)); + remaining -= bytesWritten; + buffer = buffer.slice(bytesWritten); + } while (remaining > 0); +} + +async function readFileHandle(filehandle, options) { + const statFields = await binding.fstat(filehandle.fd, kUsePromises); + + let size; + if ((statFields[1/*mode*/] & S_IFMT) === S_IFREG) { + size = statFields[8/*size*/]; + } else { + size = 0; + } + + if (size === 0) + return Buffer.alloc(0); + + if (size > kMaxLength) + throw new errors.RangeError('ERR_BUFFER_TOO_LARGE'); + + const chunks = []; + const chunkSize = Math.min(size, 16384); + const buf = Buffer.alloc(chunkSize); + let totalRead = 0; + do { + const { bytesRead, buffer } = + await read(filehandle, buf, 0, buf.length); + totalRead = bytesRead; + if (totalRead > 0) + chunks.push(buffer.slice(0, totalRead)); + } while (totalRead === chunkSize); + + return Buffer.concat(chunks); +} + +// All of the functions are defined as async in order to ensure that errors +// thrown cause promise rejections rather than being thrown synchronously. +async function access(path, mode = F_OK) { + path = getPathFromURL(path); + validatePath(path); + + mode = mode | 0; + return binding.access(pathModule.toNamespacedPath(path), mode, + kUsePromises); +} + +async function copyFile(src, dest, flags) { + src = getPathFromURL(src); + dest = getPathFromURL(dest); + validatePath(src, 'src'); + validatePath(dest, 'dest'); + flags = flags | 0; + return binding.copyFile(pathModule.toNamespacedPath(src), + pathModule.toNamespacedPath(dest), + flags, kUsePromises); +} + +// Note that unlike fs.open() which uses numeric file descriptors, +// fsPromises.open() uses the fs.FileHandle class. +async function open(path, flags, mode) { + mode = modeNum(mode, 0o666); + path = getPathFromURL(path); + validatePath(path); + validateUint32(mode, 'mode'); + return new FileHandle( + await binding.openFileHandle(pathModule.toNamespacedPath(path), + stringToFlags(flags), + mode, kUsePromises)); +} + +async function read(handle, buffer, offset, length, position) { + validateFileHandle(handle); + validateBuffer(buffer); + + offset |= 0; + length |= 0; + + if (length === 0) + return { bytesRead: length, buffer }; + + validateOffsetLengthRead(offset, length, buffer.length); + + if (!isUint32(position)) + position = -1; + + const bytesRead = (await binding.read(handle.fd, buffer, offset, length, + position, kUsePromises)) || 0; + + return { bytesRead, buffer }; +} + +async function write(handle, buffer, offset, length, position) { + validateFileHandle(handle); + + if (buffer.length === 0) + return { bytesWritten: 0, buffer }; + + if (isUint8Array(buffer)) { + if (typeof offset !== 'number') + offset = 0; + if (typeof length !== 'number') + length = buffer.length - offset; + if (typeof position !== 'number') + position = null; + validateOffsetLengthWrite(offset, length, buffer.byteLength); + const bytesWritten = + (await binding.writeBuffer(handle.fd, buffer, offset, + length, position, kUsePromises)) || 0; + return { bytesWritten, buffer }; + } + + if (typeof buffer !== 'string') + buffer += ''; + if (typeof position !== 'function') { + if (typeof offset === 'function') { + position = offset; + offset = null; + } else { + position = length; + } + length = 'utf8'; + } + const bytesWritten = (await binding.writeString(handle.fd, buffer, offset, + length, kUsePromises)) || 0; + return { bytesWritten, buffer }; +} + +async function rename(oldPath, newPath) { + oldPath = getPathFromURL(oldPath); + newPath = getPathFromURL(newPath); + validatePath(oldPath, 'oldPath'); + validatePath(newPath, 'newPath'); + return binding.rename(pathModule.toNamespacedPath(oldPath), + pathModule.toNamespacedPath(newPath), + kUsePromises); +} + +async function truncate(path, len = 0) { + return ftruncate(await open(path, 'r+'), len); +} + +async function ftruncate(handle, len = 0) { + validateFileHandle(handle); + validateLen(len); + len = Math.max(0, len); + return binding.ftruncate(handle.fd, len, kUsePromises); +} + +async function rmdir(path) { + path = getPathFromURL(path); + validatePath(path); + return binding.rmdir(pathModule.toNamespacedPath(path), kUsePromises); +} + +async function fdatasync(handle) { + validateFileHandle(handle); + return binding.fdatasync(handle.fd, kUsePromises); +} + +async function fsync(handle) { + validateFileHandle(handle); + return binding.fsync(handle.fd, kUsePromises); +} + +async function mkdir(path, mode) { + mode = modeNum(mode, 0o777); + path = getPathFromURL(path); + validatePath(path); + validateUint32(mode, 'mode'); + return binding.mkdir(pathModule.toNamespacedPath(path), mode, kUsePromises); +} + +async function readdir(path, options) { + options = getOptions(options, {}); + path = getPathFromURL(path); + validatePath(path); + return binding.readdir(pathModule.toNamespacedPath(path), + options.encoding, kUsePromises); +} + +async function readlink(path, options) { + options = getOptions(options, {}); + path = getPathFromURL(path); + validatePath(path, 'oldPath'); + return binding.readlink(pathModule.toNamespacedPath(path), + options.encoding, kUsePromises); +} + +async function symlink(target, path, type_) { + const type = (typeof type_ === 'string' ? type_ : null); + target = getPathFromURL(target); + path = getPathFromURL(path); + validatePath(target, 'target'); + validatePath(path); + return binding.symlink(preprocessSymlinkDestination(target, type, path), + pathModule.toNamespacedPath(path), + stringToSymlinkType(type), + kUsePromises); +} + +async function fstat(handle) { + validateFileHandle(handle); + return statsFromValues(await binding.fstat(handle.fd, kUsePromises)); +} + +async function lstat(path) { + path = getPathFromURL(path); + validatePath(path); + return statsFromValues( + await binding.lstat(pathModule.toNamespacedPath(path), kUsePromises)); +} + +async function stat(path) { + path = getPathFromURL(path); + validatePath(path); + return statsFromValues( + await binding.stat(pathModule.toNamespacedPath(path), kUsePromises)); +} + +async function link(existingPath, newPath) { + existingPath = getPathFromURL(existingPath); + newPath = getPathFromURL(newPath); + validatePath(existingPath, 'existingPath'); + validatePath(newPath, 'newPath'); + return binding.link(pathModule.toNamespacedPath(existingPath), + pathModule.toNamespacedPath(newPath), + kUsePromises); +} + +async function unlink(path) { + path = getPathFromURL(path); + validatePath(path); + return binding.unlink(pathModule.toNamespacedPath(path), kUsePromises); +} + +async function fchmod(handle, mode) { + mode = modeNum(mode); + validateFileHandle(handle); + validateUint32(mode, 'mode'); + if (mode < 0 || mode > 0o777) + throw new errors.RangeError('ERR_OUT_OF_RANGE', 'mode'); + return binding.fchmod(handle.fd, mode, kUsePromises); +} + +async function chmod(path, mode) { + path = getPathFromURL(path); + validatePath(path); + mode = modeNum(mode); + validateUint32(mode, 'mode'); + return binding.chmod(pathModule.toNamespacedPath(path), mode, kUsePromises); +} + +async function lchmod(path, mode) { + if (O_SYMLINK !== undefined) { + const fd = await open(path, + O_WRONLY | O_SYMLINK); + return fchmod(fd, mode).finally(fd.close.bind(fd)); + } + throw new errors.Error('ERR_METHOD_NOT_IMPLEMENTED'); +} + +async function lchown(path, uid, gid) { + if (O_SYMLINK !== undefined) { + const fd = await open(path, + O_WRONLY | O_SYMLINK); + return fchmod(fd, uid, gid).finally(fd.close.bind(fd)); + } + throw new errors.Error('ERR_METHOD_NOT_IMPLEMENTED'); +} + +async function fchown(handle, uid, gid) { + validateFileHandle(handle); + validateUint32(uid, 'uid'); + validateUint32(gid, 'gid'); + return binding.fchown(handle.fd, uid, gid, kUsePromises); +} + +async function chown(path, uid, gid) { + path = getPathFromURL(path); + validatePath(path); + validateUint32(uid, 'uid'); + validateUint32(gid, 'gid'); + return binding.chown(pathModule.toNamespacedPath(path), + uid, gid, kUsePromises); +} + +async function utimes(path, atime, mtime) { + path = getPathFromURL(path); + validatePath(path); + return binding.utimes(pathModule.toNamespacedPath(path), + toUnixTimestamp(atime), + toUnixTimestamp(mtime), + kUsePromises); +} + +async function futimes(handle, atime, mtime) { + validateFileHandle(handle); + atime = toUnixTimestamp(atime, 'atime'); + mtime = toUnixTimestamp(mtime, 'mtime'); + return binding.futimes(handle.fd, atime, mtime, kUsePromises); +} + +async function realpath(path, options) { + options = getOptions(options, {}); + path = getPathFromURL(path); + validatePath(path); + return binding.realpath(path, options.encoding, kUsePromises); +} + +async function mkdtemp(prefix, options) { + options = getOptions(options, {}); + if (!prefix || typeof prefix !== 'string') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', + 'prefix', + 'string', + prefix); + } + nullCheck(prefix); + return binding.mkdtemp(`${prefix}XXXXXX`, options.encoding, kUsePromises); +} + +async function writeFile(path, data, options) { + options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' }); + const flag = options.flag || 'w'; + + if (path instanceof FileHandle) + return writeFileHandle(path, data, options); + + const fd = await open(path, flag, options.mode); + return writeFileHandle(fd, data, options).finally(fd.close.bind(fd)); +} + +async function appendFile(path, data, options) { + options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' }); + options = copyObject(options); + options.flag = options.flag || 'a'; + return writeFile(path, data, options); +} + +async function readFile(path, options) { + options = getOptions(options, { flag: 'r' }); + + if (path instanceof FileHandle) + return readFileHandle(path, options); + + const fd = await open(path, options.flag, 0o666); + return readFileHandle(fd, options).finally(fd.close.bind(fd)); +} + +module.exports = { + access, + copyFile, + open, + read, + write, + rename, + truncate, + ftruncate, + rmdir, + fdatasync, + fsync, + mkdir, + readdir, + readlink, + symlink, + fstat, + lstat, + stat, + link, + unlink, + fchmod, + chmod, + lchmod, + lchown, + fchown, + chown, + utimes, + futimes, + realpath, + mkdtemp, + writeFile, + appendFile, + readFile +}; diff --git a/node.gyp b/node.gyp index 8e41cc053d4a78..08eee428560770 100644 --- a/node.gyp +++ b/node.gyp @@ -39,6 +39,7 @@ 'lib/domain.js', 'lib/events.js', 'lib/fs.js', + 'lib/fs/promises.js', 'lib/http.js', 'lib/http2.js', 'lib/_http_agent.js', diff --git a/test/parallel/test-fs-promises-writefile.js b/test/parallel/test-fs-promises-writefile.js index 655dc73a1dfdb5..e2ae289b180bc2 100644 --- a/test/parallel/test-fs-promises-writefile.js +++ b/test/parallel/test-fs-promises-writefile.js @@ -2,6 +2,7 @@ const common = require('../common'); const fs = require('fs'); +const fsPromises = require('fs/promises'); const path = require('path'); const tmpdir = require('../common/tmpdir'); const assert = require('assert'); @@ -16,20 +17,20 @@ const buffer = Buffer.from('abc'.repeat(1000)); const buffer2 = Buffer.from('xyz'.repeat(1000)); async function doWrite() { - await fs.promises.writeFile(dest, buffer); + await fsPromises.writeFile(dest, buffer); const data = fs.readFileSync(dest); assert.deepStrictEqual(data, buffer); } async function doAppend() { - await fs.promises.appendFile(dest, buffer2); + await fsPromises.appendFile(dest, buffer2); const data = fs.readFileSync(dest); const buf = Buffer.concat([buffer, buffer2]); assert.deepStrictEqual(buf, data); } async function doRead() { - const data = await fs.promises.readFile(dest); + const data = await fsPromises.readFile(dest); const buf = fs.readFileSync(dest); assert.deepStrictEqual(buf, data); } diff --git a/test/parallel/test-fs-promises.js b/test/parallel/test-fs-promises.js index 5d493208ff85cb..e24374602bfc3f 100644 --- a/test/parallel/test-fs-promises.js +++ b/test/parallel/test-fs-promises.js @@ -5,7 +5,7 @@ const assert = require('assert'); const tmpdir = require('../common/tmpdir'); const fixtures = require('../common/fixtures'); const path = require('path'); -const fs = require('fs'); +const fsPromises = require('fs/promises'); const { access, chmod, @@ -32,7 +32,7 @@ const { write, unlink, utimes -} = fs.promises; +} = fsPromises; const tmpDir = tmpdir.path; diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index 2db6ba9db661cf..1877d53dbfd3ae 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -3,6 +3,7 @@ const common = require('../common'); const assert = require('assert'); const fs = require('fs'); +const fsPromises = require('fs/promises'); const net = require('net'); const providers = Object.assign({}, process.binding('async_wrap').Providers); const fixtures = require('../common/fixtures'); @@ -171,7 +172,7 @@ if (common.hasCrypto) { // eslint-disable-line crypto-check { async function openTest() { - const fd = await fs.promises.open(__filename, 'r'); + const fd = await fsPromises.open(__filename, 'r'); testInitialized(fd, 'FileHandle'); await fd.close(); }