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 | |