1'use strict'; 2 3// This file is a modified version of the fs-extra's copy method. 4 5const { 6 ArrayPrototypeEvery, 7 ArrayPrototypeFilter, 8 Boolean, 9 PromisePrototypeThen, 10 PromiseReject, 11 SafePromiseAll, 12 StringPrototypeSplit, 13} = primordials; 14const { 15 codes: { 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 }, 26} = require('internal/errors'); 27const { 28 os: { 29 errno: { 30 EEXIST, 31 EISDIR, 32 EINVAL, 33 ENOTDIR, 34 }, 35 }, 36} = internalBinding('constants'); 37const { 38 chmod, 39 copyFile, 40 lstat, 41 mkdir, 42 opendir, 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} = require('path'); 57 58async function cpFn(src, dest, opts) { 59 // Warn about using preserveTimestamps on 32-bit node 60 if (opts.preserveTimestamps && process.arch === 'ia32') { 61 const warning = 'Using the preserveTimestamps option in 32-bit ' + 62 'node is not recommended'; 63 process.emitWarning(warning, 'TimestampPrecisionWarning'); 64 } 65 const stats = await checkPaths(src, dest, opts); 66 const { srcStat, destStat, skipped } = stats; 67 if (skipped) return; 68 await checkParentPaths(src, srcStat, dest); 69 return checkParentDir(destStat, src, dest, opts); 70} 71 72async function checkPaths(src, dest, opts) { 73 if (opts.filter && !(await opts.filter(src, dest))) { 74 return { __proto__: null, skipped: true }; 75 } 76 const { 0: srcStat, 1: destStat } = await getStats(src, dest, opts); 77 if (destStat) { 78 if (areIdentical(srcStat, destStat)) { 79 throw new ERR_FS_CP_EINVAL({ 80 message: 'src and dest cannot be the same', 81 path: dest, 82 syscall: 'cp', 83 errno: EINVAL, 84 code: 'EINVAL', 85 }); 86 } 87 if (srcStat.isDirectory() && !destStat.isDirectory()) { 88 throw new ERR_FS_CP_DIR_TO_NON_DIR({ 89 message: `cannot overwrite directory ${src} ` + 90 `with non-directory ${dest}`, 91 path: dest, 92 syscall: 'cp', 93 errno: EISDIR, 94 code: 'EISDIR', 95 }); 96 } 97 if (!srcStat.isDirectory() && destStat.isDirectory()) { 98 throw new ERR_FS_CP_NON_DIR_TO_DIR({ 99 message: `cannot overwrite non-directory ${src} ` + 100 `with directory ${dest}`, 101 path: dest, 102 syscall: 'cp', 103 errno: ENOTDIR, 104 code: 'ENOTDIR', 105 }); 106 } 107 } 108 109 if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { 110 throw new ERR_FS_CP_EINVAL({ 111 message: `cannot copy ${src} to a subdirectory of self ${dest}`, 112 path: dest, 113 syscall: 'cp', 114 errno: EINVAL, 115 code: 'EINVAL', 116 }); 117 } 118 return { srcStat, destStat, skipped: false }; 119} 120 121function areIdentical(srcStat, destStat) { 122 return destStat.ino && destStat.dev && destStat.ino === srcStat.ino && 123 destStat.dev === srcStat.dev; 124} 125 126function getStats(src, dest, opts) { 127 const statFunc = opts.dereference ? 128 (file) => stat(file, { bigint: true }) : 129 (file) => lstat(file, { bigint: true }); 130 return SafePromiseAll([ 131 statFunc(src), 132 PromisePrototypeThen(statFunc(dest), undefined, (err) => { 133 if (err.code === 'ENOENT') return null; 134 throw err; 135 }), 136 ]); 137} 138 139async function checkParentDir(destStat, src, dest, opts) { 140 const destParent = dirname(dest); 141 const dirExists = await pathExists(destParent); 142 if (dirExists) return getStatsForCopy(destStat, src, dest, opts); 143 await mkdir(destParent, { recursive: true }); 144 return getStatsForCopy(destStat, src, dest, opts); 145} 146 147function pathExists(dest) { 148 return PromisePrototypeThen( 149 stat(dest), 150 () => true, 151 (err) => (err.code === 'ENOENT' ? false : PromiseReject(err))); 152} 153 154// Recursively check if dest parent is a subdirectory of src. 155// It works for all file types including symlinks since it 156// checks the src and dest inodes. It starts from the deepest 157// parent and stops once it reaches the src parent or the root path. 158async function checkParentPaths(src, srcStat, dest) { 159 const srcParent = resolve(dirname(src)); 160 const destParent = resolve(dirname(dest)); 161 if (destParent === srcParent || destParent === parse(destParent).root) { 162 return; 163 } 164 let destStat; 165 try { 166 destStat = await stat(destParent, { bigint: true }); 167 } catch (err) { 168 if (err.code === 'ENOENT') return; 169 throw err; 170 } 171 if (areIdentical(srcStat, destStat)) { 172 throw new ERR_FS_CP_EINVAL({ 173 message: `cannot copy ${src} to a subdirectory of self ${dest}`, 174 path: dest, 175 syscall: 'cp', 176 errno: EINVAL, 177 code: 'EINVAL', 178 }); 179 } 180 return checkParentPaths(src, srcStat, destParent); 181} 182 183const normalizePathToArray = (path) => 184 ArrayPrototypeFilter(StringPrototypeSplit(resolve(path), sep), Boolean); 185 186// Return true if dest is a subdir of src, otherwise false. 187// It only checks the path strings. 188function isSrcSubdir(src, dest) { 189 const srcArr = normalizePathToArray(src); 190 const destArr = normalizePathToArray(dest); 191 return ArrayPrototypeEvery(srcArr, (cur, i) => destArr[i] === cur); 192} 193 194async function getStatsForCopy(destStat, src, dest, opts) { 195 const statFn = opts.dereference ? stat : lstat; 196 const srcStat = await statFn(src); 197 if (srcStat.isDirectory() && opts.recursive) { 198 return onDir(srcStat, destStat, src, dest, opts); 199 } else if (srcStat.isDirectory()) { 200 throw new ERR_FS_EISDIR({ 201 message: `${src} is a directory (not copied)`, 202 path: src, 203 syscall: 'cp', 204 errno: EISDIR, 205 code: 'EISDIR', 206 }); 207 } else if (srcStat.isFile() || 208 srcStat.isCharacterDevice() || 209 srcStat.isBlockDevice()) { 210 return onFile(srcStat, destStat, src, dest, opts); 211 } else if (srcStat.isSymbolicLink()) { 212 return onLink(destStat, src, dest, opts); 213 } else if (srcStat.isSocket()) { 214 throw new ERR_FS_CP_SOCKET({ 215 message: `cannot copy a socket file: ${dest}`, 216 path: dest, 217 syscall: 'cp', 218 errno: EINVAL, 219 code: 'EINVAL', 220 }); 221 } else if (srcStat.isFIFO()) { 222 throw new ERR_FS_CP_FIFO_PIPE({ 223 message: `cannot copy a FIFO pipe: ${dest}`, 224 path: dest, 225 syscall: 'cp', 226 errno: EINVAL, 227 code: 'EINVAL', 228 }); 229 } 230 throw new ERR_FS_CP_UNKNOWN({ 231 message: `cannot copy an unknown file type: ${dest}`, 232 path: dest, 233 syscall: 'cp', 234 errno: EINVAL, 235 code: 'EINVAL', 236 }); 237} 238 239function onFile(srcStat, destStat, src, dest, opts) { 240 if (!destStat) return _copyFile(srcStat, src, dest, opts); 241 return mayCopyFile(srcStat, src, dest, opts); 242} 243 244async function mayCopyFile(srcStat, src, dest, opts) { 245 if (opts.force) { 246 await unlink(dest); 247 return _copyFile(srcStat, src, dest, opts); 248 } else if (opts.errorOnExist) { 249 throw new ERR_FS_CP_EEXIST({ 250 message: `${dest} already exists`, 251 path: dest, 252 syscall: 'cp', 253 errno: EEXIST, 254 code: 'EEXIST', 255 }); 256 } 257} 258 259async function _copyFile(srcStat, src, dest, opts) { 260 await copyFile(src, dest, opts.mode); 261 if (opts.preserveTimestamps) { 262 return handleTimestampsAndMode(srcStat.mode, src, dest); 263 } 264 return setDestMode(dest, srcStat.mode); 265} 266 267async function handleTimestampsAndMode(srcMode, src, dest) { 268 // Make sure the file is writable before setting the timestamp 269 // otherwise open fails with EPERM when invoked with 'r+' 270 // (through utimes call) 271 if (fileIsNotWritable(srcMode)) { 272 await makeFileWritable(dest, srcMode); 273 return setDestTimestampsAndMode(srcMode, src, dest); 274 } 275 return setDestTimestampsAndMode(srcMode, src, dest); 276} 277 278function fileIsNotWritable(srcMode) { 279 return (srcMode & 0o200) === 0; 280} 281 282function makeFileWritable(dest, srcMode) { 283 return setDestMode(dest, srcMode | 0o200); 284} 285 286async function setDestTimestampsAndMode(srcMode, src, dest) { 287 await setDestTimestamps(src, dest); 288 return setDestMode(dest, srcMode); 289} 290 291function setDestMode(dest, srcMode) { 292 return chmod(dest, srcMode); 293} 294 295async function setDestTimestamps(src, dest) { 296 // The initial srcStat.atime cannot be trusted 297 // because it is modified by the read(2) system call 298 // (See https://nodejs.org/api/fs.html#fs_stat_time_values) 299 const updatedSrcStat = await stat(src); 300 return utimes(dest, updatedSrcStat.atime, updatedSrcStat.mtime); 301} 302 303function onDir(srcStat, destStat, src, dest, opts) { 304 if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts); 305 return copyDir(src, dest, opts); 306} 307 308async function mkDirAndCopy(srcMode, src, dest, opts) { 309 await mkdir(dest); 310 await copyDir(src, dest, opts); 311 return setDestMode(dest, srcMode); 312} 313 314async function copyDir(src, dest, opts) { 315 const dir = await opendir(src); 316 317 for await (const { name } of dir) { 318 const srcItem = join(src, name); 319 const destItem = join(dest, name); 320 const { destStat, skipped } = await checkPaths(srcItem, destItem, opts); 321 if (!skipped) await getStatsForCopy(destStat, srcItem, destItem, opts); 322 } 323} 324 325async function onLink(destStat, src, dest, opts) { 326 let resolvedSrc = await readlink(src); 327 if (!opts.verbatimSymlinks && !isAbsolute(resolvedSrc)) { 328 resolvedSrc = resolve(dirname(src), resolvedSrc); 329 } 330 if (!destStat) { 331 return symlink(resolvedSrc, dest); 332 } 333 let resolvedDest; 334 try { 335 resolvedDest = await readlink(dest); 336 } catch (err) { 337 // Dest exists and is a regular file or directory, 338 // Windows may throw UNKNOWN error. If dest already exists, 339 // fs throws error anyway, so no need to guard against it here. 340 if (err.code === 'EINVAL' || err.code === 'UNKNOWN') { 341 return symlink(resolvedSrc, dest); 342 } 343 throw err; 344 } 345 if (!isAbsolute(resolvedDest)) { 346 resolvedDest = resolve(dirname(dest), resolvedDest); 347 } 348 if (isSrcSubdir(resolvedSrc, resolvedDest)) { 349 throw new ERR_FS_CP_EINVAL({ 350 message: `cannot copy ${resolvedSrc} to a subdirectory of self ` + 351 `${resolvedDest}`, 352 path: dest, 353 syscall: 'cp', 354 errno: EINVAL, 355 code: 'EINVAL', 356 }); 357 } 358 // Do not copy if src is a subdir of dest since unlinking 359 // dest in this case would result in removing src contents 360 // and therefore a broken symlink would be created. 361 const srcStat = await stat(src); 362 if (srcStat.isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) { 363 throw new ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY({ 364 message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`, 365 path: dest, 366 syscall: 'cp', 367 errno: EINVAL, 368 code: 'EINVAL', 369 }); 370 } 371 return copyLink(resolvedSrc, dest); 372} 373 374async function copyLink(resolvedSrc, dest) { 375 await unlink(dest); 376 return symlink(resolvedSrc, dest); 377} 378 379module.exports = { 380 areIdentical, 381 cpFn, 382 isSrcSubdir, 383}; 384