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 2210d7ecb3b96f..b66632512c3a15 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -3215,9 +3215,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 @@ -3531,18 +3531,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]) @@ -3562,9 +3562,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) @@ -3576,7 +3576,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) @@ -3589,7 +3589,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]) @@ -3616,7 +3616,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')); ``` @@ -3629,12 +3629,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) @@ -3646,7 +3646,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) @@ -3659,7 +3659,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) @@ -3670,7 +3670,7 @@ added: REPLACEME Asynchronous fdatasync(2). The `Promise` is resolved with no arguments upon success. -### fs.promises.fstat(filehandle) +### fsPromises.fstat(filehandle) @@ -3680,7 +3680,7 @@ added: REPLACEME Retrieves the [`fs.Stats`][] for the given `filehandle`. -### fs.promises.fsync(filehandle) +### fsPromises.fsync(filehandle) @@ -3691,7 +3691,7 @@ added: REPLACEME Asynchronous fsync(2). The `Promise` is resolved with no arguments upon success. -### fs.promises.ftruncate(filehandle[, len]) +### fsPromises.ftruncate(filehandle[, len]) @@ -3714,8 +3714,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 } @@ -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, 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 } @@ -3740,7 +3740,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) @@ -3756,7 +3756,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) @@ -3768,7 +3768,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) @@ -3781,7 +3781,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) @@ -3792,7 +3792,7 @@ added: REPLACEME Asynchronous link(2). The `Promise` is resolved with no arguments upon success. -### fs.promises.lstat(path) +### fsPromises.lstat(path) @@ -3803,7 +3803,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]) @@ -3815,7 +3815,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]) @@ -3835,7 +3835,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); ``` @@ -3845,7 +3845,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]) @@ -3873,7 +3873,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. @@ -3913,7 +3913,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. @@ -3924,11 +3924,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) @@ -3957,7 +3957,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]) @@ -3975,7 +3975,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]) @@ -3994,14 +3994,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]) @@ -4019,7 +4019,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]) @@ -4044,7 +4044,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) @@ -4056,7 +4056,7 @@ added: REPLACEME Renames `oldPath` to `newPath` and resolves the `Promise` with no arguments upon success. -### fs.promises.rmdir(path) +### fsPromises.rmdir(path) @@ -4067,11 +4067,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) @@ -4081,7 +4081,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]) @@ -4099,7 +4099,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]) @@ -4111,7 +4111,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) @@ -4122,7 +4122,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) @@ -4141,7 +4141,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]]]) @@ -4166,7 +4166,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. @@ -4174,7 +4174,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]) @@ -4198,7 +4198,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 71eace0aef5b0a..c21227aabf156f 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -37,14 +37,29 @@ const { Buffer } = require('buffer'); const errors = require('internal/errors'); const { Readable, Writable } = require('stream'); const EventEmitter = require('events'); -const { FSReqWrap } = binding; +const { FSReqWrap, statValues } = binding; const { FSEvent } = process.binding('fs_event_wrap'); const internalFS = require('internal/fs'); const { getPathFromURL } = require('internal/url'); const internalUtil = require('internal/util'); const { - assertEncoding, - stringToFlags + copyObject, + getOptions, + isUint32, + modeNum, + nullCheck, + preprocessSymlinkDestination, + Stats, + statsFromValues, + stringToFlags, + stringToSymlinkType, + toUnixTimestamp, + validateBuffer, + validateLen, + validateOffsetLengthRead, + validateOffsetLengthWrite, + validatePath, + validateUint32 } = internalFS; Object.defineProperty(exports, 'constants', { @@ -53,9 +68,6 @@ Object.defineProperty(exports, 'constants', { value: constants }); -const kHandle = Symbol('handle'); -const { kUsePromises } = binding; - const kMinPoolSpace = 128; const { kMaxLength } = require('buffer'); @@ -66,9 +78,6 @@ const errnoException = errors.errnoException; let truncateWarn = true; -function isInt32(n) { return n === (n | 0); } -function isUint32(n) { return n === (n >>> 0); } - function showTruncateDeprecation() { if (truncateWarn) { process.emitWarning( @@ -79,35 +88,6 @@ function showTruncateDeprecation() { } } -function getOptions(options, defaultOptions) { - if (options === null || options === undefined || - typeof options === 'function') { - return defaultOptions; - } - - if (typeof options === 'string') { - defaultOptions = util._extend({}, defaultOptions); - defaultOptions.encoding = options; - options = defaultOptions; - } else if (typeof options !== 'object') { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', - 'options', - ['string', 'Object'], - options); - } - - if (options.encoding !== 'buffer') - assertEncoding(options.encoding); - return options; -} - -function copyObject(source) { - var target = {}; - for (var key in source) - target[key] = source[key]; - return target; -} - function handleErrorFromBinding(ctx) { if (ctx.errno !== undefined) { // libuv error numbers const err = errors.uvException(ctx); @@ -171,117 +151,6 @@ function makeCallback(cb) { }; } -function validateBuffer(buffer) { - if (!isUint8Array(buffer)) { - const err = new errors.TypeError('ERR_INVALID_ARG_TYPE', 'buffer', - ['Buffer', 'Uint8Array']); - Error.captureStackTrace(err, validateBuffer); - throw err; - } -} - -function validateLen(len) { - let err; - - if (!isInt32(len)) - err = new errors.TypeError('ERR_INVALID_ARG_TYPE', 'len', 'integer'); - - if (err !== undefined) { - Error.captureStackTrace(err, validateLen); - throw err; - } -} - -function validateOffsetLengthRead(offset, length, bufferLength) { - let err; - - if (offset < 0 || offset >= bufferLength) { - err = new errors.RangeError('ERR_OUT_OF_RANGE', 'offset'); - } else if (length < 0 || offset + length > bufferLength) { - err = new errors.RangeError('ERR_OUT_OF_RANGE', 'length'); - } - - if (err !== undefined) { - Error.captureStackTrace(err, validateOffsetLengthRead); - throw err; - } -} - -function validateOffsetLengthWrite(offset, length, byteLength) { - let err; - - if (offset > byteLength) { - err = new errors.RangeError('ERR_OUT_OF_RANGE', 'offset'); - } else if (offset + length > byteLength || offset + length > kMaxLength) { - err = new errors.RangeError('ERR_OUT_OF_RANGE', 'length'); - } - - if (err !== undefined) { - Error.captureStackTrace(err, validateOffsetLengthWrite); - throw err; - } -} - -// Check if the path contains null types if it is a string nor Uint8Array, -// otherwise return silently. -function nullCheck(path, propName, throwError = true) { - const pathIsString = typeof path === 'string'; - const pathIsUint8Array = isUint8Array(path); - - // We can only perform meaningful checks on strings and Uint8Arrays. - if (!pathIsString && !pathIsUint8Array) { - return; - } - - if (pathIsString && path.indexOf('\u0000') === -1) { - return; - } else if (pathIsUint8Array && path.indexOf(0) === -1) { - return; - } - - const err = new errors.Error( - 'ERR_INVALID_ARG_VALUE', propName, path, - 'must be a string or Uint8Array without null bytes'); - - if (throwError) { - Error.captureStackTrace(err, nullCheck); - throw err; - } - return err; -} - -function validatePath(path, propName) { - let err; - - if (propName === undefined) { - propName = 'path'; - } - - if (typeof path !== 'string' && !isUint8Array(path)) { - err = new errors.TypeError('ERR_INVALID_ARG_TYPE', propName, - ['string', 'Buffer', 'URL']); - } else { - err = nullCheck(path, propName, false); - } - - if (err !== undefined) { - Error.captureStackTrace(err, validatePath); - throw err; - } -} - -function validateUint32(value, propName) { - let err; - - if (!isUint32(value)) - err = new errors.TypeError('ERR_INVALID_ARG_TYPE', propName, 'integer'); - - if (err !== undefined) { - Error.captureStackTrace(err, validateUint32); - throw err; - } -} - // Special case of `makeCallback()` that is specific to async `*stat()` calls as // an optimization, since the data passed back to the callback needs to be // transformed anyway. @@ -304,85 +173,8 @@ function isFd(path) { return (path >>> 0) === path; } -// Constructor for file stats. -function Stats( - dev, - mode, - nlink, - uid, - gid, - rdev, - blksize, - ino, - size, - blocks, - atim_msec, - mtim_msec, - ctim_msec, - birthtim_msec -) { - this.dev = dev; - this.mode = mode; - this.nlink = nlink; - this.uid = uid; - this.gid = gid; - this.rdev = rdev; - this.blksize = blksize; - this.ino = ino; - this.size = size; - this.blocks = blocks; - this.atimeMs = atim_msec; - this.mtimeMs = mtim_msec; - this.ctimeMs = ctim_msec; - this.birthtimeMs = birthtim_msec; - this.atime = new Date(atim_msec + 0.5); - this.mtime = new Date(mtim_msec + 0.5); - this.ctime = new Date(ctim_msec + 0.5); - this.birthtime = new Date(birthtim_msec + 0.5); -} fs.Stats = Stats; -Stats.prototype._checkModeProperty = function(property) { - return ((this.mode & S_IFMT) === property); -}; - -Stats.prototype.isDirectory = function() { - return this._checkModeProperty(constants.S_IFDIR); -}; - -Stats.prototype.isFile = function() { - return this._checkModeProperty(S_IFREG); -}; - -Stats.prototype.isBlockDevice = function() { - return this._checkModeProperty(constants.S_IFBLK); -}; - -Stats.prototype.isCharacterDevice = function() { - return this._checkModeProperty(constants.S_IFCHR); -}; - -Stats.prototype.isSymbolicLink = function() { - return this._checkModeProperty(S_IFLNK); -}; - -Stats.prototype.isFIFO = function() { - return this._checkModeProperty(S_IFIFO); -}; - -Stats.prototype.isSocket = function() { - return this._checkModeProperty(S_IFSOCK); -}; - -const statValues = binding.statValues; - -function statsFromValues(stats = statValues) { - return new Stats(stats[0], stats[1], stats[2], stats[3], stats[4], stats[5], - stats[6] < 0 ? undefined : stats[6], stats[7], stats[8], - stats[9] < 0 ? undefined : stats[9], stats[10], stats[11], - stats[12], stats[13]); -} - // Don't allow mode to accidentally be overwritten. Object.defineProperties(fs, { F_OK: { enumerable: true, value: constants.F_OK || 0 }, @@ -756,16 +548,6 @@ fs.closeSync = function(fd) { handleErrorFromBinding(ctx); }; -function modeNum(m, def) { - if (typeof m === 'number') - return m; - if (typeof m === 'string') - return parseInt(m, 8); - if (def) - return modeNum(def); - return undefined; -} - fs.open = function(path, flags, mode, callback_) { var callback = makeCallback(arguments[arguments.length - 1]); mode = modeNum(mode, 0o666); @@ -1158,42 +940,6 @@ fs.readlinkSync = function(path, options) { return result; }; -function preprocessSymlinkDestination(path, type, linkPath) { - if (!isWindows) { - // No preprocessing is needed on Unix. - return path; - } else if (type === 'junction') { - // Junctions paths need to be absolute and \\?\-prefixed. - // A relative target is relative to the link's parent directory. - path = pathModule.resolve(linkPath, '..', path); - return pathModule.toNamespacedPath(path); - } else { - // Windows symlinks don't tolerate forward slashes. - return ('' + path).replace(/\//g, '\\'); - } -} - -function stringToSymlinkType(type) { - let flags = 0; - if (typeof type === 'string') { - switch (type) { - case 'dir': - flags |= constants.UV_FS_SYMLINK_DIR; - break; - case 'junction': - flags |= constants.UV_FS_SYMLINK_JUNCTION; - break; - case 'file': - break; - default: - const err = new errors.Error('ERR_FS_INVALID_SYMLINK_TYPE', type); - Error.captureStackTrace(err, stringToSymlinkType); - throw err; - } - } - return flags; -} - fs.symlink = function(target, path, type_, callback_) { var type = (typeof type_ === 'string' ? type_ : null); var callback = makeCallback(arguments[arguments.length - 1]); @@ -1417,28 +1163,6 @@ fs.chownSync = function(path, uid, gid) { return binding.chown(pathModule.toNamespacedPath(path), uid, gid); }; -// converts Date or number to a fractional UNIX timestamp -function toUnixTimestamp(time, name = 'time') { - // eslint-disable-next-line eqeqeq - if (typeof time === 'string' && +time == time) { - return +time; - } - if (Number.isFinite(time)) { - if (time < 0) { - return Date.now() / 1000; - } - return time; - } - if (util.isDate(time)) { - // convert to 123.456 UNIX timestamp - return time.getTime() / 1000; - } - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', - name, - ['Date', 'Time in seconds'], - time); -} - // exported for unit tests, not for public consumption fs._toUnixTimestamp = toUnixTimestamp; @@ -2551,459 +2275,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/lib/internal/fs.js b/lib/internal/fs.js index 01c5ff4bfc2e4d..9c5c4d8ad7936f 100644 --- a/lib/internal/fs.js +++ b/lib/internal/fs.js @@ -1,9 +1,11 @@ 'use strict'; -const { Buffer } = require('buffer'); +const { Buffer, kMaxLength } = require('buffer'); const { Writable } = require('stream'); const errors = require('internal/errors'); +const { isUint8Array } = require('internal/util/types'); const fs = require('fs'); +const pathModule = require('path'); const util = require('util'); const { @@ -14,8 +16,21 @@ const { O_RDWR, O_SYNC, O_TRUNC, - O_WRONLY + O_WRONLY, + S_IFBLK, + S_IFCHR, + S_IFDIR, + S_IFIFO, + S_IFLNK, + S_IFMT, + S_IFREG, + S_IFSOCK, + UV_FS_SYMLINK_DIR, + UV_FS_SYMLINK_JUNCTION } = process.binding('constants').fs; +const { statValues } = process.binding('fs'); + +const isWindows = process.platform === 'win32'; function assertEncoding(encoding) { if (encoding && !Buffer.isEncoding(encoding)) { @@ -23,6 +38,167 @@ function assertEncoding(encoding) { } } +function copyObject(source) { + var target = {}; + for (var key in source) + target[key] = source[key]; + return target; +} + +function getOptions(options, defaultOptions) { + if (options === null || options === undefined || + typeof options === 'function') { + return defaultOptions; + } + + if (typeof options === 'string') { + defaultOptions = util._extend({}, defaultOptions); + defaultOptions.encoding = options; + options = defaultOptions; + } else if (typeof options !== 'object') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', + 'options', + ['string', 'Object'], + options); + } + + if (options.encoding !== 'buffer') + assertEncoding(options.encoding); + return options; +} + +function isInt32(n) { return n === (n | 0); } +function isUint32(n) { return n === (n >>> 0); } + +function modeNum(m, def) { + if (typeof m === 'number') + return m; + if (typeof m === 'string') + return parseInt(m, 8); + if (def) + return modeNum(def); + return undefined; +} + +// Check if the path contains null types if it is a string nor Uint8Array, +// otherwise return silently. +function nullCheck(path, propName, throwError = true) { + const pathIsString = typeof path === 'string'; + const pathIsUint8Array = isUint8Array(path); + + // We can only perform meaningful checks on strings and Uint8Arrays. + if (!pathIsString && !pathIsUint8Array) { + return; + } + + if (pathIsString && path.indexOf('\u0000') === -1) { + return; + } else if (pathIsUint8Array && path.indexOf(0) === -1) { + return; + } + + const err = new errors.Error( + 'ERR_INVALID_ARG_VALUE', propName, path, + 'must be a string or Uint8Array without null bytes'); + + if (throwError) { + Error.captureStackTrace(err, nullCheck); + throw err; + } + return err; +} + +function preprocessSymlinkDestination(path, type, linkPath) { + if (!isWindows) { + // No preprocessing is needed on Unix. + return path; + } else if (type === 'junction') { + // Junctions paths need to be absolute and \\?\-prefixed. + // A relative target is relative to the link's parent directory. + path = pathModule.resolve(linkPath, '..', path); + return pathModule.toNamespacedPath(path); + } else { + // Windows symlinks don't tolerate forward slashes. + return ('' + path).replace(/\//g, '\\'); + } +} + +// Constructor for file stats. +function Stats( + dev, + mode, + nlink, + uid, + gid, + rdev, + blksize, + ino, + size, + blocks, + atim_msec, + mtim_msec, + ctim_msec, + birthtim_msec +) { + this.dev = dev; + this.mode = mode; + this.nlink = nlink; + this.uid = uid; + this.gid = gid; + this.rdev = rdev; + this.blksize = blksize; + this.ino = ino; + this.size = size; + this.blocks = blocks; + this.atimeMs = atim_msec; + this.mtimeMs = mtim_msec; + this.ctimeMs = ctim_msec; + this.birthtimeMs = birthtim_msec; + this.atime = new Date(atim_msec + 0.5); + this.mtime = new Date(mtim_msec + 0.5); + this.ctime = new Date(ctim_msec + 0.5); + this.birthtime = new Date(birthtim_msec + 0.5); +} + +Stats.prototype._checkModeProperty = function(property) { + return ((this.mode & S_IFMT) === property); +}; + +Stats.prototype.isDirectory = function() { + return this._checkModeProperty(S_IFDIR); +}; + +Stats.prototype.isFile = function() { + return this._checkModeProperty(S_IFREG); +}; + +Stats.prototype.isBlockDevice = function() { + return this._checkModeProperty(S_IFBLK); +}; + +Stats.prototype.isCharacterDevice = function() { + return this._checkModeProperty(S_IFCHR); +}; + +Stats.prototype.isSymbolicLink = function() { + return this._checkModeProperty(S_IFLNK); +}; + +Stats.prototype.isFIFO = function() { + return this._checkModeProperty(S_IFIFO); +}; + +Stats.prototype.isSocket = function() { + return this._checkModeProperty(S_IFSOCK); +}; + +function statsFromValues(stats = statValues) { + return new Stats(stats[0], stats[1], stats[2], stats[3], stats[4], stats[5], + stats[6] < 0 ? undefined : stats[6], stats[7], stats[8], + stats[9] < 0 ? undefined : stats[9], stats[10], stats[11], + stats[12], stats[13]); +} + function stringToFlags(flags) { if (typeof flags === 'number') { return flags; @@ -56,6 +232,27 @@ function stringToFlags(flags) { throw new errors.TypeError('ERR_INVALID_OPT_VALUE', 'flags', flags); } +function stringToSymlinkType(type) { + let flags = 0; + if (typeof type === 'string') { + switch (type) { + case 'dir': + flags |= UV_FS_SYMLINK_DIR; + break; + case 'junction': + flags |= UV_FS_SYMLINK_JUNCTION; + break; + case 'file': + break; + default: + const err = new errors.Error('ERR_FS_INVALID_SYMLINK_TYPE', type); + Error.captureStackTrace(err, stringToSymlinkType); + throw err; + } + } + return flags; +} + // Temporary hack for process.stdout and process.stderr when piped to files. function SyncWriteStream(fd, options) { Writable.call(this); @@ -95,9 +292,131 @@ SyncWriteStream.prototype.destroy = function() { return true; }; +// converts Date or number to a fractional UNIX timestamp +function toUnixTimestamp(time, name = 'time') { + // eslint-disable-next-line eqeqeq + if (typeof time === 'string' && +time == time) { + return +time; + } + if (Number.isFinite(time)) { + if (time < 0) { + return Date.now() / 1000; + } + return time; + } + if (util.isDate(time)) { + // convert to 123.456 UNIX timestamp + return time.getTime() / 1000; + } + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', + name, + ['Date', 'Time in seconds'], + time); +} + +function validateBuffer(buffer) { + if (!isUint8Array(buffer)) { + const err = new errors.TypeError('ERR_INVALID_ARG_TYPE', 'buffer', + ['Buffer', 'Uint8Array']); + Error.captureStackTrace(err, validateBuffer); + throw err; + } +} + +function validateLen(len) { + let err; + + if (!isInt32(len)) + err = new errors.TypeError('ERR_INVALID_ARG_TYPE', 'len', 'integer'); + + if (err !== undefined) { + Error.captureStackTrace(err, validateLen); + throw err; + } +} + +function validateOffsetLengthRead(offset, length, bufferLength) { + let err; + + if (offset < 0 || offset >= bufferLength) { + err = new errors.RangeError('ERR_OUT_OF_RANGE', 'offset'); + } else if (length < 0 || offset + length > bufferLength) { + err = new errors.RangeError('ERR_OUT_OF_RANGE', 'length'); + } + + if (err !== undefined) { + Error.captureStackTrace(err, validateOffsetLengthRead); + throw err; + } +} + +function validateOffsetLengthWrite(offset, length, byteLength) { + let err; + + if (offset > byteLength) { + err = new errors.RangeError('ERR_OUT_OF_RANGE', 'offset'); + } else if (offset + length > byteLength || offset + length > kMaxLength) { + err = new errors.RangeError('ERR_OUT_OF_RANGE', 'length'); + } + + if (err !== undefined) { + Error.captureStackTrace(err, validateOffsetLengthWrite); + throw err; + } +} + +function validatePath(path, propName) { + let err; + + if (propName === undefined) { + propName = 'path'; + } + + if (typeof path !== 'string' && !isUint8Array(path)) { + err = new errors.TypeError('ERR_INVALID_ARG_TYPE', propName, + ['string', 'Buffer', 'URL']); + } else { + err = nullCheck(path, propName, false); + } + + if (err !== undefined) { + Error.captureStackTrace(err, validatePath); + throw err; + } +} + +function validateUint32(value, propName) { + let err; + + if (!isUint32(value)) + err = new errors.TypeError('ERR_INVALID_ARG_TYPE', propName, 'integer'); + + if (err !== undefined) { + Error.captureStackTrace(err, validateUint32); + throw err; + } +} + module.exports = { assertEncoding, + copyObject, + getOptions, + isInt32, + isUint32, + modeNum, + nullCheck, + preprocessSymlinkDestination, + realpathCacheKey: Symbol('realpathCacheKey'), + statsFromValues, stringToFlags, + stringToSymlinkType, + Stats, SyncWriteStream, - realpathCacheKey: Symbol('realpathCacheKey') + toUnixTimestamp, + validateBuffer, + validateLen, + validateOffsetLengthRead, + validateOffsetLengthWrite, + validatePath, + validateUint32 }; diff --git a/node.gyp b/node.gyp index 826543a9d17ce1..9b185267bb539b 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(); }