Spaces:
Running
Running
// 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 | |
const { | |
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, | |
ERR_INVALID_ARG_TYPE, | |
} = require('./errors.js') | |
const { | |
constants: { | |
errno: { | |
EEXIST, | |
EISDIR, | |
EINVAL, | |
ENOTDIR, | |
}, | |
}, | |
} = require('os') | |
const { | |
chmod, | |
copyFile, | |
lstat, | |
mkdir, | |
readdir, | |
readlink, | |
stat, | |
symlink, | |
unlink, | |
utimes, | |
} = require('fs/promises') | |
const { | |
dirname, | |
isAbsolute, | |
join, | |
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 != null && 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 | |
// 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') | |
} | |
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 Promise.all([ | |
statFunc(src), | |
statFunc(dest).catch((err) => { | |
// istanbul ignore next: unsure how to cover. | |
if (err.code === 'ENOENT') { | |
return null | |
} | |
// istanbul ignore next: unsure how to cover. | |
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 stat(dest).then( | |
() => true, | |
// istanbul ignore next: not sure when this would occur | |
(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)) | |
if (destParent === srcParent || destParent === parse(destParent).root) { | |
return | |
} | |
let destStat | |
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)) { | |
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) => | |
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) | |
} | |
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) | |
// istanbul ignore else: can't portably test FIFO | |
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, | |
}) | |
} | |
// istanbul ignore next: should be unreachable | |
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. | |
// 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)) { | |
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 = cp | |