1// this file is a modified version of the code in node 17.2.0 2// which is, in turn, a modified version of the fs-extra module on npm 3// node core changes: 4// - Use of the assert module has been replaced with core's error system. 5// - All code related to the glob dependency has been removed. 6// - Bring your own custom fs module is not currently supported. 7// - Some basic code cleanup. 8// changes here: 9// - remove all callback related code 10// - drop sync support 11// - change assertions back to non-internal methods (see options.js) 12// - throws ENOTDIR when rmdir gets an ENOENT for a path that exists in Windows 13'use strict' 14 15const { 16 ERR_FS_CP_DIR_TO_NON_DIR, 17 ERR_FS_CP_EEXIST, 18 ERR_FS_CP_EINVAL, 19 ERR_FS_CP_FIFO_PIPE, 20 ERR_FS_CP_NON_DIR_TO_DIR, 21 ERR_FS_CP_SOCKET, 22 ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY, 23 ERR_FS_CP_UNKNOWN, 24 ERR_FS_EISDIR, 25 ERR_INVALID_ARG_TYPE, 26} = require('./errors.js') 27const { 28 constants: { 29 errno: { 30 EEXIST, 31 EISDIR, 32 EINVAL, 33 ENOTDIR, 34 }, 35 }, 36} = require('os') 37const { 38 chmod, 39 copyFile, 40 lstat, 41 mkdir, 42 readdir, 43 readlink, 44 stat, 45 symlink, 46 unlink, 47 utimes, 48} = require('fs/promises') 49const { 50 dirname, 51 isAbsolute, 52 join, 53 parse, 54 resolve, 55 sep, 56 toNamespacedPath, 57} = require('path') 58const { fileURLToPath } = require('url') 59 60const defaultOptions = { 61 dereference: false, 62 errorOnExist: false, 63 filter: undefined, 64 force: true, 65 preserveTimestamps: false, 66 recursive: false, 67} 68 69async function cp (src, dest, opts) { 70 if (opts != null && typeof opts !== 'object') { 71 throw new ERR_INVALID_ARG_TYPE('options', ['Object'], opts) 72 } 73 return cpFn( 74 toNamespacedPath(getValidatedPath(src)), 75 toNamespacedPath(getValidatedPath(dest)), 76 { ...defaultOptions, ...opts }) 77} 78 79function getValidatedPath (fileURLOrPath) { 80 const path = fileURLOrPath != null && fileURLOrPath.href 81 && fileURLOrPath.origin 82 ? fileURLToPath(fileURLOrPath) 83 : fileURLOrPath 84 return path 85} 86 87async function cpFn (src, dest, opts) { 88 // Warn about using preserveTimestamps on 32-bit node 89 // istanbul ignore next 90 if (opts.preserveTimestamps && process.arch === 'ia32') { 91 const warning = 'Using the preserveTimestamps option in 32-bit ' + 92 'node is not recommended' 93 process.emitWarning(warning, 'TimestampPrecisionWarning') 94 } 95 const stats = await checkPaths(src, dest, opts) 96 const { srcStat, destStat } = stats 97 await checkParentPaths(src, srcStat, dest) 98 if (opts.filter) { 99 return handleFilter(checkParentDir, destStat, src, dest, opts) 100 } 101 return checkParentDir(destStat, src, dest, opts) 102} 103 104async function checkPaths (src, dest, opts) { 105 const { 0: srcStat, 1: destStat } = await getStats(src, dest, opts) 106 if (destStat) { 107 if (areIdentical(srcStat, destStat)) { 108 throw new ERR_FS_CP_EINVAL({ 109 message: 'src and dest cannot be the same', 110 path: dest, 111 syscall: 'cp', 112 errno: EINVAL, 113 }) 114 } 115 if (srcStat.isDirectory() && !destStat.isDirectory()) { 116 throw new ERR_FS_CP_DIR_TO_NON_DIR({ 117 message: `cannot overwrite directory ${src} ` + 118 `with non-directory ${dest}`, 119 path: dest, 120 syscall: 'cp', 121 errno: EISDIR, 122 }) 123 } 124 if (!srcStat.isDirectory() && destStat.isDirectory()) { 125 throw new ERR_FS_CP_NON_DIR_TO_DIR({ 126 message: `cannot overwrite non-directory ${src} ` + 127 `with directory ${dest}`, 128 path: dest, 129 syscall: 'cp', 130 errno: ENOTDIR, 131 }) 132 } 133 } 134 135 if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { 136 throw new ERR_FS_CP_EINVAL({ 137 message: `cannot copy ${src} to a subdirectory of self ${dest}`, 138 path: dest, 139 syscall: 'cp', 140 errno: EINVAL, 141 }) 142 } 143 return { srcStat, destStat } 144} 145 146function areIdentical (srcStat, destStat) { 147 return destStat.ino && destStat.dev && destStat.ino === srcStat.ino && 148 destStat.dev === srcStat.dev 149} 150 151function getStats (src, dest, opts) { 152 const statFunc = opts.dereference ? 153 (file) => stat(file, { bigint: true }) : 154 (file) => lstat(file, { bigint: true }) 155 return Promise.all([ 156 statFunc(src), 157 statFunc(dest).catch((err) => { 158 // istanbul ignore next: unsure how to cover. 159 if (err.code === 'ENOENT') { 160 return null 161 } 162 // istanbul ignore next: unsure how to cover. 163 throw err 164 }), 165 ]) 166} 167 168async function checkParentDir (destStat, src, dest, opts) { 169 const destParent = dirname(dest) 170 const dirExists = await pathExists(destParent) 171 if (dirExists) { 172 return getStatsForCopy(destStat, src, dest, opts) 173 } 174 await mkdir(destParent, { recursive: true }) 175 return getStatsForCopy(destStat, src, dest, opts) 176} 177 178function pathExists (dest) { 179 return stat(dest).then( 180 () => true, 181 // istanbul ignore next: not sure when this would occur 182 (err) => (err.code === 'ENOENT' ? false : Promise.reject(err))) 183} 184 185// Recursively check if dest parent is a subdirectory of src. 186// It works for all file types including symlinks since it 187// checks the src and dest inodes. It starts from the deepest 188// parent and stops once it reaches the src parent or the root path. 189async function checkParentPaths (src, srcStat, dest) { 190 const srcParent = resolve(dirname(src)) 191 const destParent = resolve(dirname(dest)) 192 if (destParent === srcParent || destParent === parse(destParent).root) { 193 return 194 } 195 let destStat 196 try { 197 destStat = await stat(destParent, { bigint: true }) 198 } catch (err) { 199 // istanbul ignore else: not sure when this would occur 200 if (err.code === 'ENOENT') { 201 return 202 } 203 // istanbul ignore next: not sure when this would occur 204 throw err 205 } 206 if (areIdentical(srcStat, destStat)) { 207 throw new ERR_FS_CP_EINVAL({ 208 message: `cannot copy ${src} to a subdirectory of self ${dest}`, 209 path: dest, 210 syscall: 'cp', 211 errno: EINVAL, 212 }) 213 } 214 return checkParentPaths(src, srcStat, destParent) 215} 216 217const normalizePathToArray = (path) => 218 resolve(path).split(sep).filter(Boolean) 219 220// Return true if dest is a subdir of src, otherwise false. 221// It only checks the path strings. 222function isSrcSubdir (src, dest) { 223 const srcArr = normalizePathToArray(src) 224 const destArr = normalizePathToArray(dest) 225 return srcArr.every((cur, i) => destArr[i] === cur) 226} 227 228async function handleFilter (onInclude, destStat, src, dest, opts, cb) { 229 const include = await opts.filter(src, dest) 230 if (include) { 231 return onInclude(destStat, src, dest, opts, cb) 232 } 233} 234 235function startCopy (destStat, src, dest, opts) { 236 if (opts.filter) { 237 return handleFilter(getStatsForCopy, destStat, src, dest, opts) 238 } 239 return getStatsForCopy(destStat, src, dest, opts) 240} 241 242async function getStatsForCopy (destStat, src, dest, opts) { 243 const statFn = opts.dereference ? stat : lstat 244 const srcStat = await statFn(src) 245 // istanbul ignore else: can't portably test FIFO 246 if (srcStat.isDirectory() && opts.recursive) { 247 return onDir(srcStat, destStat, src, dest, opts) 248 } else if (srcStat.isDirectory()) { 249 throw new ERR_FS_EISDIR({ 250 message: `${src} is a directory (not copied)`, 251 path: src, 252 syscall: 'cp', 253 errno: EINVAL, 254 }) 255 } else if (srcStat.isFile() || 256 srcStat.isCharacterDevice() || 257 srcStat.isBlockDevice()) { 258 return onFile(srcStat, destStat, src, dest, opts) 259 } else if (srcStat.isSymbolicLink()) { 260 return onLink(destStat, src, dest) 261 } else if (srcStat.isSocket()) { 262 throw new ERR_FS_CP_SOCKET({ 263 message: `cannot copy a socket file: ${dest}`, 264 path: dest, 265 syscall: 'cp', 266 errno: EINVAL, 267 }) 268 } else if (srcStat.isFIFO()) { 269 throw new ERR_FS_CP_FIFO_PIPE({ 270 message: `cannot copy a FIFO pipe: ${dest}`, 271 path: dest, 272 syscall: 'cp', 273 errno: EINVAL, 274 }) 275 } 276 // istanbul ignore next: should be unreachable 277 throw new ERR_FS_CP_UNKNOWN({ 278 message: `cannot copy an unknown file type: ${dest}`, 279 path: dest, 280 syscall: 'cp', 281 errno: EINVAL, 282 }) 283} 284 285function onFile (srcStat, destStat, src, dest, opts) { 286 if (!destStat) { 287 return _copyFile(srcStat, src, dest, opts) 288 } 289 return mayCopyFile(srcStat, src, dest, opts) 290} 291 292async function mayCopyFile (srcStat, src, dest, opts) { 293 if (opts.force) { 294 await unlink(dest) 295 return _copyFile(srcStat, src, dest, opts) 296 } else if (opts.errorOnExist) { 297 throw new ERR_FS_CP_EEXIST({ 298 message: `${dest} already exists`, 299 path: dest, 300 syscall: 'cp', 301 errno: EEXIST, 302 }) 303 } 304} 305 306async function _copyFile (srcStat, src, dest, opts) { 307 await copyFile(src, dest) 308 if (opts.preserveTimestamps) { 309 return handleTimestampsAndMode(srcStat.mode, src, dest) 310 } 311 return setDestMode(dest, srcStat.mode) 312} 313 314async function handleTimestampsAndMode (srcMode, src, dest) { 315 // Make sure the file is writable before setting the timestamp 316 // otherwise open fails with EPERM when invoked with 'r+' 317 // (through utimes call) 318 if (fileIsNotWritable(srcMode)) { 319 await makeFileWritable(dest, srcMode) 320 return setDestTimestampsAndMode(srcMode, src, dest) 321 } 322 return setDestTimestampsAndMode(srcMode, src, dest) 323} 324 325function fileIsNotWritable (srcMode) { 326 return (srcMode & 0o200) === 0 327} 328 329function makeFileWritable (dest, srcMode) { 330 return setDestMode(dest, srcMode | 0o200) 331} 332 333async function setDestTimestampsAndMode (srcMode, src, dest) { 334 await setDestTimestamps(src, dest) 335 return setDestMode(dest, srcMode) 336} 337 338function setDestMode (dest, srcMode) { 339 return chmod(dest, srcMode) 340} 341 342async function setDestTimestamps (src, dest) { 343 // The initial srcStat.atime cannot be trusted 344 // because it is modified by the read(2) system call 345 // (See https://nodejs.org/api/fs.html#fs_stat_time_values) 346 const updatedSrcStat = await stat(src) 347 return utimes(dest, updatedSrcStat.atime, updatedSrcStat.mtime) 348} 349 350function onDir (srcStat, destStat, src, dest, opts) { 351 if (!destStat) { 352 return mkDirAndCopy(srcStat.mode, src, dest, opts) 353 } 354 return copyDir(src, dest, opts) 355} 356 357async function mkDirAndCopy (srcMode, src, dest, opts) { 358 await mkdir(dest) 359 await copyDir(src, dest, opts) 360 return setDestMode(dest, srcMode) 361} 362 363async function copyDir (src, dest, opts) { 364 const dir = await readdir(src) 365 for (let i = 0; i < dir.length; i++) { 366 const item = dir[i] 367 const srcItem = join(src, item) 368 const destItem = join(dest, item) 369 const { destStat } = await checkPaths(srcItem, destItem, opts) 370 await startCopy(destStat, srcItem, destItem, opts) 371 } 372} 373 374async function onLink (destStat, src, dest) { 375 let resolvedSrc = await readlink(src) 376 if (!isAbsolute(resolvedSrc)) { 377 resolvedSrc = resolve(dirname(src), resolvedSrc) 378 } 379 if (!destStat) { 380 return symlink(resolvedSrc, dest) 381 } 382 let resolvedDest 383 try { 384 resolvedDest = await readlink(dest) 385 } catch (err) { 386 // Dest exists and is a regular file or directory, 387 // Windows may throw UNKNOWN error. If dest already exists, 388 // fs throws error anyway, so no need to guard against it here. 389 // istanbul ignore next: can only test on windows 390 if (err.code === 'EINVAL' || err.code === 'UNKNOWN') { 391 return symlink(resolvedSrc, dest) 392 } 393 // istanbul ignore next: should not be possible 394 throw err 395 } 396 if (!isAbsolute(resolvedDest)) { 397 resolvedDest = resolve(dirname(dest), resolvedDest) 398 } 399 if (isSrcSubdir(resolvedSrc, resolvedDest)) { 400 throw new ERR_FS_CP_EINVAL({ 401 message: `cannot copy ${resolvedSrc} to a subdirectory of self ` + 402 `${resolvedDest}`, 403 path: dest, 404 syscall: 'cp', 405 errno: EINVAL, 406 }) 407 } 408 // Do not copy if src is a subdir of dest since unlinking 409 // dest in this case would result in removing src contents 410 // and therefore a broken symlink would be created. 411 const srcStat = await stat(src) 412 if (srcStat.isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) { 413 throw new ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY({ 414 message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`, 415 path: dest, 416 syscall: 'cp', 417 errno: EINVAL, 418 }) 419 } 420 return copyLink(resolvedSrc, dest) 421} 422 423async function copyLink (resolvedSrc, dest) { 424 await unlink(dest) 425 return symlink(resolvedSrc, dest) 426} 427 428module.exports = cp 429