From ba14028864fef473f4cd955ea19bf2f7d9d3d13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caleb=20=E3=83=84=20Everett?= Date: Mon, 6 Dec 2021 15:49:38 -0800 Subject: [PATCH 01/12] feat(cp): initial commit of cp from node I need `fs.cp` in `npm copy` to copy node_modules files. I'm adapting node's [lib/internal/fs/cp/cp.js][0]. I'm checking in the original so I can record changes in git. ref npm/cli#4082 [0]: https://github.com/nodejs/node/blob/1fa507f098ca7a89012f76f0c849fa698e73a1a1/lib/internal/fs/cp/cp.js --- lib/cp/LICENSE | 15 ++ lib/cp/index.js | 22 +++ lib/cp/polyfill.js | 394 +++++++++++++++++++++++++++++++++++++++++++++ lib/index.js | 1 + 4 files changed, 432 insertions(+) create mode 100644 lib/cp/LICENSE create mode 100644 lib/cp/index.js create mode 100644 lib/cp/polyfill.js diff --git a/lib/cp/LICENSE b/lib/cp/LICENSE new file mode 100644 index 0000000..93546df --- /dev/null +++ b/lib/cp/LICENSE @@ -0,0 +1,15 @@ +(The MIT License) + +Copyright (c) 2011-2017 JP Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/cp/index.js b/lib/cp/index.js new file mode 100644 index 0000000..78fec48 --- /dev/null +++ b/lib/cp/index.js @@ -0,0 +1,22 @@ +const fs = require('../fs.js') +const getOptions = require('../common/get-options.js') +const node = require('../common/node.js') +const polyfill = require('./polyfill.js') + +// node 16.7.0 added fs.cp +const useNative = node.satisfies('>=16.7.0') + +const rm = async (path, opts) => { + const options = getOptions(opts, { + copy: ['dereference', 'errorOnExist', 'filter', 'force', 'preserveTimestamps', 'recursive'], + }) + + // the polyfill is tested separately from this module, no need to hack + // process.version to try to trigger it just for coverage + // istanbul ignore next + return useNative + ? fs.rm(path, options) + : polyfill(path, options) +} + +module.exports = rm diff --git a/lib/cp/polyfill.js b/lib/cp/polyfill.js new file mode 100644 index 0000000..91d34cb --- /dev/null +++ b/lib/cp/polyfill.js @@ -0,0 +1,394 @@ +// this file is a modified version of the code in node 17.2.0 +// which is, in turn, a modified version of the fs-extra module on npm +// node core changes: +// - Use of the assert module has been replaced with core's error system. +// - All code related to the glob dependency has been removed. +// - Bring your own custom fs module is not currently supported. +// - Some basic code cleanup. +// changes here: +// - remove all callback related code +// - drop sync support +// - change assertions back to non-internal methods (see options.js) +// - throws ENOTDIR when rmdir gets an ENOENT for a path that exists in Windows +'use strict'; + +const { + ArrayPrototypeEvery, + ArrayPrototypeFilter, + Boolean, + PromiseAll, + PromisePrototypeCatch, + PromisePrototypeThen, + PromiseReject, + SafeArrayIterator, + StringPrototypeSplit, +} = primordials; +const { + codes: { + ERR_FS_CP_DIR_TO_NON_DIR, + ERR_FS_CP_EEXIST, + ERR_FS_CP_EINVAL, + ERR_FS_CP_FIFO_PIPE, + ERR_FS_CP_NON_DIR_TO_DIR, + ERR_FS_CP_SOCKET, + ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY, + ERR_FS_CP_UNKNOWN, + ERR_FS_EISDIR, + }, +} = require('internal/errors'); +const { + os: { + errno: { + EEXIST, + EISDIR, + EINVAL, + ENOTDIR, + } + } +} = internalBinding('constants'); +const { + chmod, + copyFile, + lstat, + mkdir, + readdir, + readlink, + stat, + symlink, + unlink, + utimes, +} = require('fs/promises'); +const { + dirname, + isAbsolute, + join, + parse, + resolve, + sep, +} = require('path'); + +async function cpFn(src, dest, opts) { + // Warn about using preserveTimestamps on 32-bit node + if (opts.preserveTimestamps && process.arch === 'ia32') { + const warning = 'Using the preserveTimestamps option in 32-bit ' + + 'node is not recommended'; + process.emitWarning(warning, 'TimestampPrecisionWarning'); + } + const stats = await checkPaths(src, dest, opts); + const { srcStat, destStat } = stats; + await checkParentPaths(src, srcStat, dest); + if (opts.filter) { + return handleFilter(checkParentDir, destStat, src, dest, opts); + } + return checkParentDir(destStat, src, dest, opts); +} + +async function checkPaths(src, dest, opts) { + const { 0: srcStat, 1: destStat } = await getStats(src, dest, opts); + if (destStat) { + if (areIdentical(srcStat, destStat)) { + throw new ERR_FS_CP_EINVAL({ + message: 'src and dest cannot be the same', + path: dest, + syscall: 'cp', + errno: EINVAL, + }); + } + if (srcStat.isDirectory() && !destStat.isDirectory()) { + throw new ERR_FS_CP_DIR_TO_NON_DIR({ + message: `cannot overwrite directory ${src} ` + + `with non-directory ${dest}`, + path: dest, + syscall: 'cp', + errno: EISDIR, + }); + } + if (!srcStat.isDirectory() && destStat.isDirectory()) { + throw new ERR_FS_CP_NON_DIR_TO_DIR({ + message: `cannot overwrite non-directory ${src} ` + + `with directory ${dest}`, + path: dest, + syscall: 'cp', + errno: ENOTDIR, + }); + } + } + + if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { + throw new ERR_FS_CP_EINVAL({ + message: `cannot copy ${src} to a subdirectory of self ${dest}`, + path: dest, + syscall: 'cp', + errno: EINVAL, + }); + } + return { srcStat, destStat }; +} + +function areIdentical(srcStat, destStat) { + return destStat.ino && destStat.dev && destStat.ino === srcStat.ino && + destStat.dev === srcStat.dev; +} + +function getStats(src, dest, opts) { + const statFunc = opts.dereference ? + (file) => stat(file, { bigint: true }) : + (file) => lstat(file, { bigint: true }); + return PromiseAll(new SafeArrayIterator([ + statFunc(src), + PromisePrototypeCatch(statFunc(dest), (err) => { + if (err.code === 'ENOENT') return null; + throw err; + }), + ])); +} + +async function checkParentDir(destStat, src, dest, opts) { + const destParent = dirname(dest); + const dirExists = await pathExists(destParent); + if (dirExists) return getStatsForCopy(destStat, src, dest, opts); + await mkdir(destParent, { recursive: true }); + return getStatsForCopy(destStat, src, dest, opts); +} + +function pathExists(dest) { + return PromisePrototypeThen( + stat(dest), + () => true, + (err) => (err.code === 'ENOENT' ? false : PromiseReject(err))); +} + +// Recursively check if dest parent is a subdirectory of src. +// It works for all file types including symlinks since it +// checks the src and dest inodes. It starts from the deepest +// parent and stops once it reaches the src parent or the root path. +async function checkParentPaths(src, srcStat, dest) { + const srcParent = resolve(dirname(src)); + const destParent = resolve(dirname(dest)); + if (destParent === srcParent || destParent === parse(destParent).root) { + return; + } + let destStat; + try { + destStat = await stat(destParent, { bigint: true }); + } catch (err) { + if (err.code === 'ENOENT') return; + throw err; + } + if (areIdentical(srcStat, destStat)) { + throw new ERR_FS_CP_EINVAL({ + message: `cannot copy ${src} to a subdirectory of self ${dest}`, + path: dest, + syscall: 'cp', + errno: EINVAL, + }); + } + return checkParentPaths(src, srcStat, destParent); +} + +const normalizePathToArray = (path) => + ArrayPrototypeFilter(StringPrototypeSplit(resolve(path), sep), Boolean); + +// Return true if dest is a subdir of src, otherwise false. +// It only checks the path strings. +function isSrcSubdir(src, dest) { + const srcArr = normalizePathToArray(src); + const destArr = normalizePathToArray(dest); + return ArrayPrototypeEvery(srcArr, (cur, i) => destArr[i] === cur); +} + +async function handleFilter(onInclude, destStat, src, dest, opts, cb) { + const include = await opts.filter(src, dest); + if (include) return onInclude(destStat, src, dest, opts, cb); +} + +function startCopy(destStat, src, dest, opts) { + if (opts.filter) { + return handleFilter(getStatsForCopy, destStat, src, dest, opts); + } + return getStatsForCopy(destStat, src, dest, opts); +} + +async function getStatsForCopy(destStat, src, dest, opts) { + const statFn = opts.dereference ? stat : lstat; + const srcStat = await statFn(src); + if (srcStat.isDirectory() && opts.recursive) { + return onDir(srcStat, destStat, src, dest, opts); + } else if (srcStat.isDirectory()) { + throw new ERR_FS_EISDIR({ + message: `${src} is a directory (not copied)`, + path: src, + syscall: 'cp', + errno: EINVAL, + }); + } else if (srcStat.isFile() || + srcStat.isCharacterDevice() || + srcStat.isBlockDevice()) { + return onFile(srcStat, destStat, src, dest, opts); + } else if (srcStat.isSymbolicLink()) { + return onLink(destStat, src, dest); + } else if (srcStat.isSocket()) { + throw new ERR_FS_CP_SOCKET({ + message: `cannot copy a socket file: ${dest}`, + path: dest, + syscall: 'cp', + errno: EINVAL, + }); + } else if (srcStat.isFIFO()) { + throw new ERR_FS_CP_FIFO_PIPE({ + message: `cannot copy a FIFO pipe: ${dest}`, + path: dest, + syscall: 'cp', + errno: EINVAL, + }); + } + throw new ERR_FS_CP_UNKNOWN({ + message: `cannot copy an unknown file type: ${dest}`, + path: dest, + syscall: 'cp', + errno: EINVAL, + }); +} + +function onFile(srcStat, destStat, src, dest, opts) { + if (!destStat) return _copyFile(srcStat, src, dest, opts); + return mayCopyFile(srcStat, src, dest, opts); +} + +async function mayCopyFile(srcStat, src, dest, opts) { + if (opts.force) { + await unlink(dest); + return _copyFile(srcStat, src, dest, opts); + } else if (opts.errorOnExist) { + throw new ERR_FS_CP_EEXIST({ + message: `${dest} already exists`, + path: dest, + syscall: 'cp', + errno: EEXIST, + }); + } +} + +async function _copyFile(srcStat, src, dest, opts) { + await copyFile(src, dest); + if (opts.preserveTimestamps) { + return handleTimestampsAndMode(srcStat.mode, src, dest); + } + return setDestMode(dest, srcStat.mode); +} + +async function handleTimestampsAndMode(srcMode, src, dest) { + // Make sure the file is writable before setting the timestamp + // otherwise open fails with EPERM when invoked with 'r+' + // (through utimes call) + if (fileIsNotWritable(srcMode)) { + await makeFileWritable(dest, srcMode); + return setDestTimestampsAndMode(srcMode, src, dest); + } + return setDestTimestampsAndMode(srcMode, src, dest); +} + +function fileIsNotWritable(srcMode) { + return (srcMode & 0o200) === 0; +} + +function makeFileWritable(dest, srcMode) { + return setDestMode(dest, srcMode | 0o200); +} + +async function setDestTimestampsAndMode(srcMode, src, dest) { + await setDestTimestamps(src, dest); + return setDestMode(dest, srcMode); +} + +function setDestMode(dest, srcMode) { + return chmod(dest, srcMode); +} + +async function setDestTimestamps(src, dest) { + // The initial srcStat.atime cannot be trusted + // because it is modified by the read(2) system call + // (See https://nodejs.org/api/fs.html#fs_stat_time_values) + const updatedSrcStat = await stat(src); + return utimes(dest, updatedSrcStat.atime, updatedSrcStat.mtime); +} + +function onDir(srcStat, destStat, src, dest, opts) { + if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts); + return copyDir(src, dest, opts); +} + +async function mkDirAndCopy(srcMode, src, dest, opts) { + await mkdir(dest); + await copyDir(src, dest, opts); + return setDestMode(dest, srcMode); +} + +async function copyDir(src, dest, opts) { + const dir = await readdir(src); + for (let i = 0; i < dir.length; i++) { + const item = dir[i]; + const srcItem = join(src, item); + const destItem = join(dest, item); + const { destStat } = await checkPaths(srcItem, destItem, opts); + await startCopy(destStat, srcItem, destItem, opts); + } +} + +async function onLink(destStat, src, dest) { + let resolvedSrc = await readlink(src); + if (!isAbsolute(resolvedSrc)) { + resolvedSrc = resolve(dirname(src), resolvedSrc); + } + if (!destStat) { + return symlink(resolvedSrc, dest); + } + let resolvedDest; + try { + resolvedDest = await readlink(dest); + } catch (err) { + // Dest exists and is a regular file or directory, + // Windows may throw UNKNOWN error. If dest already exists, + // fs throws error anyway, so no need to guard against it here. + if (err.code === 'EINVAL' || err.code === 'UNKNOWN') { + return symlink(resolvedSrc, dest); + } + throw err; + } + if (!isAbsolute(resolvedDest)) { + resolvedDest = resolve(dirname(dest), resolvedDest); + } + if (isSrcSubdir(resolvedSrc, resolvedDest)) { + throw new ERR_FS_CP_EINVAL({ + message: `cannot copy ${resolvedSrc} to a subdirectory of self ` + + `${resolvedDest}`, + path: dest, + syscall: 'cp', + errno: EINVAL, + }); + } + // Do not copy if src is a subdir of dest since unlinking + // dest in this case would result in removing src contents + // and therefore a broken symlink would be created. + const srcStat = await stat(src); + if (srcStat.isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) { + throw new ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY({ + message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`, + path: dest, + syscall: 'cp', + errno: EINVAL, + }); + } + return copyLink(resolvedSrc, dest); +} + +async function copyLink(resolvedSrc, dest) { + await unlink(dest); + return symlink(resolvedSrc, dest); +} + +module.exports = { + areIdentical, + cpFn, + isSrcSubdir, +}; diff --git a/lib/index.js b/lib/index.js index f669efc..e40d748 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,7 @@ module.exports = { ...require('./fs.js'), copyFile: require('./copy-file.js'), + cp: require('./cp/index.js'), mkdir: require('./mkdir/index.js'), mkdtemp: require('./mkdtemp.js'), rm: require('./rm/index.js'), From dedfaafda11f30cc3909e139ec0e55111884150b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caleb=20=E3=83=84=20Everett?= Date: Mon, 6 Dec 2021 15:59:25 -0800 Subject: [PATCH 02/12] fix(cp): replace node primordials --- lib/cp/polyfill.js | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/lib/cp/polyfill.js b/lib/cp/polyfill.js index 91d34cb..d7bba0c 100644 --- a/lib/cp/polyfill.js +++ b/lib/cp/polyfill.js @@ -12,17 +12,6 @@ // - throws ENOTDIR when rmdir gets an ENOENT for a path that exists in Windows 'use strict'; -const { - ArrayPrototypeEvery, - ArrayPrototypeFilter, - Boolean, - PromiseAll, - PromisePrototypeCatch, - PromisePrototypeThen, - PromiseReject, - SafeArrayIterator, - StringPrototypeSplit, -} = primordials; const { codes: { ERR_FS_CP_DIR_TO_NON_DIR, @@ -134,13 +123,13 @@ function getStats(src, dest, opts) { const statFunc = opts.dereference ? (file) => stat(file, { bigint: true }) : (file) => lstat(file, { bigint: true }); - return PromiseAll(new SafeArrayIterator([ + return Promise.all([ statFunc(src), - PromisePrototypeCatch(statFunc(dest), (err) => { + statFunc(dest).catch((err) => { if (err.code === 'ENOENT') return null; throw err; }), - ])); + ]); } async function checkParentDir(destStat, src, dest, opts) { @@ -152,10 +141,9 @@ async function checkParentDir(destStat, src, dest, opts) { } function pathExists(dest) { - return PromisePrototypeThen( - stat(dest), + return stat(dest).then( () => true, - (err) => (err.code === 'ENOENT' ? false : PromiseReject(err))); + (err) => (err.code === 'ENOENT' ? false : Promise.reject(err))); } // Recursively check if dest parent is a subdirectory of src. @@ -187,14 +175,14 @@ async function checkParentPaths(src, srcStat, dest) { } const normalizePathToArray = (path) => - ArrayPrototypeFilter(StringPrototypeSplit(resolve(path), sep), Boolean); + resolve(path).split(sep).filter(Boolean); // Return true if dest is a subdir of src, otherwise false. // It only checks the path strings. function isSrcSubdir(src, dest) { const srcArr = normalizePathToArray(src); const destArr = normalizePathToArray(dest); - return ArrayPrototypeEvery(srcArr, (cur, i) => destArr[i] === cur); + return srcArr.every((cur, i) => destArr[i] === cur); } async function handleFilter(onInclude, destStat, src, dest, opts, cb) { From a067a7519e1b730bc766b6084a46b53f91c42c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caleb=20=E3=83=84=20Everett?= Date: Mon, 6 Dec 2021 16:57:22 -0800 Subject: [PATCH 03/12] fix(cp): implement errors --- lib/cp/polyfill.js | 35 +++++++-------- lib/errors.js | 107 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 20 deletions(-) create mode 100644 lib/errors.js diff --git a/lib/cp/polyfill.js b/lib/cp/polyfill.js index d7bba0c..2ab39e2 100644 --- a/lib/cp/polyfill.js +++ b/lib/cp/polyfill.js @@ -13,20 +13,19 @@ 'use strict'; const { - codes: { - ERR_FS_CP_DIR_TO_NON_DIR, - ERR_FS_CP_EEXIST, - ERR_FS_CP_EINVAL, - ERR_FS_CP_FIFO_PIPE, - ERR_FS_CP_NON_DIR_TO_DIR, - ERR_FS_CP_SOCKET, - ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY, - ERR_FS_CP_UNKNOWN, - ERR_FS_EISDIR, - }, -} = require('internal/errors'); + ERR_FS_CP_DIR_TO_NON_DIR, + ERR_FS_CP_EEXIST, + ERR_FS_CP_EINVAL, + ERR_FS_CP_FIFO_PIPE, + ERR_FS_CP_NON_DIR_TO_DIR, + ERR_FS_CP_SOCKET, + ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY, + ERR_FS_CP_UNKNOWN, + ERR_FS_EISDIR, +} = require('../errors.js') + const { - os: { + constants: { errno: { EEXIST, EISDIR, @@ -34,7 +33,7 @@ const { ENOTDIR, } } -} = internalBinding('constants'); +} = require('os') const { chmod, copyFile, @@ -56,7 +55,7 @@ const { sep, } = require('path'); -async function cpFn(src, dest, opts) { +async function cp(src, dest, opts) { // Warn about using preserveTimestamps on 32-bit node if (opts.preserveTimestamps && process.arch === 'ia32') { const warning = 'Using the preserveTimestamps option in 32-bit ' + @@ -375,8 +374,4 @@ async function copyLink(resolvedSrc, dest) { return symlink(resolvedSrc, dest); } -module.exports = { - areIdentical, - cpFn, - isSrcSubdir, -}; +module.exports = cp diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 0000000..1cab0d4 --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,107 @@ +'use strict' + +// adapted from node's internal/errors +// https://github.com/nodejs/node/blob/c8a04049be96d2a6d625d4417df095fc0f3eaa7b/lib/internal/errors.js + +// close copy of node's internal SystemError class. +class SystemError { + constructor(code, prefix, context) { + // XXX context.code is undefined in all constructors used in cp/polyfill + // that may be a bug copied from node + // await require('fs/promises').cp('error.ts', 'error.ts') + // + // Uncaught: + // SystemError [ERR_FS_CP_EINVAL]: Invalid src or dest: cp returned undefined (src and dest cannot be the same) error.ts + // maybe the constructor should use `code` not `errno`? + // nodejs/node#41104 + let message = `${prefix}: ${context.syscall} returned ` + + `${context.code} (${context.message})` + + if (context.path !== undefined) + message += ` ${context.path}` + if (context.dest !== undefined) + message += ` => ${context.dest}` + + this.code = code + Object.defineProperties(this, { + name: { + value: 'SystemError', + enumerable: false, + writable: true, + configurable: true, + }, + message: { + value: message, + enumerable: false, + writable: true, + configurable: true, + }, + info: { + value: context, + enumerable: true, + configurable: true, + writable: false, + }, + errno: { + get() { return context.errno }, + set(value) { context.errno = value }, + enumerable: true, + configurable: true, + }, + syscall: { + get() { return context.syscall }, + set(value) { context.syscall = value }, + enumerable: true, + configurable: true, + }, + }) + + if (context.path !== undefined) { + Object.defineProperty(this, 'path', { + get() { return context.path }, + set(value) { context.path = value }, + enumerable: true, + configurable: true + }) + } + + if (context.dest !== undefined) { + Object.defineProperty(this, 'dest', { + get() { return context.dest }, + set(value) { context.dest = value }, + enumerable: true, + configurable: true + }) + } + } + + toString() { + return `${this.name} [${this.code}]: ${this.message}` + } + + [Symbol.for('nodejs.util.inspect.custom')](_recurseTimes, ctx) { + return lazyInternalUtilInspect().inspect(this, { + ...ctx, + getters: true, + customInspect: false + }) + } +} + +function E (code, message) { + module.exports[code] = class NodeError extends SystemError { + constructor(ctx) { + super(code, message, ctx) + } + } +} + +E('ERR_FS_CP_DIR_TO_NON_DIR', 'Cannot overwrite directory with non-directory') +E('ERR_FS_CP_EEXIST', 'Target already exists') +E('ERR_FS_CP_EINVAL', 'Invalid src or dest') +E('ERR_FS_CP_FIFO_PIPE', 'Cannot copy a FIFO pipe') +E('ERR_FS_CP_NON_DIR_TO_DIR', 'Cannot overwrite non-directory with directory') +E('ERR_FS_CP_SOCKET', 'Cannot copy a socket file') +E('ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY', 'Cannot overwrite symlink in subdirectory of self') +E('ERR_FS_CP_UNKNOWN', 'Cannot copy an unknown file type') +E('ERR_FS_EISDIR', 'Path is a directory') From dbc03aca1383cbd299f177eb1c9985afc73347be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caleb=20=E3=83=84=20Everett?= Date: Mon, 6 Dec 2021 17:37:28 -0800 Subject: [PATCH 04/12] fix(cp): use this modules fs promise wrapper --- lib/cp/polyfill.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cp/polyfill.js b/lib/cp/polyfill.js index 2ab39e2..ebb10c5 100644 --- a/lib/cp/polyfill.js +++ b/lib/cp/polyfill.js @@ -45,7 +45,7 @@ const { symlink, unlink, utimes, -} = require('fs/promises'); +} = require('../fs.js'); const { dirname, isAbsolute, From 4eaaa8dd6416a5cc9f0c0aef927c09d3ef7c485a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caleb=20=E3=83=84=20Everett?= Date: Mon, 6 Dec 2021 17:44:04 -0800 Subject: [PATCH 05/12] test(cp): copy test from node copied from [node][0] [0]: https://github.com/nodejs/node/blob/87d6fd7e696ee02178a8dc33a51e8e59bdc61d68/test/parallel/test-fs-cp.mjs --- test/cp/polyfill.js | 766 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 766 insertions(+) create mode 100644 test/cp/polyfill.js diff --git a/test/cp/polyfill.js b/test/cp/polyfill.js new file mode 100644 index 0000000..96e9a77 --- /dev/null +++ b/test/cp/polyfill.js @@ -0,0 +1,766 @@ +// copied from node test/parallel/test-fs-cp.mjs + +import { mustCall } from '../common/index.mjs'; + +import assert from 'assert'; +import fs from 'fs'; +const { + cp, + cpSync, + lstatSync, + mkdirSync, + readdirSync, + readFileSync, + readlinkSync, + symlinkSync, + statSync, + writeFileSync, +} = fs; +import net from 'net'; +import { join } from 'path'; +import { pathToFileURL } from 'url'; +import { setTimeout } from 'timers/promises'; + +const isWindows = process.platform === 'win32'; +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +let dirc = 0; +function nextdir() { + return join(tmpdir.path, `copy_${++dirc}`); +} + +// Synchronous implementation of copy. + +// It copies a nested folder structure with files and folders. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + cpSync(src, dest, { recursive: true }); + assertDirEquivalent(src, dest); +} + +// It does not throw errors when directory is copied over and force is false. +{ + const src = nextdir(); + mkdirSync(join(src, 'a', 'b'), { recursive: true }); + writeFileSync(join(src, 'README.md'), 'hello world', 'utf8'); + const dest = nextdir(); + cpSync(src, dest, { recursive: true }); + const initialStat = lstatSync(join(dest, 'README.md')); + cpSync(src, dest, { force: false, recursive: true }); + // File should not have been copied over, so access times will be identical: + assertDirEquivalent(src, dest); + const finalStat = lstatSync(join(dest, 'README.md')); + assert.strictEqual(finalStat.ctime.getTime(), initialStat.ctime.getTime()); +} + +// It overwrites existing files if force is true. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + mkdirSync(dest, { recursive: true }); + writeFileSync(join(dest, 'README.md'), '# Goodbye', 'utf8'); + cpSync(src, dest, { recursive: true }); + assertDirEquivalent(src, dest); + const content = readFileSync(join(dest, 'README.md'), 'utf8'); + assert.strictEqual(content.trim(), '# Hello'); +} + +// It does not fail if the same directory is copied to dest twice, +// when dereference is true, and force is false (fails silently). +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + const destFile = join(dest, 'a/b/README2.md'); + cpSync(src, dest, { dereference: true, recursive: true }); + cpSync(src, dest, { dereference: true, recursive: true }); + const stat = lstatSync(destFile); + assert(stat.isFile()); +} + + +// It copies file itself, rather than symlink, when dereference is true. +{ + const src = nextdir(); + mkdirSync(src, { recursive: true }); + writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); + symlinkSync(join(src, 'foo.js'), join(src, 'bar.js')); + + const dest = nextdir(); + mkdirSync(dest, { recursive: true }); + const destFile = join(dest, 'foo.js'); + + cpSync(join(src, 'bar.js'), destFile, { dereference: true, recursive: true }); + const stat = lstatSync(destFile); + assert(stat.isFile()); +} + + +// It throws error when src and dest are identical. +{ + const src = './test/fixtures/copy/kitchen-sink'; + assert.throws( + () => cpSync(src, src), + { code: 'ERR_FS_CP_EINVAL' } + ); +} + +// It throws error if symlink in src points to location in dest. +{ + const src = nextdir(); + mkdirSync(src, { recursive: true }); + const dest = nextdir(); + mkdirSync(dest); + symlinkSync(dest, join(src, 'link')); + cpSync(src, dest, { recursive: true }); + assert.throws( + () => cpSync(src, dest, { recursive: true }), + { + code: 'ERR_FS_CP_EINVAL' + } + ); +} + +// It throws error if symlink in dest points to location in src. +{ + const src = nextdir(); + mkdirSync(join(src, 'a', 'b'), { recursive: true }); + symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); + + const dest = nextdir(); + mkdirSync(join(dest, 'a'), { recursive: true }); + symlinkSync(src, join(dest, 'a', 'c')); + assert.throws( + () => cpSync(src, dest, { recursive: true }), + { code: 'ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY' } + ); +} + +// It throws error if parent directory of symlink in dest points to src. +{ + const src = nextdir(); + mkdirSync(join(src, 'a'), { recursive: true }); + const dest = nextdir(); + // Create symlink in dest pointing to src. + const destLink = join(dest, 'b'); + mkdirSync(dest, { recursive: true }); + symlinkSync(src, destLink); + assert.throws( + () => cpSync(src, join(dest, 'b', 'c')), + { code: 'ERR_FS_CP_EINVAL' } + ); +} + +// It throws error if attempt is made to copy directory to file. +{ + const src = nextdir(); + mkdirSync(src, { recursive: true }); + const dest = './test/fixtures/copy/kitchen-sink/README.md'; + assert.throws( + () => cpSync(src, dest), + { code: 'ERR_FS_CP_DIR_TO_NON_DIR' } + ); +} + +// It allows file to be copied to a file path. +{ + const srcFile = './test/fixtures/copy/kitchen-sink/index.js'; + const destFile = join(nextdir(), 'index.js'); + cpSync(srcFile, destFile, { dereference: true }); + const stat = lstatSync(destFile); + assert(stat.isFile()); +} + +// It throws error if directory copied without recursive flag. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + assert.throws( + () => cpSync(src, dest), + { code: 'ERR_FS_EISDIR' } + ); +} + + +// It throws error if attempt is made to copy file to directory. +{ + const src = './test/fixtures/copy/kitchen-sink/README.md'; + const dest = nextdir(); + mkdirSync(dest, { recursive: true }); + assert.throws( + () => cpSync(src, dest), + { code: 'ERR_FS_CP_NON_DIR_TO_DIR' } + ); +} + +// It throws error if attempt is made to copy to subdirectory of self. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = './test/fixtures/copy/kitchen-sink/a'; + assert.throws( + () => cpSync(src, dest), + { code: 'ERR_FS_CP_EINVAL' } + ); +} + +// It throws an error if attempt is made to copy socket. +if (!isWindows) { + const dest = nextdir(); + const sock = `${process.pid}.sock`; + const server = net.createServer(); + server.listen(sock); + assert.throws( + () => cpSync(sock, dest), + { code: 'ERR_FS_CP_SOCKET' } + ); + server.close(); +} + +// It copies timestamps from src to dest if preserveTimestamps is true. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + cpSync(src, dest, { preserveTimestamps: true, recursive: true }); + assertDirEquivalent(src, dest); + const srcStat = lstatSync(join(src, 'index.js')); + const destStat = lstatSync(join(dest, 'index.js')); + assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime()); +} + +// It applies filter function. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + cpSync(src, dest, { + filter: (path) => { + const pathStat = statSync(path); + return pathStat.isDirectory() || path.endsWith('.js'); + }, + dereference: true, + recursive: true, + }); + const destEntries = []; + collectEntries(dest, destEntries); + for (const entry of destEntries) { + assert.strictEqual( + entry.isDirectory() || entry.name.endsWith('.js'), + true + ); + } +} + +// It throws error if filter function is asynchronous. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + assert.throws(() => { + cpSync(src, dest, { + filter: async (path) => { + await setTimeout(5, 'done'); + const pathStat = statSync(path); + return pathStat.isDirectory() || path.endsWith('.js'); + }, + dereference: true, + recursive: true, + }); + }, { code: 'ERR_INVALID_RETURN_VALUE' }); +} + +// It throws error if errorOnExist is true, force is false, and file or folder +// copied over. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + cpSync(src, dest, { recursive: true }); + assert.throws( + () => cpSync(src, dest, { + dereference: true, + errorOnExist: true, + force: false, + recursive: true, + }), + { code: 'ERR_FS_CP_EEXIST' } + ); +} + +// It throws EEXIST error if attempt is made to copy symlink over file. +{ + const src = nextdir(); + mkdirSync(join(src, 'a', 'b'), { recursive: true }); + symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); + + const dest = nextdir(); + mkdirSync(join(dest, 'a'), { recursive: true }); + writeFileSync(join(dest, 'a', 'c'), 'hello', 'utf8'); + assert.throws( + () => cpSync(src, dest, { recursive: true }), + { code: 'EEXIST' } + ); +} + +// It makes file writeable when updating timestamp, if not writeable. +{ + const src = nextdir(); + mkdirSync(src, { recursive: true }); + const dest = nextdir(); + mkdirSync(dest, { recursive: true }); + writeFileSync(join(src, 'foo.txt'), 'foo', { mode: 0o444 }); + cpSync(src, dest, { preserveTimestamps: true, recursive: true }); + assertDirEquivalent(src, dest); + const srcStat = lstatSync(join(src, 'foo.txt')); + const destStat = lstatSync(join(dest, 'foo.txt')); + assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime()); +} + +// It copies link if it does not point to folder in src. +{ + const src = nextdir(); + mkdirSync(join(src, 'a', 'b'), { recursive: true }); + symlinkSync(src, join(src, 'a', 'c')); + const dest = nextdir(); + mkdirSync(join(dest, 'a'), { recursive: true }); + symlinkSync(dest, join(dest, 'a', 'c')); + cpSync(src, dest, { recursive: true }); + const link = readlinkSync(join(dest, 'a', 'c')); + assert.strictEqual(link, src); +} + +// It accepts file URL as src and dest. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + cpSync(pathToFileURL(src), pathToFileURL(dest), { recursive: true }); + assertDirEquivalent(src, dest); +} + +// It throws if options is not object. +{ + assert.throws( + () => cpSync('a', 'b', () => {}), + { code: 'ERR_INVALID_ARG_TYPE' } + ); +} + +// Callback implementation of copy. + +// It copies a nested folder structure with files and folders. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + cp(src, dest, { recursive: true }, mustCall((err) => { + assert.strictEqual(err, null); + assertDirEquivalent(src, dest); + })); +} + +// It does not throw errors when directory is copied over and force is false. +{ + const src = nextdir(); + mkdirSync(join(src, 'a', 'b'), { recursive: true }); + writeFileSync(join(src, 'README.md'), 'hello world', 'utf8'); + const dest = nextdir(); + cpSync(src, dest, { dereference: true, recursive: true }); + const initialStat = lstatSync(join(dest, 'README.md')); + cp(src, dest, { + dereference: true, + force: false, + recursive: true, + }, mustCall((err) => { + assert.strictEqual(err, null); + assertDirEquivalent(src, dest); + // File should not have been copied over, so access times will be identical: + const finalStat = lstatSync(join(dest, 'README.md')); + assert.strictEqual(finalStat.ctime.getTime(), initialStat.ctime.getTime()); + })); +} + +// It overwrites existing files if force is true. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + mkdirSync(dest, { recursive: true }); + writeFileSync(join(dest, 'README.md'), '# Goodbye', 'utf8'); + + cp(src, dest, { recursive: true }, mustCall((err) => { + assert.strictEqual(err, null); + assertDirEquivalent(src, dest); + const content = readFileSync(join(dest, 'README.md'), 'utf8'); + assert.strictEqual(content.trim(), '# Hello'); + })); +} + +// It does not fail if the same directory is copied to dest twice, +// when dereference is true, and force is false (fails silently). +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + const destFile = join(dest, 'a/b/README2.md'); + cpSync(src, dest, { dereference: true, recursive: true }); + cp(src, dest, { + dereference: true, + recursive: true + }, mustCall((err) => { + assert.strictEqual(err, null); + const stat = lstatSync(destFile); + assert(stat.isFile()); + })); +} + +// It copies file itself, rather than symlink, when dereference is true. +{ + const src = nextdir(); + mkdirSync(src, { recursive: true }); + writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); + symlinkSync(join(src, 'foo.js'), join(src, 'bar.js')); + + const dest = nextdir(); + mkdirSync(dest, { recursive: true }); + const destFile = join(dest, 'foo.js'); + + cp(join(src, 'bar.js'), destFile, { dereference: true }, + mustCall((err) => { + assert.strictEqual(err, null); + const stat = lstatSync(destFile); + assert(stat.isFile()); + }) + ); +} + +// It returns error when src and dest are identical. +{ + const src = './test/fixtures/copy/kitchen-sink'; + cp(src, src, mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL'); + })); +} + +// It returns error if symlink in src points to location in dest. +{ + const src = nextdir(); + mkdirSync(src, { recursive: true }); + const dest = nextdir(); + mkdirSync(dest); + symlinkSync(dest, join(src, 'link')); + cpSync(src, dest, { recursive: true }); + cp(src, dest, { recursive: true }, mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL'); + })); +} + +// It returns error if symlink in dest points to location in src. +{ + const src = nextdir(); + mkdirSync(join(src, 'a', 'b'), { recursive: true }); + symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); + + const dest = nextdir(); + mkdirSync(join(dest, 'a'), { recursive: true }); + symlinkSync(src, join(dest, 'a', 'c')); + cp(src, dest, { recursive: true }, mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY'); + })); +} + +// It returns error if parent directory of symlink in dest points to src. +{ + const src = nextdir(); + mkdirSync(join(src, 'a'), { recursive: true }); + const dest = nextdir(); + // Create symlink in dest pointing to src. + const destLink = join(dest, 'b'); + mkdirSync(dest, { recursive: true }); + symlinkSync(src, destLink); + cp(src, join(dest, 'b', 'c'), mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL'); + })); +} + +// It returns error if attempt is made to copy directory to file. +{ + const src = nextdir(); + mkdirSync(src, { recursive: true }); + const dest = './test/fixtures/copy/kitchen-sink/README.md'; + cp(src, dest, mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_DIR_TO_NON_DIR'); + })); +} + +// It allows file to be copied to a file path. +{ + const srcFile = './test/fixtures/copy/kitchen-sink/README.md'; + const destFile = join(nextdir(), 'index.js'); + cp(srcFile, destFile, { dereference: true }, mustCall((err) => { + assert.strictEqual(err, null); + const stat = lstatSync(destFile); + assert(stat.isFile()); + })); +} + +// It returns error if directory copied without recursive flag. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + cp(src, dest, mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_EISDIR'); + })); +} + +// It returns error if attempt is made to copy file to directory. +{ + const src = './test/fixtures/copy/kitchen-sink/README.md'; + const dest = nextdir(); + mkdirSync(dest, { recursive: true }); + cp(src, dest, mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_NON_DIR_TO_DIR'); + })); +} + +// It returns error if attempt is made to copy to subdirectory of self. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = './test/fixtures/copy/kitchen-sink/a'; + cp(src, dest, mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL'); + })); +} + +// It returns an error if attempt is made to copy socket. +if (!isWindows) { + const dest = nextdir(); + const sock = `${process.pid}.sock`; + const server = net.createServer(); + server.listen(sock); + cp(sock, dest, mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_SOCKET'); + server.close(); + })); +} + +// It copies timestamps from src to dest if preserveTimestamps is true. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + cp(src, dest, { + preserveTimestamps: true, + recursive: true + }, mustCall((err) => { + assert.strictEqual(err, null); + assertDirEquivalent(src, dest); + const srcStat = lstatSync(join(src, 'index.js')); + const destStat = lstatSync(join(dest, 'index.js')); + assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime()); + })); +} + +// It applies filter function. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + cp(src, dest, { + filter: (path) => { + const pathStat = statSync(path); + return pathStat.isDirectory() || path.endsWith('.js'); + }, + dereference: true, + recursive: true, + }, mustCall((err) => { + assert.strictEqual(err, null); + const destEntries = []; + collectEntries(dest, destEntries); + for (const entry of destEntries) { + assert.strictEqual( + entry.isDirectory() || entry.name.endsWith('.js'), + true + ); + } + })); +} + +// It supports async filter function. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + cp(src, dest, { + filter: async (path) => { + await setTimeout(5, 'done'); + const pathStat = statSync(path); + return pathStat.isDirectory() || path.endsWith('.js'); + }, + dereference: true, + recursive: true, + }, mustCall((err) => { + assert.strictEqual(err, null); + const destEntries = []; + collectEntries(dest, destEntries); + for (const entry of destEntries) { + assert.strictEqual( + entry.isDirectory() || entry.name.endsWith('.js'), + true + ); + } + })); +} + +// It returns error if errorOnExist is true, force is false, and file or folder +// copied over. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + cpSync(src, dest, { recursive: true }); + cp(src, dest, { + dereference: true, + errorOnExist: true, + force: false, + recursive: true, + }, mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_EEXIST'); + })); +} + +// It returns EEXIST error if attempt is made to copy symlink over file. +{ + const src = nextdir(); + mkdirSync(join(src, 'a', 'b'), { recursive: true }); + symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); + + const dest = nextdir(); + mkdirSync(join(dest, 'a'), { recursive: true }); + writeFileSync(join(dest, 'a', 'c'), 'hello', 'utf8'); + cp(src, dest, { recursive: true }, mustCall((err) => { + assert.strictEqual(err.code, 'EEXIST'); + })); +} + +// It makes file writeable when updating timestamp, if not writeable. +{ + const src = nextdir(); + mkdirSync(src, { recursive: true }); + const dest = nextdir(); + mkdirSync(dest, { recursive: true }); + writeFileSync(join(src, 'foo.txt'), 'foo', { mode: 0o444 }); + cp(src, dest, { + preserveTimestamps: true, + recursive: true, + }, mustCall((err) => { + assert.strictEqual(err, null); + assertDirEquivalent(src, dest); + const srcStat = lstatSync(join(src, 'foo.txt')); + const destStat = lstatSync(join(dest, 'foo.txt')); + assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime()); + })); +} + +// It copies link if it does not point to folder in src. +{ + const src = nextdir(); + mkdirSync(join(src, 'a', 'b'), { recursive: true }); + symlinkSync(src, join(src, 'a', 'c')); + const dest = nextdir(); + mkdirSync(join(dest, 'a'), { recursive: true }); + symlinkSync(dest, join(dest, 'a', 'c')); + cp(src, dest, { recursive: true }, mustCall((err) => { + assert.strictEqual(err, null); + const link = readlinkSync(join(dest, 'a', 'c')); + assert.strictEqual(link, src); + })); +} + +// It accepts file URL as src and dest. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + cp(pathToFileURL(src), pathToFileURL(dest), { recursive: true }, + mustCall((err) => { + assert.strictEqual(err, null); + assertDirEquivalent(src, dest); + })); +} + +// It throws if options is not object. +{ + assert.throws( + () => cp('a', 'b', 'hello', () => {}), + { code: 'ERR_INVALID_ARG_TYPE' } + ); +} + +// Promises implementation of copy. + +// It copies a nested folder structure with files and folders. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + const p = await fs.promises.cp(src, dest, { recursive: true }); + assert.strictEqual(p, undefined); + assertDirEquivalent(src, dest); +} + +// It accepts file URL as src and dest. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + const p = await fs.promises.cp( + pathToFileURL(src), + pathToFileURL(dest), + { recursive: true } + ); + assert.strictEqual(p, undefined); + assertDirEquivalent(src, dest); +} + +// It allows async error to be caught. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + await fs.promises.cp(src, dest, { recursive: true }); + await assert.rejects( + fs.promises.cp(src, dest, { + dereference: true, + errorOnExist: true, + force: false, + recursive: true, + }), + { code: 'ERR_FS_CP_EEXIST' } + ); +} + +// It rejects if options is not object. +{ + await assert.rejects( + fs.promises.cp('a', 'b', () => {}), + { code: 'ERR_INVALID_ARG_TYPE' } + ); +} + +function assertDirEquivalent(dir1, dir2) { + const dir1Entries = []; + collectEntries(dir1, dir1Entries); + const dir2Entries = []; + collectEntries(dir2, dir2Entries); + assert.strictEqual(dir1Entries.length, dir2Entries.length); + for (const entry1 of dir1Entries) { + const entry2 = dir2Entries.find((entry) => { + return entry.name === entry1.name; + }); + assert(entry2, `entry ${entry2.name} not copied`); + if (entry1.isFile()) { + assert(entry2.isFile(), `${entry2.name} was not file`); + } else if (entry1.isDirectory()) { + assert(entry2.isDirectory(), `${entry2.name} was not directory`); + } else if (entry1.isSymbolicLink()) { + assert(entry2.isSymbolicLink(), `${entry2.name} was not symlink`); + } + } +} + +function collectEntries(dir, dirEntries) { + const newEntries = readdirSync(dir, { withFileTypes: true }); + for (const entry of newEntries) { + if (entry.isDirectory()) { + collectEntries(join(dir, entry.name), dirEntries); + } + } + dirEntries.push(...newEntries); +} + From 5df3efeaf90ced17875a9a4a8172e4f0f4483fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caleb=20=E3=83=84=20Everett?= Date: Tue, 7 Dec 2021 12:50:52 -0800 Subject: [PATCH 06/12] fix(cp): add invalid type error --- lib/errors.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/errors.js b/lib/errors.js index 1cab0d4..6c8dee4 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -105,3 +105,11 @@ E('ERR_FS_CP_SOCKET', 'Cannot copy a socket file') E('ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY', 'Cannot overwrite symlink in subdirectory of self') E('ERR_FS_CP_UNKNOWN', 'Cannot copy an unknown file type') E('ERR_FS_EISDIR', 'Path is a directory') + +module.exports.ERR_INVALID_ARG_TYPE = class ERR_INVALID_ARG_TYPE extends Error { + constructor(name, expected, actual) { + super() + this.code = 'ERR_INVALID_ARG_TYPE' + this.message = `The ${name} argument must be ${expected}. Received ${typeof actual}` + } +} From 9f9dafd656b81fdb3cc9375df0894229599a34a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caleb=20=E3=83=84=20Everett?= Date: Tue, 7 Dec 2021 12:51:24 -0800 Subject: [PATCH 07/12] fix(cp): add default arguments --- lib/cp/polyfill.js | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/lib/cp/polyfill.js b/lib/cp/polyfill.js index ebb10c5..607e804 100644 --- a/lib/cp/polyfill.js +++ b/lib/cp/polyfill.js @@ -22,8 +22,8 @@ const { ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY, ERR_FS_CP_UNKNOWN, ERR_FS_EISDIR, -} = require('../errors.js') - + ERR_INVALID_ARG_TYPE, +} = require('../errors.js'); const { constants: { errno: { @@ -33,7 +33,7 @@ const { ENOTDIR, } } -} = require('os') +} = require('os'); const { chmod, copyFile, @@ -53,9 +53,37 @@ const { parse, resolve, sep, + toNamespacedPath, } = require('path'); +const { fileURLToPath } = require('url'); + +const defaultOptions = { + dereference: false, + errorOnExist: false, + filter: undefined, + force: true, + preserveTimestamps: false, + recursive: false, +}; async function cp(src, dest, opts) { + if (opts != undefined && typeof opts !== 'object') { + throw new ERR_INVALID_ARG_TYPE('options', ['Object'], opts); + } + return cpFn( + toNamespacedPath(getValidatedPath(src)), + toNamespacedPath(getValidatedPath(dest)), + { ...defaultOptions, ...opts }); +} + +function getValidatedPath(fileURLOrPath) { + const path = fileURLOrPath != null && fileURLOrPath.href && fileURLOrPath.origin + ? fileURLToPath(fileURLOrPath) + : fileURLOrPath + return path; +} + +async function cpFn(src, dest, opts) { // Warn about using preserveTimestamps on 32-bit node if (opts.preserveTimestamps && process.arch === 'ia32') { const warning = 'Using the preserveTimestamps option in 32-bit ' + From f7d5cb99f28f6b9c5a8dbaf8f1af7bee1763edc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caleb=20=E3=83=84=20Everett?= Date: Tue, 7 Dec 2021 12:52:01 -0800 Subject: [PATCH 08/12] fix(cp): adapt test to polyfil --- test/cp/polyfill.js | 796 +++++++++++--------------------------------- 1 file changed, 197 insertions(+), 599 deletions(-) diff --git a/test/cp/polyfill.js b/test/cp/polyfill.js index 96e9a77..9ae596e 100644 --- a/test/cp/polyfill.js +++ b/test/cp/polyfill.js @@ -1,12 +1,5 @@ -// copied from node test/parallel/test-fs-cp.mjs - -import { mustCall } from '../common/index.mjs'; - -import assert from 'assert'; -import fs from 'fs'; +const fs = require('fs'); const { - cp, - cpSync, lstatSync, mkdirSync, readdirSync, @@ -16,72 +9,94 @@ const { statSync, writeFileSync, } = fs; -import net from 'net'; -import { join } from 'path'; -import { pathToFileURL } from 'url'; -import { setTimeout } from 'timers/promises'; + +const net = require('net'); +const { join } = require('path'); +const { pathToFileURL } = require('url'); +const t = require('tap'); + +const cp = require('../../lib/cp/polyfill') const isWindows = process.platform === 'win32'; -import tmpdir from '../common/tmpdir.js'; -tmpdir.refresh(); +const tmpdir = t.testdir({ + 'kitchen-sink': { + a: { + b: { + 'index.js': 'module.exports = { purpose: "testing copy" };', + 'README2.md': '# Hello', + }, + c: { + d: { + 'index.js': 'module.exports = { purpose: "testing copy" };', + 'README3.md': '# Hello', + }, + }, + 'index.js': 'module.exports = { purpose: "testing copy" };', + 'README2.md': '# Hello', + }, + 'index.js': 'module.exports = { purpose: "testing copy" };', + 'README.md': '# Hello', + }, +}) +const kitchenSink = join(tmpdir, 'kitchen-sink') let dirc = 0; function nextdir() { - return join(tmpdir.path, `copy_${++dirc}`); + return join(tmpdir, `copy_${++dirc}`); } -// Synchronous implementation of copy. - -// It copies a nested folder structure with files and folders. -{ - const src = './test/fixtures/copy/kitchen-sink'; +t.test('It copies a nested folder structure with files and folders.', async t => { + const src = kitchenSink; const dest = nextdir(); - cpSync(src, dest, { recursive: true }); - assertDirEquivalent(src, dest); -} + await cp(src, dest, { recursive: true }) + assertDirEquivalent(t, src, dest); +}) -// It does not throw errors when directory is copied over and force is false. -{ +t.test('It does not throw errors when directory is copied over and force is false.', async t => { const src = nextdir(); mkdirSync(join(src, 'a', 'b'), { recursive: true }); writeFileSync(join(src, 'README.md'), 'hello world', 'utf8'); const dest = nextdir(); - cpSync(src, dest, { recursive: true }); + await cp(src, dest, { dereference: true, recursive: true }); const initialStat = lstatSync(join(dest, 'README.md')); - cpSync(src, dest, { force: false, recursive: true }); + await cp(src, dest, { + dereference: true, + force: false, + recursive: true, + }) + // File should not have been copied over, so access times will be identical: - assertDirEquivalent(src, dest); const finalStat = lstatSync(join(dest, 'README.md')); - assert.strictEqual(finalStat.ctime.getTime(), initialStat.ctime.getTime()); -} + t.equal(finalStat.ctime.getTime(), initialStat.ctime.getTime()); +}) -// It overwrites existing files if force is true. -{ - const src = './test/fixtures/copy/kitchen-sink'; +t.test('It overwrites existing files if force is true.', async t => { + const src = kitchenSink; const dest = nextdir(); mkdirSync(dest, { recursive: true }); writeFileSync(join(dest, 'README.md'), '# Goodbye', 'utf8'); - cpSync(src, dest, { recursive: true }); - assertDirEquivalent(src, dest); + + await cp(src, dest, { recursive: true }) + assertDirEquivalent(t, src, dest); const content = readFileSync(join(dest, 'README.md'), 'utf8'); - assert.strictEqual(content.trim(), '# Hello'); -} + t.equal(content.trim(), '# Hello'); +}) -// It does not fail if the same directory is copied to dest twice, -// when dereference is true, and force is false (fails silently). -{ - const src = './test/fixtures/copy/kitchen-sink'; +t.test('It does not fail if the same directory is copied to dest twice, when dereference is true, and force is false (fails silently).', async t => { + const src = kitchenSink; const dest = nextdir(); const destFile = join(dest, 'a/b/README2.md'); - cpSync(src, dest, { dereference: true, recursive: true }); - cpSync(src, dest, { dereference: true, recursive: true }); - const stat = lstatSync(destFile); - assert(stat.isFile()); -} + await cp(src, dest, { dereference: true, recursive: true }); + await cp(src, dest, { + dereference: true, + recursive: true + }) + const stat = lstatSync(destFile); + t.ok(stat.isFile()); +}) -// It copies file itself, rather than symlink, when dereference is true. -{ +t.test('It copies file itself, rather than symlink, when dereference is true.', async t => { const src = nextdir(); mkdirSync(src, { recursive: true }); writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); @@ -91,39 +106,30 @@ function nextdir() { mkdirSync(dest, { recursive: true }); const destFile = join(dest, 'foo.js'); - cpSync(join(src, 'bar.js'), destFile, { dereference: true, recursive: true }); + await cp(join(src, 'bar.js'), destFile, { dereference: true }) const stat = lstatSync(destFile); - assert(stat.isFile()); -} + t.ok(stat.isFile()); +}) +t.test('It returns error when src and dest are identical.', async t => { + t.rejects( + cp(kitchenSink, kitchenSink), + { code: 'ERR_FS_CP_EINVAL' }) +}) -// It throws error when src and dest are identical. -{ - const src = './test/fixtures/copy/kitchen-sink'; - assert.throws( - () => cpSync(src, src), - { code: 'ERR_FS_CP_EINVAL' } - ); -} - -// It throws error if symlink in src points to location in dest. -{ +t.test('It returns error if symlink in src points to location in dest.', async t => { const src = nextdir(); mkdirSync(src, { recursive: true }); const dest = nextdir(); mkdirSync(dest); symlinkSync(dest, join(src, 'link')); - cpSync(src, dest, { recursive: true }); - assert.throws( - () => cpSync(src, dest, { recursive: true }), - { - code: 'ERR_FS_CP_EINVAL' - } - ); -} + await cp(src, dest, { recursive: true }); + t.rejects( + cp(src, dest, { recursive: true }), + { code: 'ERR_FS_CP_EINVAL' }) +}) -// It throws error if symlink in dest points to location in src. -{ +t.test('It returns error if symlink in dest points to location in src.', async t => { const src = nextdir(); mkdirSync(join(src, 'a', 'b'), { recursive: true }); symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); @@ -131,14 +137,12 @@ function nextdir() { const dest = nextdir(); mkdirSync(join(dest, 'a'), { recursive: true }); symlinkSync(src, join(dest, 'a', 'c')); - assert.throws( - () => cpSync(src, dest, { recursive: true }), - { code: 'ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY' } - ); -} + t.rejects( + cp(src, dest, { recursive: true }), + { code: 'ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY' }) +}) -// It throws error if parent directory of symlink in dest points to src. -{ +t.test('It returns error if parent directory of symlink in dest points to src.', async t => { const src = nextdir(); mkdirSync(join(src, 'a'), { recursive: true }); const dest = nextdir(); @@ -146,146 +150,134 @@ function nextdir() { const destLink = join(dest, 'b'); mkdirSync(dest, { recursive: true }); symlinkSync(src, destLink); - assert.throws( - () => cpSync(src, join(dest, 'b', 'c')), - { code: 'ERR_FS_CP_EINVAL' } - ); -} + t.rejects( + cp(src, join(dest, 'b', 'c')), + { code: 'ERR_FS_CP_EINVAL'}); +}) -// It throws error if attempt is made to copy directory to file. -{ +t.test('It returns error if attempt is made to copy directory to file.', async t => { const src = nextdir(); mkdirSync(src, { recursive: true }); - const dest = './test/fixtures/copy/kitchen-sink/README.md'; - assert.throws( - () => cpSync(src, dest), - { code: 'ERR_FS_CP_DIR_TO_NON_DIR' } - ); -} - -// It allows file to be copied to a file path. -{ - const srcFile = './test/fixtures/copy/kitchen-sink/index.js'; + const dest = join(kitchenSink, 'README.md'); + t.rejects( + cp(src, dest), + { code: 'ERR_FS_CP_DIR_TO_NON_DIR'}); +}) + +t.test('It allows file to be copied to a file path.', async t => { + const srcFile = join(kitchenSink, 'README.md'); const destFile = join(nextdir(), 'index.js'); - cpSync(srcFile, destFile, { dereference: true }); + await cp(srcFile, destFile, { dereference: true }) const stat = lstatSync(destFile); - assert(stat.isFile()); -} + t.ok(stat.isFile()); +}) -// It throws error if directory copied without recursive flag. -{ - const src = './test/fixtures/copy/kitchen-sink'; +t.test('It returns error if directory copied without recursive flag.', async t => { + const src = kitchenSink; const dest = nextdir(); - assert.throws( - () => cpSync(src, dest), - { code: 'ERR_FS_EISDIR' } - ); -} - + t.rejects( + cp(src, dest), + { code: 'ERR_FS_EISDIR'}); +}) -// It throws error if attempt is made to copy file to directory. -{ - const src = './test/fixtures/copy/kitchen-sink/README.md'; +t.test('It returns error if attempt is made to copy file to directory.', async t => { + const src = join(kitchenSink, 'README.md'); const dest = nextdir(); mkdirSync(dest, { recursive: true }); - assert.throws( - () => cpSync(src, dest), - { code: 'ERR_FS_CP_NON_DIR_TO_DIR' } - ); -} + t.rejects( + cp(src, dest), + { code: 'ERR_FS_CP_NON_DIR_TO_DIR' }); +}) -// It throws error if attempt is made to copy to subdirectory of self. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = './test/fixtures/copy/kitchen-sink/a'; - assert.throws( - () => cpSync(src, dest), - { code: 'ERR_FS_CP_EINVAL' } - ); -} +t.test('It returns error if attempt is made to copy to subdirectory of self.', async t => { + const src = kitchenSink; + const dest = join(kitchenSink, 'a'); + t.rejects( + cp(src, dest), + { code: 'ERR_FS_CP_EINVAL' }); +}) -// It throws an error if attempt is made to copy socket. -if (!isWindows) { +t.test('It returns an error if attempt is made to copy socket.', { skip: isWindows }, async t => { const dest = nextdir(); const sock = `${process.pid}.sock`; const server = net.createServer(); server.listen(sock); - assert.throws( - () => cpSync(sock, dest), - { code: 'ERR_FS_CP_SOCKET' } - ); - server.close(); -} + t.teardown(() => server.close()) + t.rejects( + cp(sock, dest), + { code: 'ERR_FS_CP_SOCKET' }); +}) -// It copies timestamps from src to dest if preserveTimestamps is true. -{ - const src = './test/fixtures/copy/kitchen-sink'; +t.test('It copies timestamps from src to dest if preserveTimestamps is true.', async t => { + const src = kitchenSink; const dest = nextdir(); - cpSync(src, dest, { preserveTimestamps: true, recursive: true }); - assertDirEquivalent(src, dest); + await cp(src, dest, { + preserveTimestamps: true, + recursive: true + }) + assertDirEquivalent(t, src, dest); const srcStat = lstatSync(join(src, 'index.js')); const destStat = lstatSync(join(dest, 'index.js')); - assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime()); -} + t.equal(srcStat.mtime.getTime(), destStat.mtime.getTime()); +}) -// It applies filter function. -{ - const src = './test/fixtures/copy/kitchen-sink'; +t.test('It applies filter function.', async t => { + const src = kitchenSink; const dest = nextdir(); - cpSync(src, dest, { + await cp(src, dest, { filter: (path) => { const pathStat = statSync(path); return pathStat.isDirectory() || path.endsWith('.js'); }, dereference: true, recursive: true, - }); + }) const destEntries = []; collectEntries(dest, destEntries); for (const entry of destEntries) { - assert.strictEqual( + t.equal( entry.isDirectory() || entry.name.endsWith('.js'), true ); } -} +}) -// It throws error if filter function is asynchronous. -{ - const src = './test/fixtures/copy/kitchen-sink'; +t.test('It supports async filter function.', async t => { + const src = kitchenSink; const dest = nextdir(); - assert.throws(() => { - cpSync(src, dest, { - filter: async (path) => { - await setTimeout(5, 'done'); - const pathStat = statSync(path); - return pathStat.isDirectory() || path.endsWith('.js'); - }, - dereference: true, - recursive: true, - }); - }, { code: 'ERR_INVALID_RETURN_VALUE' }); -} + await cp(src, dest, { + filter: async (path) => { + const pathStat = statSync(path) + return pathStat.isDirectory() || path.endsWith('.js') + }, + dereference: true, + recursive: true, + }) + const destEntries = []; + collectEntries(dest, destEntries); + for (const entry of destEntries) { + t.equal( + entry.isDirectory() || entry.name.endsWith('.js'), + true + ); + } +}) -// It throws error if errorOnExist is true, force is false, and file or folder -// copied over. -{ - const src = './test/fixtures/copy/kitchen-sink'; +t.test('It returns error if errorOnExist is true, force is false, and file or folder copied over.', async t => { + const src = kitchenSink; const dest = nextdir(); - cpSync(src, dest, { recursive: true }); - assert.throws( - () => cpSync(src, dest, { + await cp(src, dest, { recursive: true }); + t.rejects( + cp(src, dest, { dereference: true, errorOnExist: true, force: false, recursive: true, }), - { code: 'ERR_FS_CP_EEXIST' } - ); -} + { code: 'ERR_FS_CP_EEXIST' }); +}) -// It throws EEXIST error if attempt is made to copy symlink over file. -{ +t.test('It returns EEXIST error if attempt is made to copy symlink over file.', async t => { const src = nextdir(); mkdirSync(join(src, 'a', 'b'), { recursive: true }); symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); @@ -293,463 +285,69 @@ if (!isWindows) { const dest = nextdir(); mkdirSync(join(dest, 'a'), { recursive: true }); writeFileSync(join(dest, 'a', 'c'), 'hello', 'utf8'); - assert.throws( - () => cpSync(src, dest, { recursive: true }), - { code: 'EEXIST' } - ); -} + t.rejects( + cp(src, dest, { recursive: true }), + { code: 'EEXIST' }); +}) -// It makes file writeable when updating timestamp, if not writeable. -{ +t.test('It makes file writeable when updating timestamp, if not writeable.', async t => { const src = nextdir(); mkdirSync(src, { recursive: true }); const dest = nextdir(); mkdirSync(dest, { recursive: true }); writeFileSync(join(src, 'foo.txt'), 'foo', { mode: 0o444 }); - cpSync(src, dest, { preserveTimestamps: true, recursive: true }); - assertDirEquivalent(src, dest); + await cp(src, dest, { + preserveTimestamps: true, + recursive: true, + }) + assertDirEquivalent(t, src, dest); const srcStat = lstatSync(join(src, 'foo.txt')); const destStat = lstatSync(join(dest, 'foo.txt')); - assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime()); -} + t.equal(srcStat.mtime.getTime(), destStat.mtime.getTime()); +}) -// It copies link if it does not point to folder in src. -{ +t.test('It copies link if it does not point to folder in src.', async t => { const src = nextdir(); mkdirSync(join(src, 'a', 'b'), { recursive: true }); symlinkSync(src, join(src, 'a', 'c')); const dest = nextdir(); mkdirSync(join(dest, 'a'), { recursive: true }); symlinkSync(dest, join(dest, 'a', 'c')); - cpSync(src, dest, { recursive: true }); + await cp(src, dest, { recursive: true }) const link = readlinkSync(join(dest, 'a', 'c')); - assert.strictEqual(link, src); -} - -// It accepts file URL as src and dest. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cpSync(pathToFileURL(src), pathToFileURL(dest), { recursive: true }); - assertDirEquivalent(src, dest); -} - -// It throws if options is not object. -{ - assert.throws( - () => cpSync('a', 'b', () => {}), - { code: 'ERR_INVALID_ARG_TYPE' } - ); -} - -// Callback implementation of copy. - -// It copies a nested folder structure with files and folders. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cp(src, dest, { recursive: true }, mustCall((err) => { - assert.strictEqual(err, null); - assertDirEquivalent(src, dest); - })); -} - -// It does not throw errors when directory is copied over and force is false. -{ - const src = nextdir(); - mkdirSync(join(src, 'a', 'b'), { recursive: true }); - writeFileSync(join(src, 'README.md'), 'hello world', 'utf8'); - const dest = nextdir(); - cpSync(src, dest, { dereference: true, recursive: true }); - const initialStat = lstatSync(join(dest, 'README.md')); - cp(src, dest, { - dereference: true, - force: false, - recursive: true, - }, mustCall((err) => { - assert.strictEqual(err, null); - assertDirEquivalent(src, dest); - // File should not have been copied over, so access times will be identical: - const finalStat = lstatSync(join(dest, 'README.md')); - assert.strictEqual(finalStat.ctime.getTime(), initialStat.ctime.getTime()); - })); -} - -// It overwrites existing files if force is true. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - mkdirSync(dest, { recursive: true }); - writeFileSync(join(dest, 'README.md'), '# Goodbye', 'utf8'); - - cp(src, dest, { recursive: true }, mustCall((err) => { - assert.strictEqual(err, null); - assertDirEquivalent(src, dest); - const content = readFileSync(join(dest, 'README.md'), 'utf8'); - assert.strictEqual(content.trim(), '# Hello'); - })); -} + t.equal(link, src); +}) -// It does not fail if the same directory is copied to dest twice, -// when dereference is true, and force is false (fails silently). -{ - const src = './test/fixtures/copy/kitchen-sink'; +t.test('It accepts file URL as src and dest.', async t => { + const src = kitchenSink; const dest = nextdir(); - const destFile = join(dest, 'a/b/README2.md'); - cpSync(src, dest, { dereference: true, recursive: true }); - cp(src, dest, { - dereference: true, - recursive: true - }, mustCall((err) => { - assert.strictEqual(err, null); - const stat = lstatSync(destFile); - assert(stat.isFile()); - })); -} - -// It copies file itself, rather than symlink, when dereference is true. -{ - const src = nextdir(); - mkdirSync(src, { recursive: true }); - writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); - symlinkSync(join(src, 'foo.js'), join(src, 'bar.js')); - - const dest = nextdir(); - mkdirSync(dest, { recursive: true }); - const destFile = join(dest, 'foo.js'); - - cp(join(src, 'bar.js'), destFile, { dereference: true }, - mustCall((err) => { - assert.strictEqual(err, null); - const stat = lstatSync(destFile); - assert(stat.isFile()); - }) - ); -} - -// It returns error when src and dest are identical. -{ - const src = './test/fixtures/copy/kitchen-sink'; - cp(src, src, mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL'); - })); -} - -// It returns error if symlink in src points to location in dest. -{ - const src = nextdir(); - mkdirSync(src, { recursive: true }); - const dest = nextdir(); - mkdirSync(dest); - symlinkSync(dest, join(src, 'link')); - cpSync(src, dest, { recursive: true }); - cp(src, dest, { recursive: true }, mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL'); - })); -} - -// It returns error if symlink in dest points to location in src. -{ - const src = nextdir(); - mkdirSync(join(src, 'a', 'b'), { recursive: true }); - symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); + await cp(pathToFileURL(src), pathToFileURL(dest), { recursive: true }) + assertDirEquivalent(t, src, dest); +}) - const dest = nextdir(); - mkdirSync(join(dest, 'a'), { recursive: true }); - symlinkSync(src, join(dest, 'a', 'c')); - cp(src, dest, { recursive: true }, mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY'); - })); -} - -// It returns error if parent directory of symlink in dest points to src. -{ - const src = nextdir(); - mkdirSync(join(src, 'a'), { recursive: true }); - const dest = nextdir(); - // Create symlink in dest pointing to src. - const destLink = join(dest, 'b'); - mkdirSync(dest, { recursive: true }); - symlinkSync(src, destLink); - cp(src, join(dest, 'b', 'c'), mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL'); - })); -} - -// It returns error if attempt is made to copy directory to file. -{ - const src = nextdir(); - mkdirSync(src, { recursive: true }); - const dest = './test/fixtures/copy/kitchen-sink/README.md'; - cp(src, dest, mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_DIR_TO_NON_DIR'); - })); -} - -// It allows file to be copied to a file path. -{ - const srcFile = './test/fixtures/copy/kitchen-sink/README.md'; - const destFile = join(nextdir(), 'index.js'); - cp(srcFile, destFile, { dereference: true }, mustCall((err) => { - assert.strictEqual(err, null); - const stat = lstatSync(destFile); - assert(stat.isFile()); - })); -} - -// It returns error if directory copied without recursive flag. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cp(src, dest, mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_EISDIR'); - })); -} - -// It returns error if attempt is made to copy file to directory. -{ - const src = './test/fixtures/copy/kitchen-sink/README.md'; - const dest = nextdir(); - mkdirSync(dest, { recursive: true }); - cp(src, dest, mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_NON_DIR_TO_DIR'); - })); -} - -// It returns error if attempt is made to copy to subdirectory of self. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = './test/fixtures/copy/kitchen-sink/a'; - cp(src, dest, mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL'); - })); -} - -// It returns an error if attempt is made to copy socket. -if (!isWindows) { - const dest = nextdir(); - const sock = `${process.pid}.sock`; - const server = net.createServer(); - server.listen(sock); - cp(sock, dest, mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_SOCKET'); - server.close(); - })); -} - -// It copies timestamps from src to dest if preserveTimestamps is true. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cp(src, dest, { - preserveTimestamps: true, - recursive: true - }, mustCall((err) => { - assert.strictEqual(err, null); - assertDirEquivalent(src, dest); - const srcStat = lstatSync(join(src, 'index.js')); - const destStat = lstatSync(join(dest, 'index.js')); - assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime()); - })); -} - -// It applies filter function. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cp(src, dest, { - filter: (path) => { - const pathStat = statSync(path); - return pathStat.isDirectory() || path.endsWith('.js'); - }, - dereference: true, - recursive: true, - }, mustCall((err) => { - assert.strictEqual(err, null); - const destEntries = []; - collectEntries(dest, destEntries); - for (const entry of destEntries) { - assert.strictEqual( - entry.isDirectory() || entry.name.endsWith('.js'), - true - ); - } - })); -} - -// It supports async filter function. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cp(src, dest, { - filter: async (path) => { - await setTimeout(5, 'done'); - const pathStat = statSync(path); - return pathStat.isDirectory() || path.endsWith('.js'); - }, - dereference: true, - recursive: true, - }, mustCall((err) => { - assert.strictEqual(err, null); - const destEntries = []; - collectEntries(dest, destEntries); - for (const entry of destEntries) { - assert.strictEqual( - entry.isDirectory() || entry.name.endsWith('.js'), - true - ); - } - })); -} - -// It returns error if errorOnExist is true, force is false, and file or folder -// copied over. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cpSync(src, dest, { recursive: true }); - cp(src, dest, { - dereference: true, - errorOnExist: true, - force: false, - recursive: true, - }, mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_EEXIST'); - })); -} - -// It returns EEXIST error if attempt is made to copy symlink over file. -{ - const src = nextdir(); - mkdirSync(join(src, 'a', 'b'), { recursive: true }); - symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); - - const dest = nextdir(); - mkdirSync(join(dest, 'a'), { recursive: true }); - writeFileSync(join(dest, 'a', 'c'), 'hello', 'utf8'); - cp(src, dest, { recursive: true }, mustCall((err) => { - assert.strictEqual(err.code, 'EEXIST'); - })); -} - -// It makes file writeable when updating timestamp, if not writeable. -{ - const src = nextdir(); - mkdirSync(src, { recursive: true }); - const dest = nextdir(); - mkdirSync(dest, { recursive: true }); - writeFileSync(join(src, 'foo.txt'), 'foo', { mode: 0o444 }); - cp(src, dest, { - preserveTimestamps: true, - recursive: true, - }, mustCall((err) => { - assert.strictEqual(err, null); - assertDirEquivalent(src, dest); - const srcStat = lstatSync(join(src, 'foo.txt')); - const destStat = lstatSync(join(dest, 'foo.txt')); - assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime()); - })); -} - -// It copies link if it does not point to folder in src. -{ - const src = nextdir(); - mkdirSync(join(src, 'a', 'b'), { recursive: true }); - symlinkSync(src, join(src, 'a', 'c')); - const dest = nextdir(); - mkdirSync(join(dest, 'a'), { recursive: true }); - symlinkSync(dest, join(dest, 'a', 'c')); - cp(src, dest, { recursive: true }, mustCall((err) => { - assert.strictEqual(err, null); - const link = readlinkSync(join(dest, 'a', 'c')); - assert.strictEqual(link, src); - })); -} - -// It accepts file URL as src and dest. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cp(pathToFileURL(src), pathToFileURL(dest), { recursive: true }, - mustCall((err) => { - assert.strictEqual(err, null); - assertDirEquivalent(src, dest); - })); -} - -// It throws if options is not object. -{ - assert.throws( +t.test('It throws if options is not object.', async t => { + t.rejects( () => cp('a', 'b', 'hello', () => {}), - { code: 'ERR_INVALID_ARG_TYPE' } - ); -} - -// Promises implementation of copy. - -// It copies a nested folder structure with files and folders. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - const p = await fs.promises.cp(src, dest, { recursive: true }); - assert.strictEqual(p, undefined); - assertDirEquivalent(src, dest); -} - -// It accepts file URL as src and dest. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - const p = await fs.promises.cp( - pathToFileURL(src), - pathToFileURL(dest), - { recursive: true } - ); - assert.strictEqual(p, undefined); - assertDirEquivalent(src, dest); -} - -// It allows async error to be caught. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - await fs.promises.cp(src, dest, { recursive: true }); - await assert.rejects( - fs.promises.cp(src, dest, { - dereference: true, - errorOnExist: true, - force: false, - recursive: true, - }), - { code: 'ERR_FS_CP_EEXIST' } - ); -} - -// It rejects if options is not object. -{ - await assert.rejects( - fs.promises.cp('a', 'b', () => {}), - { code: 'ERR_INVALID_ARG_TYPE' } - ); -} + { code: 'ERR_INVALID_ARG_TYPE' }); +}) -function assertDirEquivalent(dir1, dir2) { +function assertDirEquivalent(t, dir1, dir2) { const dir1Entries = []; collectEntries(dir1, dir1Entries); const dir2Entries = []; collectEntries(dir2, dir2Entries); - assert.strictEqual(dir1Entries.length, dir2Entries.length); + t.equal(dir1Entries.length, dir2Entries.length); for (const entry1 of dir1Entries) { const entry2 = dir2Entries.find((entry) => { return entry.name === entry1.name; }); - assert(entry2, `entry ${entry2.name} not copied`); + t.ok(entry2, `entry ${entry2.name} not copied`); if (entry1.isFile()) { - assert(entry2.isFile(), `${entry2.name} was not file`); + t.ok(entry2.isFile(), `${entry2.name} was not file`); } else if (entry1.isDirectory()) { - assert(entry2.isDirectory(), `${entry2.name} was not directory`); + t.ok(entry2.isDirectory(), `${entry2.name} was not directory`); } else if (entry1.isSymbolicLink()) { - assert(entry2.isSymbolicLink(), `${entry2.name} was not symlink`); + t.ok(entry2.isSymbolicLink(), `${entry2.name} was not symlink`); } } } From 3dd3591e5d6cb4cca3d5b4af4a32ab8744345756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caleb=20=E3=83=84=20Everett?= Date: Tue, 7 Dec 2021 14:11:14 -0800 Subject: [PATCH 09/12] test(cp): 100% coverage --- lib/cp/index.js | 8 ++--- lib/cp/polyfill.js | 16 +++++++-- lib/errors.js | 3 +- test/cp/index.js | 29 ++++++++++++++++ test/cp/polyfill.js | 28 +++++++++++++++- test/errors.js | 81 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 test/cp/index.js create mode 100644 test/errors.js diff --git a/lib/cp/index.js b/lib/cp/index.js index 78fec48..5da4739 100644 --- a/lib/cp/index.js +++ b/lib/cp/index.js @@ -6,7 +6,7 @@ const polyfill = require('./polyfill.js') // node 16.7.0 added fs.cp const useNative = node.satisfies('>=16.7.0') -const rm = async (path, opts) => { +const cp = async (src, dest, opts) => { const options = getOptions(opts, { copy: ['dereference', 'errorOnExist', 'filter', 'force', 'preserveTimestamps', 'recursive'], }) @@ -15,8 +15,8 @@ const rm = async (path, opts) => { // process.version to try to trigger it just for coverage // istanbul ignore next return useNative - ? fs.rm(path, options) - : polyfill(path, options) + ? fs.cp(src, dest, options) + : polyfill(src, dest, options) } -module.exports = rm +module.exports = cp diff --git a/lib/cp/polyfill.js b/lib/cp/polyfill.js index 607e804..1a3148d 100644 --- a/lib/cp/polyfill.js +++ b/lib/cp/polyfill.js @@ -85,6 +85,7 @@ function getValidatedPath(fileURLOrPath) { async function cpFn(src, dest, opts) { // Warn about using preserveTimestamps on 32-bit node + // istanbul ignore next if (opts.preserveTimestamps && process.arch === 'ia32') { const warning = 'Using the preserveTimestamps option in 32-bit ' + 'node is not recommended'; @@ -153,7 +154,11 @@ function getStats(src, dest, opts) { return Promise.all([ statFunc(src), statFunc(dest).catch((err) => { - if (err.code === 'ENOENT') return null; + // istanbul ignore next: unsure how to cover. + if (err.code === 'ENOENT') { + return null + } + // istanbul ignore next: unsure how to cover. throw err; }), ]); @@ -170,6 +175,7 @@ async function checkParentDir(destStat, src, dest, opts) { function pathExists(dest) { return stat(dest).then( () => true, + // istanbul ignore next: not sure when this would occur (err) => (err.code === 'ENOENT' ? false : Promise.reject(err))); } @@ -187,7 +193,9 @@ async function checkParentPaths(src, srcStat, dest) { try { destStat = await stat(destParent, { bigint: true }); } catch (err) { + // istanbul ignore else: not sure when this would occur if (err.code === 'ENOENT') return; + // istanbul ignore next: not sure when this would occur throw err; } if (areIdentical(srcStat, destStat)) { @@ -227,6 +235,7 @@ function startCopy(destStat, src, dest, opts) { async function getStatsForCopy(destStat, src, dest, opts) { const statFn = opts.dereference ? stat : lstat; const srcStat = await statFn(src); + // istanbul ignore else: can't portably test FIFO if (srcStat.isDirectory() && opts.recursive) { return onDir(srcStat, destStat, src, dest, opts); } else if (srcStat.isDirectory()) { @@ -249,7 +258,7 @@ async function getStatsForCopy(destStat, src, dest, opts) { syscall: 'cp', errno: EINVAL, }); - } else if (srcStat.isFIFO()) { + } else if (srcStat.isFIFO()) { throw new ERR_FS_CP_FIFO_PIPE({ message: `cannot copy a FIFO pipe: ${dest}`, path: dest, @@ -257,6 +266,7 @@ async function getStatsForCopy(destStat, src, dest, opts) { errno: EINVAL, }); } + // istanbul ignore next: should be unreachable throw new ERR_FS_CP_UNKNOWN({ message: `cannot copy an unknown file type: ${dest}`, path: dest, @@ -365,9 +375,11 @@ async function onLink(destStat, src, dest) { // Dest exists and is a regular file or directory, // Windows may throw UNKNOWN error. If dest already exists, // fs throws error anyway, so no need to guard against it here. + // istanbul ignore next: can only test on windows if (err.code === 'EINVAL' || err.code === 'UNKNOWN') { return symlink(resolvedSrc, dest); } + // istanbul ignore next: should not be possible throw err; } if (!isAbsolute(resolvedDest)) { diff --git a/lib/errors.js b/lib/errors.js index 6c8dee4..673e976 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -1,4 +1,5 @@ 'use strict' +const { inspect } = require('util') // adapted from node's internal/errors // https://github.com/nodejs/node/blob/c8a04049be96d2a6d625d4417df095fc0f3eaa7b/lib/internal/errors.js @@ -80,7 +81,7 @@ class SystemError { } [Symbol.for('nodejs.util.inspect.custom')](_recurseTimes, ctx) { - return lazyInternalUtilInspect().inspect(this, { + return inspect(this, { ...ctx, getters: true, customInspect: false diff --git a/test/cp/index.js b/test/cp/index.js new file mode 100644 index 0000000..16ef58a --- /dev/null +++ b/test/cp/index.js @@ -0,0 +1,29 @@ +const { join } = require('path') +const t = require('tap') + +const fs = require('../../') + +t.test('can copy a file', async (t) => { + const dir = t.testdir({ + file: 'some random file', + }) + const src = join(dir, 'file') + const dest = join(dir, 'dest') + + await fs.cp(src, dest) + + t.equal(await fs.exists(dest), true, 'dest exits') +}) + +t.test('can copy a directory', async (t) => { + const dir = t.testdir({ + directory: {}, + }) + const src = join(dir, 'directory') + const dest = join(dir, 'dest') + + await fs.cp(src, dest, { recursive: true }) + + t.equal(await fs.exists(dest), true, 'dest exists') +}) + diff --git a/test/cp/polyfill.js b/test/cp/polyfill.js index 9ae596e..1401b89 100644 --- a/test/cp/polyfill.js +++ b/test/cp/polyfill.js @@ -111,6 +111,23 @@ t.test('It copies file itself, rather than symlink, when dereference is true.', t.ok(stat.isFile()); }) +t.test('It copies relative symlinks', async t => { + const src = nextdir(); + mkdirSync(src, { recursive: true }); + writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); + symlinkSync('./foo.js', join(src, 'bar.js')); + + const dest = nextdir(); + const destFile = join(dest, 'bar.js'); + mkdirSync(dest, { recursive: true }); + writeFileSync(join(dest, 'foo.js'), 'foo', 'utf8'); + symlinkSync('./foo.js', destFile); + + await cp(src, dest, {recursive: true}) + const stat = lstatSync(destFile); + t.ok(stat.isSymbolicLink()); +}) + t.test('It returns error when src and dest are identical.', async t => { t.rejects( cp(kitchenSink, kitchenSink), @@ -327,10 +344,19 @@ t.test('It accepts file URL as src and dest.', async t => { t.test('It throws if options is not object.', async t => { t.rejects( - () => cp('a', 'b', 'hello', () => {}), + () => cp('a', 'b', 'hello'), { code: 'ERR_INVALID_ARG_TYPE' }); }) +t.test('It throws ENAMETOOLONG when name is too long', async t => { + const src = nextdir(); + mkdirSync(src, { recursive: true }); + const dest = join(tmpdir, 'a'.repeat(10_000)); + t.rejects( + cp(src, dest), + { code: 'ENAMETOOLONG' }); +}) + function assertDirEquivalent(t, dir1, dir2) { const dir1Entries = []; collectEntries(dir1, dir1Entries); diff --git a/test/errors.js b/test/errors.js new file mode 100644 index 0000000..12997ac --- /dev/null +++ b/test/errors.js @@ -0,0 +1,81 @@ +const t = require('tap') +const { ERR_FS_EISDIR } = require('../lib/errors') +const { constants: { errno: { EISDIR, EIO } } } = require('os') +const { inspect } = require('util') + +t.test('message with path and dest', async t => { + const err = new ERR_FS_EISDIR({ + path: 'path', + dest: 'dest', + syscall: 'cp', + code: EISDIR, + message: 'failed', + }) + + t.equal(err.message, `Path is a directory: cp returned ${EISDIR} (failed) path => dest`) +}) + +t.test('message without path or dest', async t => { + const err = new ERR_FS_EISDIR({ + syscall: 'cp', + code: EISDIR, + message: 'failed', + }) + + t.equal(err.message, `Path is a directory: cp returned ${EISDIR} (failed)`) +}) + +t.test('errno is alias for info.errno', async t => { + const err = new ERR_FS_EISDIR({ errno: EISDIR }) + t.equal(err.errno, EISDIR) + t.equal(err.info.errno, EISDIR) + err.errno = EIO + t.equal(err.errno, EIO) + t.equal(err.info.errno, EIO) +}) + +t.test('syscall is alias for info.syscall', async t => { + const err = new ERR_FS_EISDIR({ syscall: 'cp' }) + t.equal(err.syscall, 'cp') + t.equal(err.info.syscall, 'cp') + err.syscall = 'readlink' + t.equal(err.syscall, 'readlink') + t.equal(err.info.syscall, 'readlink') +}) + +t.test('path is alias for info.path', async t => { + const err = new ERR_FS_EISDIR({ path: 'first' }) + t.equal(err.path, 'first') + t.equal(err.info.path, 'first') + err.path = 'second' + t.equal(err.path, 'second') + t.equal(err.info.path, 'second') +}) + +t.test('dest is alias for info.dest', async t => { + const err = new ERR_FS_EISDIR({ dest: 'first' }) + t.equal(err.dest, 'first') + t.equal(err.info.dest, 'first') + err.dest = 'second' + t.equal(err.dest, 'second') + t.equal(err.info.dest, 'second') +}) + +t.test('toString', async t => { + const err = new ERR_FS_EISDIR({ + syscall: 'cp', + code: EISDIR, + message: 'failed', + }) + t.equal(err.toString(), + `SystemError [ERR_FS_EISDIR]: Path is a directory: cp returned ${EISDIR} (failed)`) +}) + +t.test('inspect', async t => { + const err = new ERR_FS_EISDIR({ + syscall: 'cp', + errno: EISDIR, + message: 'failed', + }) + t.ok(inspect(err)) +}) From dd5b5106eaad6a67cef6899f9d34451f2d7b80cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caleb=20=E3=83=84=20Everett?= Date: Tue, 7 Dec 2021 15:39:32 -0800 Subject: [PATCH 10/12] fix(cp): lintfix --- lib/cp/polyfill.js | 279 ++++++++++++++++++----------------- lib/errors.js | 65 +++++---- test/cp/index.js | 1 - test/cp/polyfill.js | 347 ++++++++++++++++++++++---------------------- 4 files changed, 357 insertions(+), 335 deletions(-) diff --git a/lib/cp/polyfill.js b/lib/cp/polyfill.js index 1a3148d..f83ccbf 100644 --- a/lib/cp/polyfill.js +++ b/lib/cp/polyfill.js @@ -10,7 +10,7 @@ // - drop sync support // - change assertions back to non-internal methods (see options.js) // - throws ENOTDIR when rmdir gets an ENOENT for a path that exists in Windows -'use strict'; +'use strict' const { ERR_FS_CP_DIR_TO_NON_DIR, @@ -23,7 +23,7 @@ const { ERR_FS_CP_UNKNOWN, ERR_FS_EISDIR, ERR_INVALID_ARG_TYPE, -} = require('../errors.js'); +} = require('../errors.js') const { constants: { errno: { @@ -31,9 +31,9 @@ const { EISDIR, EINVAL, ENOTDIR, - } - } -} = require('os'); + }, + }, +} = require('os') const { chmod, copyFile, @@ -45,7 +45,7 @@ const { symlink, unlink, utimes, -} = require('../fs.js'); +} = require('../fs.js') const { dirname, isAbsolute, @@ -54,8 +54,8 @@ const { resolve, sep, toNamespacedPath, -} = require('path'); -const { fileURLToPath } = require('url'); +} = require('path') +const { fileURLToPath } = require('url') const defaultOptions = { dereference: false, @@ -64,44 +64,45 @@ const defaultOptions = { force: true, preserveTimestamps: false, recursive: false, -}; +} -async function cp(src, dest, opts) { - if (opts != undefined && typeof opts !== 'object') { - throw new ERR_INVALID_ARG_TYPE('options', ['Object'], opts); +async function cp (src, dest, opts) { + if (opts != null && typeof opts !== 'object') { + throw new ERR_INVALID_ARG_TYPE('options', ['Object'], opts) } return cpFn( toNamespacedPath(getValidatedPath(src)), toNamespacedPath(getValidatedPath(dest)), - { ...defaultOptions, ...opts }); + { ...defaultOptions, ...opts }) } -function getValidatedPath(fileURLOrPath) { - const path = fileURLOrPath != null && fileURLOrPath.href && fileURLOrPath.origin +function getValidatedPath (fileURLOrPath) { + const path = fileURLOrPath != null && fileURLOrPath.href + && fileURLOrPath.origin ? fileURLToPath(fileURLOrPath) : fileURLOrPath - return path; + return path } -async function cpFn(src, dest, opts) { +async function cpFn (src, dest, opts) { // Warn about using preserveTimestamps on 32-bit node // istanbul ignore next if (opts.preserveTimestamps && process.arch === 'ia32') { const warning = 'Using the preserveTimestamps option in 32-bit ' + - 'node is not recommended'; - process.emitWarning(warning, 'TimestampPrecisionWarning'); + 'node is not recommended' + process.emitWarning(warning, 'TimestampPrecisionWarning') } - const stats = await checkPaths(src, dest, opts); - const { srcStat, destStat } = stats; - await checkParentPaths(src, srcStat, dest); + const stats = await checkPaths(src, dest, opts) + const { srcStat, destStat } = stats + await checkParentPaths(src, srcStat, dest) if (opts.filter) { - return handleFilter(checkParentDir, destStat, src, dest, opts); + return handleFilter(checkParentDir, destStat, src, dest, opts) } - return checkParentDir(destStat, src, dest, opts); + return checkParentDir(destStat, src, dest, opts) } -async function checkPaths(src, dest, opts) { - const { 0: srcStat, 1: destStat } = await getStats(src, dest, opts); +async function checkPaths (src, dest, opts) { + const { 0: srcStat, 1: destStat } = await getStats(src, dest, opts) if (destStat) { if (areIdentical(srcStat, destStat)) { throw new ERR_FS_CP_EINVAL({ @@ -109,7 +110,7 @@ async function checkPaths(src, dest, opts) { path: dest, syscall: 'cp', errno: EINVAL, - }); + }) } if (srcStat.isDirectory() && !destStat.isDirectory()) { throw new ERR_FS_CP_DIR_TO_NON_DIR({ @@ -118,7 +119,7 @@ async function checkPaths(src, dest, opts) { path: dest, syscall: 'cp', errno: EISDIR, - }); + }) } if (!srcStat.isDirectory() && destStat.isDirectory()) { throw new ERR_FS_CP_NON_DIR_TO_DIR({ @@ -127,7 +128,7 @@ async function checkPaths(src, dest, opts) { path: dest, syscall: 'cp', errno: ENOTDIR, - }); + }) } } @@ -137,20 +138,20 @@ async function checkPaths(src, dest, opts) { path: dest, syscall: 'cp', errno: EINVAL, - }); + }) } - return { srcStat, destStat }; + return { srcStat, destStat } } -function areIdentical(srcStat, destStat) { +function areIdentical (srcStat, destStat) { return destStat.ino && destStat.dev && destStat.ino === srcStat.ino && - destStat.dev === srcStat.dev; + destStat.dev === srcStat.dev } -function getStats(src, dest, opts) { +function getStats (src, dest, opts) { const statFunc = opts.dereference ? (file) => stat(file, { bigint: true }) : - (file) => lstat(file, { bigint: true }); + (file) => lstat(file, { bigint: true }) return Promise.all([ statFunc(src), statFunc(dest).catch((err) => { @@ -159,44 +160,48 @@ function getStats(src, dest, opts) { return null } // istanbul ignore next: unsure how to cover. - throw err; + throw err }), - ]); + ]) } -async function checkParentDir(destStat, src, dest, opts) { - const destParent = dirname(dest); - const dirExists = await pathExists(destParent); - if (dirExists) return getStatsForCopy(destStat, src, dest, opts); - await mkdir(destParent, { recursive: true }); - return getStatsForCopy(destStat, src, dest, opts); +async function checkParentDir (destStat, src, dest, opts) { + const destParent = dirname(dest) + const dirExists = await pathExists(destParent) + if (dirExists) { + return getStatsForCopy(destStat, src, dest, opts) + } + await mkdir(destParent, { recursive: true }) + return getStatsForCopy(destStat, src, dest, opts) } -function pathExists(dest) { +function pathExists (dest) { return stat(dest).then( () => true, // istanbul ignore next: not sure when this would occur - (err) => (err.code === 'ENOENT' ? false : Promise.reject(err))); + (err) => (err.code === 'ENOENT' ? false : Promise.reject(err))) } // Recursively check if dest parent is a subdirectory of src. // It works for all file types including symlinks since it // checks the src and dest inodes. It starts from the deepest // parent and stops once it reaches the src parent or the root path. -async function checkParentPaths(src, srcStat, dest) { - const srcParent = resolve(dirname(src)); - const destParent = resolve(dirname(dest)); +async function checkParentPaths (src, srcStat, dest) { + const srcParent = resolve(dirname(src)) + const destParent = resolve(dirname(dest)) if (destParent === srcParent || destParent === parse(destParent).root) { - return; + return } - let destStat; + let destStat try { - destStat = await stat(destParent, { bigint: true }); + destStat = await stat(destParent, { bigint: true }) } catch (err) { // istanbul ignore else: not sure when this would occur - if (err.code === 'ENOENT') return; + if (err.code === 'ENOENT') { + return + } // istanbul ignore next: not sure when this would occur - throw err; + throw err } if (areIdentical(srcStat, destStat)) { throw new ERR_FS_CP_EINVAL({ @@ -204,67 +209,69 @@ async function checkParentPaths(src, srcStat, dest) { path: dest, syscall: 'cp', errno: EINVAL, - }); + }) } - return checkParentPaths(src, srcStat, destParent); + return checkParentPaths(src, srcStat, destParent) } const normalizePathToArray = (path) => - resolve(path).split(sep).filter(Boolean); + resolve(path).split(sep).filter(Boolean) // Return true if dest is a subdir of src, otherwise false. // It only checks the path strings. -function isSrcSubdir(src, dest) { - const srcArr = normalizePathToArray(src); - const destArr = normalizePathToArray(dest); - return srcArr.every((cur, i) => destArr[i] === cur); +function isSrcSubdir (src, dest) { + const srcArr = normalizePathToArray(src) + const destArr = normalizePathToArray(dest) + return srcArr.every((cur, i) => destArr[i] === cur) } -async function handleFilter(onInclude, destStat, src, dest, opts, cb) { - const include = await opts.filter(src, dest); - if (include) return onInclude(destStat, src, dest, opts, cb); +async function handleFilter (onInclude, destStat, src, dest, opts, cb) { + const include = await opts.filter(src, dest) + if (include) { + return onInclude(destStat, src, dest, opts, cb) + } } -function startCopy(destStat, src, dest, opts) { +function startCopy (destStat, src, dest, opts) { if (opts.filter) { - return handleFilter(getStatsForCopy, destStat, src, dest, opts); + return handleFilter(getStatsForCopy, destStat, src, dest, opts) } - return getStatsForCopy(destStat, src, dest, opts); + return getStatsForCopy(destStat, src, dest, opts) } -async function getStatsForCopy(destStat, src, dest, opts) { - const statFn = opts.dereference ? stat : lstat; - const srcStat = await statFn(src); +async function getStatsForCopy (destStat, src, dest, opts) { + const statFn = opts.dereference ? stat : lstat + const srcStat = await statFn(src) // istanbul ignore else: can't portably test FIFO if (srcStat.isDirectory() && opts.recursive) { - return onDir(srcStat, destStat, src, dest, opts); + return onDir(srcStat, destStat, src, dest, opts) } else if (srcStat.isDirectory()) { throw new ERR_FS_EISDIR({ message: `${src} is a directory (not copied)`, path: src, syscall: 'cp', errno: EINVAL, - }); + }) } else if (srcStat.isFile() || srcStat.isCharacterDevice() || srcStat.isBlockDevice()) { - return onFile(srcStat, destStat, src, dest, opts); + return onFile(srcStat, destStat, src, dest, opts) } else if (srcStat.isSymbolicLink()) { - return onLink(destStat, src, dest); + return onLink(destStat, src, dest) } else if (srcStat.isSocket()) { throw new ERR_FS_CP_SOCKET({ message: `cannot copy a socket file: ${dest}`, path: dest, syscall: 'cp', errno: EINVAL, - }); - } else if (srcStat.isFIFO()) { + }) + } else if (srcStat.isFIFO()) { throw new ERR_FS_CP_FIFO_PIPE({ message: `cannot copy a FIFO pipe: ${dest}`, path: dest, syscall: 'cp', errno: EINVAL, - }); + }) } // istanbul ignore next: should be unreachable throw new ERR_FS_CP_UNKNOWN({ @@ -272,118 +279,122 @@ async function getStatsForCopy(destStat, src, dest, opts) { path: dest, syscall: 'cp', errno: EINVAL, - }); + }) } -function onFile(srcStat, destStat, src, dest, opts) { - if (!destStat) return _copyFile(srcStat, src, dest, opts); - return mayCopyFile(srcStat, src, dest, opts); +function onFile (srcStat, destStat, src, dest, opts) { + if (!destStat) { + return _copyFile(srcStat, src, dest, opts) + } + return mayCopyFile(srcStat, src, dest, opts) } -async function mayCopyFile(srcStat, src, dest, opts) { +async function mayCopyFile (srcStat, src, dest, opts) { if (opts.force) { - await unlink(dest); - return _copyFile(srcStat, src, dest, opts); + await unlink(dest) + return _copyFile(srcStat, src, dest, opts) } else if (opts.errorOnExist) { throw new ERR_FS_CP_EEXIST({ message: `${dest} already exists`, path: dest, syscall: 'cp', errno: EEXIST, - }); + }) } } -async function _copyFile(srcStat, src, dest, opts) { - await copyFile(src, dest); +async function _copyFile (srcStat, src, dest, opts) { + await copyFile(src, dest) if (opts.preserveTimestamps) { - return handleTimestampsAndMode(srcStat.mode, src, dest); + return handleTimestampsAndMode(srcStat.mode, src, dest) } - return setDestMode(dest, srcStat.mode); + return setDestMode(dest, srcStat.mode) } -async function handleTimestampsAndMode(srcMode, src, dest) { +async function handleTimestampsAndMode (srcMode, src, dest) { // Make sure the file is writable before setting the timestamp // otherwise open fails with EPERM when invoked with 'r+' // (through utimes call) if (fileIsNotWritable(srcMode)) { - await makeFileWritable(dest, srcMode); - return setDestTimestampsAndMode(srcMode, src, dest); + await makeFileWritable(dest, srcMode) + return setDestTimestampsAndMode(srcMode, src, dest) } - return setDestTimestampsAndMode(srcMode, src, dest); + return setDestTimestampsAndMode(srcMode, src, dest) } -function fileIsNotWritable(srcMode) { - return (srcMode & 0o200) === 0; +function fileIsNotWritable (srcMode) { + return (srcMode & 0o200) === 0 } -function makeFileWritable(dest, srcMode) { - return setDestMode(dest, srcMode | 0o200); +function makeFileWritable (dest, srcMode) { + return setDestMode(dest, srcMode | 0o200) } -async function setDestTimestampsAndMode(srcMode, src, dest) { - await setDestTimestamps(src, dest); - return setDestMode(dest, srcMode); +async function setDestTimestampsAndMode (srcMode, src, dest) { + await setDestTimestamps(src, dest) + return setDestMode(dest, srcMode) } -function setDestMode(dest, srcMode) { - return chmod(dest, srcMode); +function setDestMode (dest, srcMode) { + return chmod(dest, srcMode) } -async function setDestTimestamps(src, dest) { +async function setDestTimestamps (src, dest) { // The initial srcStat.atime cannot be trusted // because it is modified by the read(2) system call // (See https://nodejs.org/api/fs.html#fs_stat_time_values) - const updatedSrcStat = await stat(src); - return utimes(dest, updatedSrcStat.atime, updatedSrcStat.mtime); + const updatedSrcStat = await stat(src) + return utimes(dest, updatedSrcStat.atime, updatedSrcStat.mtime) } -function onDir(srcStat, destStat, src, dest, opts) { - if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts); - return copyDir(src, dest, opts); +function onDir (srcStat, destStat, src, dest, opts) { + if (!destStat) { + return mkDirAndCopy(srcStat.mode, src, dest, opts) + } + return copyDir(src, dest, opts) } -async function mkDirAndCopy(srcMode, src, dest, opts) { - await mkdir(dest); - await copyDir(src, dest, opts); - return setDestMode(dest, srcMode); +async function mkDirAndCopy (srcMode, src, dest, opts) { + await mkdir(dest) + await copyDir(src, dest, opts) + return setDestMode(dest, srcMode) } -async function copyDir(src, dest, opts) { - const dir = await readdir(src); +async function copyDir (src, dest, opts) { + const dir = await readdir(src) for (let i = 0; i < dir.length; i++) { - const item = dir[i]; - const srcItem = join(src, item); - const destItem = join(dest, item); - const { destStat } = await checkPaths(srcItem, destItem, opts); - await startCopy(destStat, srcItem, destItem, opts); + const item = dir[i] + const srcItem = join(src, item) + const destItem = join(dest, item) + const { destStat } = await checkPaths(srcItem, destItem, opts) + await startCopy(destStat, srcItem, destItem, opts) } } -async function onLink(destStat, src, dest) { - let resolvedSrc = await readlink(src); +async function onLink (destStat, src, dest) { + let resolvedSrc = await readlink(src) if (!isAbsolute(resolvedSrc)) { - resolvedSrc = resolve(dirname(src), resolvedSrc); + resolvedSrc = resolve(dirname(src), resolvedSrc) } if (!destStat) { - return symlink(resolvedSrc, dest); + return symlink(resolvedSrc, dest) } - let resolvedDest; + let resolvedDest try { - resolvedDest = await readlink(dest); + resolvedDest = await readlink(dest) } catch (err) { // Dest exists and is a regular file or directory, // Windows may throw UNKNOWN error. If dest already exists, // fs throws error anyway, so no need to guard against it here. // istanbul ignore next: can only test on windows if (err.code === 'EINVAL' || err.code === 'UNKNOWN') { - return symlink(resolvedSrc, dest); + return symlink(resolvedSrc, dest) } // istanbul ignore next: should not be possible - throw err; + throw err } if (!isAbsolute(resolvedDest)) { - resolvedDest = resolve(dirname(dest), resolvedDest); + resolvedDest = resolve(dirname(dest), resolvedDest) } if (isSrcSubdir(resolvedSrc, resolvedDest)) { throw new ERR_FS_CP_EINVAL({ @@ -392,26 +403,26 @@ async function onLink(destStat, src, dest) { path: dest, syscall: 'cp', errno: EINVAL, - }); + }) } // Do not copy if src is a subdir of dest since unlinking // dest in this case would result in removing src contents // and therefore a broken symlink would be created. - const srcStat = await stat(src); + const srcStat = await stat(src) if (srcStat.isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) { throw new ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY({ message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`, path: dest, syscall: 'cp', errno: EINVAL, - }); + }) } - return copyLink(resolvedSrc, dest); + return copyLink(resolvedSrc, dest) } -async function copyLink(resolvedSrc, dest) { - await unlink(dest); - return symlink(resolvedSrc, dest); +async function copyLink (resolvedSrc, dest) { + await unlink(dest) + return symlink(resolvedSrc, dest) } module.exports = cp diff --git a/lib/errors.js b/lib/errors.js index 673e976..1cd1e05 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -2,26 +2,23 @@ const { inspect } = require('util') // adapted from node's internal/errors -// https://github.com/nodejs/node/blob/c8a04049be96d2a6d625d4417df095fc0f3eaa7b/lib/internal/errors.js +// https://github.com/nodejs/node/blob/c8a04049/lib/internal/errors.js // close copy of node's internal SystemError class. class SystemError { - constructor(code, prefix, context) { + constructor (code, prefix, context) { // XXX context.code is undefined in all constructors used in cp/polyfill - // that may be a bug copied from node - // await require('fs/promises').cp('error.ts', 'error.ts') - // - // Uncaught: - // SystemError [ERR_FS_CP_EINVAL]: Invalid src or dest: cp returned undefined (src and dest cannot be the same) error.ts - // maybe the constructor should use `code` not `errno`? - // nodejs/node#41104 + // that may be a bug copied from node, maybe the constructor should use + // `code` not `errno`? nodejs/node#41104 let message = `${prefix}: ${context.syscall} returned ` + `${context.code} (${context.message})` - if (context.path !== undefined) + if (context.path !== undefined) { message += ` ${context.path}` - if (context.dest !== undefined) + } + if (context.dest !== undefined) { message += ` => ${context.dest}` + } this.code = code Object.defineProperties(this, { @@ -44,14 +41,22 @@ class SystemError { writable: false, }, errno: { - get() { return context.errno }, - set(value) { context.errno = value }, + get () { + return context.errno + }, + set (value) { + context.errno = value + }, enumerable: true, configurable: true, }, syscall: { - get() { return context.syscall }, - set(value) { context.syscall = value }, + get () { + return context.syscall + }, + set (value) { + context.syscall = value + }, enumerable: true, configurable: true, }, @@ -59,39 +64,47 @@ class SystemError { if (context.path !== undefined) { Object.defineProperty(this, 'path', { - get() { return context.path }, - set(value) { context.path = value }, + get () { + return context.path + }, + set (value) { + context.path = value + }, enumerable: true, - configurable: true + configurable: true, }) } if (context.dest !== undefined) { Object.defineProperty(this, 'dest', { - get() { return context.dest }, - set(value) { context.dest = value }, + get () { + return context.dest + }, + set (value) { + context.dest = value + }, enumerable: true, - configurable: true + configurable: true, }) } } - toString() { + toString () { return `${this.name} [${this.code}]: ${this.message}` } - [Symbol.for('nodejs.util.inspect.custom')](_recurseTimes, ctx) { + [Symbol.for('nodejs.util.inspect.custom')] (_recurseTimes, ctx) { return inspect(this, { ...ctx, getters: true, - customInspect: false + customInspect: false, }) } } function E (code, message) { module.exports[code] = class NodeError extends SystemError { - constructor(ctx) { + constructor (ctx) { super(code, message, ctx) } } @@ -108,7 +121,7 @@ E('ERR_FS_CP_UNKNOWN', 'Cannot copy an unknown file type') E('ERR_FS_EISDIR', 'Path is a directory') module.exports.ERR_INVALID_ARG_TYPE = class ERR_INVALID_ARG_TYPE extends Error { - constructor(name, expected, actual) { + constructor (name, expected, actual) { super() this.code = 'ERR_INVALID_ARG_TYPE' this.message = `The ${name} argument must be ${expected}. Received ${typeof actual}` diff --git a/test/cp/index.js b/test/cp/index.js index 16ef58a..5ff75fd 100644 --- a/test/cp/index.js +++ b/test/cp/index.js @@ -26,4 +26,3 @@ t.test('can copy a directory', async (t) => { t.equal(await fs.exists(dest), true, 'dest exists') }) - diff --git a/test/cp/polyfill.js b/test/cp/polyfill.js index 1401b89..bb45e12 100644 --- a/test/cp/polyfill.js +++ b/test/cp/polyfill.js @@ -1,4 +1,4 @@ -const fs = require('fs'); +const fs = require('fs') const { lstatSync, mkdirSync, @@ -8,16 +8,16 @@ const { symlinkSync, statSync, writeFileSync, -} = fs; +} = fs -const net = require('net'); -const { join } = require('path'); -const { pathToFileURL } = require('url'); -const t = require('tap'); +const net = require('net') +const { join } = require('path') +const { pathToFileURL } = require('url') +const t = require('tap') const cp = require('../../lib/cp/polyfill') -const isWindows = process.platform === 'win32'; +const isWindows = process.platform === 'win32' const tmpdir = t.testdir({ 'kitchen-sink': { a: { @@ -40,25 +40,25 @@ const tmpdir = t.testdir({ }) const kitchenSink = join(tmpdir, 'kitchen-sink') -let dirc = 0; -function nextdir() { - return join(tmpdir, `copy_${++dirc}`); +let dirc = 0 +function nextdir () { + return join(tmpdir, `copy_${++dirc}`) } t.test('It copies a nested folder structure with files and folders.', async t => { - const src = kitchenSink; - const dest = nextdir(); + const src = kitchenSink + const dest = nextdir() await cp(src, dest, { recursive: true }) - assertDirEquivalent(t, src, dest); + assertDirEquivalent(t, src, dest) }) t.test('It does not throw errors when directory is copied over and force is false.', async t => { - const src = nextdir(); - mkdirSync(join(src, 'a', 'b'), { recursive: true }); - writeFileSync(join(src, 'README.md'), 'hello world', 'utf8'); - const dest = nextdir(); - await cp(src, dest, { dereference: true, recursive: true }); - const initialStat = lstatSync(join(dest, 'README.md')); + const src = nextdir() + mkdirSync(join(src, 'a', 'b'), { recursive: true }) + writeFileSync(join(src, 'README.md'), 'hello world', 'utf8') + const dest = nextdir() + await cp(src, dest, { dereference: true, recursive: true }) + const initialStat = lstatSync(join(dest, 'README.md')) await cp(src, dest, { dereference: true, force: false, @@ -66,66 +66,66 @@ t.test('It does not throw errors when directory is copied over and force is fals }) // File should not have been copied over, so access times will be identical: - const finalStat = lstatSync(join(dest, 'README.md')); - t.equal(finalStat.ctime.getTime(), initialStat.ctime.getTime()); + const finalStat = lstatSync(join(dest, 'README.md')) + t.equal(finalStat.ctime.getTime(), initialStat.ctime.getTime()) }) t.test('It overwrites existing files if force is true.', async t => { - const src = kitchenSink; - const dest = nextdir(); - mkdirSync(dest, { recursive: true }); - writeFileSync(join(dest, 'README.md'), '# Goodbye', 'utf8'); + const src = kitchenSink + const dest = nextdir() + mkdirSync(dest, { recursive: true }) + writeFileSync(join(dest, 'README.md'), '# Goodbye', 'utf8') await cp(src, dest, { recursive: true }) - assertDirEquivalent(t, src, dest); - const content = readFileSync(join(dest, 'README.md'), 'utf8'); - t.equal(content.trim(), '# Hello'); + assertDirEquivalent(t, src, dest) + const content = readFileSync(join(dest, 'README.md'), 'utf8') + t.equal(content.trim(), '# Hello') }) -t.test('It does not fail if the same directory is copied to dest twice, when dereference is true, and force is false (fails silently).', async t => { - const src = kitchenSink; - const dest = nextdir(); - const destFile = join(dest, 'a/b/README2.md'); - await cp(src, dest, { dereference: true, recursive: true }); +t.test('It can overwrite directory when dereference is true and force is false', async t => { + const src = kitchenSink + const dest = nextdir() + const destFile = join(dest, 'a/b/README2.md') + await cp(src, dest, { dereference: true, recursive: true }) await cp(src, dest, { dereference: true, - recursive: true + recursive: true, }) - const stat = lstatSync(destFile); - t.ok(stat.isFile()); + const stat = lstatSync(destFile) + t.ok(stat.isFile()) }) t.test('It copies file itself, rather than symlink, when dereference is true.', async t => { - const src = nextdir(); - mkdirSync(src, { recursive: true }); - writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); - symlinkSync(join(src, 'foo.js'), join(src, 'bar.js')); + const src = nextdir() + mkdirSync(src, { recursive: true }) + writeFileSync(join(src, 'foo.js'), 'foo', 'utf8') + symlinkSync(join(src, 'foo.js'), join(src, 'bar.js')) - const dest = nextdir(); - mkdirSync(dest, { recursive: true }); - const destFile = join(dest, 'foo.js'); + const dest = nextdir() + mkdirSync(dest, { recursive: true }) + const destFile = join(dest, 'foo.js') await cp(join(src, 'bar.js'), destFile, { dereference: true }) - const stat = lstatSync(destFile); - t.ok(stat.isFile()); + const stat = lstatSync(destFile) + t.ok(stat.isFile()) }) t.test('It copies relative symlinks', async t => { - const src = nextdir(); - mkdirSync(src, { recursive: true }); - writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); - symlinkSync('./foo.js', join(src, 'bar.js')); - - const dest = nextdir(); - const destFile = join(dest, 'bar.js'); - mkdirSync(dest, { recursive: true }); - writeFileSync(join(dest, 'foo.js'), 'foo', 'utf8'); - symlinkSync('./foo.js', destFile); - - await cp(src, dest, {recursive: true}) - const stat = lstatSync(destFile); - t.ok(stat.isSymbolicLink()); + const src = nextdir() + mkdirSync(src, { recursive: true }) + writeFileSync(join(src, 'foo.js'), 'foo', 'utf8') + symlinkSync('./foo.js', join(src, 'bar.js')) + + const dest = nextdir() + const destFile = join(dest, 'bar.js') + mkdirSync(dest, { recursive: true }) + writeFileSync(join(dest, 'foo.js'), 'foo', 'utf8') + symlinkSync('./foo.js', destFile) + + await cp(src, dest, { recursive: true }) + const stat = lstatSync(destFile) + t.ok(stat.isSymbolicLink()) }) t.test('It returns error when src and dest are identical.', async t => { @@ -135,133 +135,133 @@ t.test('It returns error when src and dest are identical.', async t => { }) t.test('It returns error if symlink in src points to location in dest.', async t => { - const src = nextdir(); - mkdirSync(src, { recursive: true }); - const dest = nextdir(); - mkdirSync(dest); - symlinkSync(dest, join(src, 'link')); - await cp(src, dest, { recursive: true }); + const src = nextdir() + mkdirSync(src, { recursive: true }) + const dest = nextdir() + mkdirSync(dest) + symlinkSync(dest, join(src, 'link')) + await cp(src, dest, { recursive: true }) t.rejects( cp(src, dest, { recursive: true }), { code: 'ERR_FS_CP_EINVAL' }) }) t.test('It returns error if symlink in dest points to location in src.', async t => { - const src = nextdir(); - mkdirSync(join(src, 'a', 'b'), { recursive: true }); - symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); + const src = nextdir() + mkdirSync(join(src, 'a', 'b'), { recursive: true }) + symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')) - const dest = nextdir(); - mkdirSync(join(dest, 'a'), { recursive: true }); - symlinkSync(src, join(dest, 'a', 'c')); + const dest = nextdir() + mkdirSync(join(dest, 'a'), { recursive: true }) + symlinkSync(src, join(dest, 'a', 'c')) t.rejects( cp(src, dest, { recursive: true }), { code: 'ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY' }) }) t.test('It returns error if parent directory of symlink in dest points to src.', async t => { - const src = nextdir(); - mkdirSync(join(src, 'a'), { recursive: true }); - const dest = nextdir(); + const src = nextdir() + mkdirSync(join(src, 'a'), { recursive: true }) + const dest = nextdir() // Create symlink in dest pointing to src. - const destLink = join(dest, 'b'); - mkdirSync(dest, { recursive: true }); - symlinkSync(src, destLink); + const destLink = join(dest, 'b') + mkdirSync(dest, { recursive: true }) + symlinkSync(src, destLink) t.rejects( cp(src, join(dest, 'b', 'c')), - { code: 'ERR_FS_CP_EINVAL'}); + { code: 'ERR_FS_CP_EINVAL' }) }) t.test('It returns error if attempt is made to copy directory to file.', async t => { - const src = nextdir(); - mkdirSync(src, { recursive: true }); - const dest = join(kitchenSink, 'README.md'); + const src = nextdir() + mkdirSync(src, { recursive: true }) + const dest = join(kitchenSink, 'README.md') t.rejects( cp(src, dest), - { code: 'ERR_FS_CP_DIR_TO_NON_DIR'}); + { code: 'ERR_FS_CP_DIR_TO_NON_DIR' }) }) t.test('It allows file to be copied to a file path.', async t => { - const srcFile = join(kitchenSink, 'README.md'); - const destFile = join(nextdir(), 'index.js'); + const srcFile = join(kitchenSink, 'README.md') + const destFile = join(nextdir(), 'index.js') await cp(srcFile, destFile, { dereference: true }) - const stat = lstatSync(destFile); - t.ok(stat.isFile()); + const stat = lstatSync(destFile) + t.ok(stat.isFile()) }) t.test('It returns error if directory copied without recursive flag.', async t => { - const src = kitchenSink; - const dest = nextdir(); + const src = kitchenSink + const dest = nextdir() t.rejects( cp(src, dest), - { code: 'ERR_FS_EISDIR'}); + { code: 'ERR_FS_EISDIR' }) }) t.test('It returns error if attempt is made to copy file to directory.', async t => { - const src = join(kitchenSink, 'README.md'); - const dest = nextdir(); - mkdirSync(dest, { recursive: true }); + const src = join(kitchenSink, 'README.md') + const dest = nextdir() + mkdirSync(dest, { recursive: true }) t.rejects( cp(src, dest), - { code: 'ERR_FS_CP_NON_DIR_TO_DIR' }); + { code: 'ERR_FS_CP_NON_DIR_TO_DIR' }) }) t.test('It returns error if attempt is made to copy to subdirectory of self.', async t => { - const src = kitchenSink; - const dest = join(kitchenSink, 'a'); + const src = kitchenSink + const dest = join(kitchenSink, 'a') t.rejects( cp(src, dest), - { code: 'ERR_FS_CP_EINVAL' }); + { code: 'ERR_FS_CP_EINVAL' }) }) t.test('It returns an error if attempt is made to copy socket.', { skip: isWindows }, async t => { - const dest = nextdir(); - const sock = `${process.pid}.sock`; - const server = net.createServer(); - server.listen(sock); + const dest = nextdir() + const sock = `${process.pid}.sock` + const server = net.createServer() + server.listen(sock) t.teardown(() => server.close()) t.rejects( cp(sock, dest), - { code: 'ERR_FS_CP_SOCKET' }); + { code: 'ERR_FS_CP_SOCKET' }) }) t.test('It copies timestamps from src to dest if preserveTimestamps is true.', async t => { - const src = kitchenSink; - const dest = nextdir(); + const src = kitchenSink + const dest = nextdir() await cp(src, dest, { preserveTimestamps: true, - recursive: true + recursive: true, }) - assertDirEquivalent(t, src, dest); - const srcStat = lstatSync(join(src, 'index.js')); - const destStat = lstatSync(join(dest, 'index.js')); - t.equal(srcStat.mtime.getTime(), destStat.mtime.getTime()); + assertDirEquivalent(t, src, dest) + const srcStat = lstatSync(join(src, 'index.js')) + const destStat = lstatSync(join(dest, 'index.js')) + t.equal(srcStat.mtime.getTime(), destStat.mtime.getTime()) }) t.test('It applies filter function.', async t => { - const src = kitchenSink; - const dest = nextdir(); + const src = kitchenSink + const dest = nextdir() await cp(src, dest, { filter: (path) => { - const pathStat = statSync(path); - return pathStat.isDirectory() || path.endsWith('.js'); + const pathStat = statSync(path) + return pathStat.isDirectory() || path.endsWith('.js') }, dereference: true, recursive: true, }) - const destEntries = []; - collectEntries(dest, destEntries); + const destEntries = [] + collectEntries(dest, destEntries) for (const entry of destEntries) { t.equal( entry.isDirectory() || entry.name.endsWith('.js'), true - ); + ) } }) t.test('It supports async filter function.', async t => { - const src = kitchenSink; - const dest = nextdir(); + const src = kitchenSink + const dest = nextdir() await cp(src, dest, { filter: async (path) => { const pathStat = statSync(path) @@ -270,20 +270,20 @@ t.test('It supports async filter function.', async t => { dereference: true, recursive: true, }) - const destEntries = []; - collectEntries(dest, destEntries); + const destEntries = [] + collectEntries(dest, destEntries) for (const entry of destEntries) { t.equal( entry.isDirectory() || entry.name.endsWith('.js'), true - ); + ) } }) -t.test('It returns error if errorOnExist is true, force is false, and file or folder copied over.', async t => { - const src = kitchenSink; - const dest = nextdir(); - await cp(src, dest, { recursive: true }); +t.test('It errors on overwrite if force is false and errorOnExist is true', async t => { + const src = kitchenSink + const dest = nextdir() + await cp(src, dest, { recursive: true }) t.rejects( cp(src, dest, { dereference: true, @@ -291,100 +291,99 @@ t.test('It returns error if errorOnExist is true, force is false, and file or fo force: false, recursive: true, }), - { code: 'ERR_FS_CP_EEXIST' }); + { code: 'ERR_FS_CP_EEXIST' }) }) t.test('It returns EEXIST error if attempt is made to copy symlink over file.', async t => { - const src = nextdir(); - mkdirSync(join(src, 'a', 'b'), { recursive: true }); - symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); + const src = nextdir() + mkdirSync(join(src, 'a', 'b'), { recursive: true }) + symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')) - const dest = nextdir(); - mkdirSync(join(dest, 'a'), { recursive: true }); - writeFileSync(join(dest, 'a', 'c'), 'hello', 'utf8'); + const dest = nextdir() + mkdirSync(join(dest, 'a'), { recursive: true }) + writeFileSync(join(dest, 'a', 'c'), 'hello', 'utf8') t.rejects( cp(src, dest, { recursive: true }), - { code: 'EEXIST' }); + { code: 'EEXIST' }) }) t.test('It makes file writeable when updating timestamp, if not writeable.', async t => { - const src = nextdir(); - mkdirSync(src, { recursive: true }); - const dest = nextdir(); - mkdirSync(dest, { recursive: true }); - writeFileSync(join(src, 'foo.txt'), 'foo', { mode: 0o444 }); + const src = nextdir() + mkdirSync(src, { recursive: true }) + const dest = nextdir() + mkdirSync(dest, { recursive: true }) + writeFileSync(join(src, 'foo.txt'), 'foo', { mode: 0o444 }) await cp(src, dest, { preserveTimestamps: true, recursive: true, }) - assertDirEquivalent(t, src, dest); - const srcStat = lstatSync(join(src, 'foo.txt')); - const destStat = lstatSync(join(dest, 'foo.txt')); - t.equal(srcStat.mtime.getTime(), destStat.mtime.getTime()); + assertDirEquivalent(t, src, dest) + const srcStat = lstatSync(join(src, 'foo.txt')) + const destStat = lstatSync(join(dest, 'foo.txt')) + t.equal(srcStat.mtime.getTime(), destStat.mtime.getTime()) }) t.test('It copies link if it does not point to folder in src.', async t => { - const src = nextdir(); - mkdirSync(join(src, 'a', 'b'), { recursive: true }); - symlinkSync(src, join(src, 'a', 'c')); - const dest = nextdir(); - mkdirSync(join(dest, 'a'), { recursive: true }); - symlinkSync(dest, join(dest, 'a', 'c')); + const src = nextdir() + mkdirSync(join(src, 'a', 'b'), { recursive: true }) + symlinkSync(src, join(src, 'a', 'c')) + const dest = nextdir() + mkdirSync(join(dest, 'a'), { recursive: true }) + symlinkSync(dest, join(dest, 'a', 'c')) await cp(src, dest, { recursive: true }) - const link = readlinkSync(join(dest, 'a', 'c')); - t.equal(link, src); + const link = readlinkSync(join(dest, 'a', 'c')) + t.equal(link, src) }) t.test('It accepts file URL as src and dest.', async t => { - const src = kitchenSink; - const dest = nextdir(); + const src = kitchenSink + const dest = nextdir() await cp(pathToFileURL(src), pathToFileURL(dest), { recursive: true }) - assertDirEquivalent(t, src, dest); + assertDirEquivalent(t, src, dest) }) t.test('It throws if options is not object.', async t => { t.rejects( () => cp('a', 'b', 'hello'), - { code: 'ERR_INVALID_ARG_TYPE' }); + { code: 'ERR_INVALID_ARG_TYPE' }) }) t.test('It throws ENAMETOOLONG when name is too long', async t => { - const src = nextdir(); - mkdirSync(src, { recursive: true }); - const dest = join(tmpdir, 'a'.repeat(10_000)); + const src = nextdir() + mkdirSync(src, { recursive: true }) + const dest = join(tmpdir, 'a'.repeat(10000)) t.rejects( cp(src, dest), - { code: 'ENAMETOOLONG' }); + { code: 'ENAMETOOLONG' }) }) -function assertDirEquivalent(t, dir1, dir2) { - const dir1Entries = []; - collectEntries(dir1, dir1Entries); - const dir2Entries = []; - collectEntries(dir2, dir2Entries); - t.equal(dir1Entries.length, dir2Entries.length); +function assertDirEquivalent (t, dir1, dir2) { + const dir1Entries = [] + collectEntries(dir1, dir1Entries) + const dir2Entries = [] + collectEntries(dir2, dir2Entries) + t.equal(dir1Entries.length, dir2Entries.length) for (const entry1 of dir1Entries) { const entry2 = dir2Entries.find((entry) => { - return entry.name === entry1.name; - }); - t.ok(entry2, `entry ${entry2.name} not copied`); + return entry.name === entry1.name + }) + t.ok(entry2, `entry ${entry2.name} not copied`) if (entry1.isFile()) { - t.ok(entry2.isFile(), `${entry2.name} was not file`); + t.ok(entry2.isFile(), `${entry2.name} was not file`) } else if (entry1.isDirectory()) { - t.ok(entry2.isDirectory(), `${entry2.name} was not directory`); + t.ok(entry2.isDirectory(), `${entry2.name} was not directory`) } else if (entry1.isSymbolicLink()) { - t.ok(entry2.isSymbolicLink(), `${entry2.name} was not symlink`); + t.ok(entry2.isSymbolicLink(), `${entry2.name} was not symlink`) } } } -function collectEntries(dir, dirEntries) { - const newEntries = readdirSync(dir, { withFileTypes: true }); +function collectEntries (dir, dirEntries) { + const newEntries = readdirSync(dir, { withFileTypes: true }) for (const entry of newEntries) { if (entry.isDirectory()) { - collectEntries(join(dir, entry.name), dirEntries); + collectEntries(join(dir, entry.name), dirEntries) } } - dirEntries.push(...newEntries); + dirEntries.push(...newEntries) } - From 730a8515247469e0c1c4f8ca0baf88e6c2b6ab7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caleb=20=E3=83=84=20Everett?= Date: Wed, 8 Dec 2021 13:34:00 -0800 Subject: [PATCH 11/12] fix(cp): remove ENAMETOOLONG test I added this test trying to cover L158-163. It worked on linux and covered the block but not windows. --- test/cp/polyfill.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test/cp/polyfill.js b/test/cp/polyfill.js index bb45e12..a75e80d 100644 --- a/test/cp/polyfill.js +++ b/test/cp/polyfill.js @@ -348,15 +348,6 @@ t.test('It throws if options is not object.', async t => { { code: 'ERR_INVALID_ARG_TYPE' }) }) -t.test('It throws ENAMETOOLONG when name is too long', async t => { - const src = nextdir() - mkdirSync(src, { recursive: true }) - const dest = join(tmpdir, 'a'.repeat(10000)) - t.rejects( - cp(src, dest), - { code: 'ENAMETOOLONG' }) -}) - function assertDirEquivalent (t, dir1, dir2) { const dir1Entries = [] collectEntries(dir1, dir1Entries) From 60d3dc7b6512936d9cc046b4fec9f7147b7b4a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caleb=20=E3=83=84=20Everett?= Date: Wed, 8 Dec 2021 15:13:34 -0800 Subject: [PATCH 12/12] docs(cp): add cp to readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a180c91..bc71a11 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ polyfills, and extensions, of the core `fs` module. - `fs.mkdtemp` extended to accept an `owner` option - `fs.writeFile` extended to accept an `owner` option - `fs.withTempDir` added +- `fs.cp` polyfill for node < 16.7.0 ## The `owner` option