| 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 |
|
|---|
| 15 | const {
|
|---|
| 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')
|
|---|
| 27 | const {
|
|---|
| 28 | constants: {
|
|---|
| 29 | errno: {
|
|---|
| 30 | EEXIST,
|
|---|
| 31 | EISDIR,
|
|---|
| 32 | EINVAL,
|
|---|
| 33 | ENOTDIR,
|
|---|
| 34 | },
|
|---|
| 35 | },
|
|---|
| 36 | } = require('os')
|
|---|
| 37 | const {
|
|---|
| 38 | chmod,
|
|---|
| 39 | copyFile,
|
|---|
| 40 | lstat,
|
|---|
| 41 | mkdir,
|
|---|
| 42 | readdir,
|
|---|
| 43 | readlink,
|
|---|
| 44 | stat,
|
|---|
| 45 | symlink,
|
|---|
| 46 | unlink,
|
|---|
| 47 | utimes,
|
|---|
| 48 | } = require('../fs.js')
|
|---|
| 49 | const {
|
|---|
| 50 | dirname,
|
|---|
| 51 | isAbsolute,
|
|---|
| 52 | join,
|
|---|
| 53 | parse,
|
|---|
| 54 | resolve,
|
|---|
| 55 | sep,
|
|---|
| 56 | toNamespacedPath,
|
|---|
| 57 | } = require('path')
|
|---|
| 58 | const { fileURLToPath } = require('url')
|
|---|
| 59 |
|
|---|
| 60 | const defaultOptions = {
|
|---|
| 61 | dereference: false,
|
|---|
| 62 | errorOnExist: false,
|
|---|
| 63 | filter: undefined,
|
|---|
| 64 | force: true,
|
|---|
| 65 | preserveTimestamps: false,
|
|---|
| 66 | recursive: false,
|
|---|
| 67 | }
|
|---|
| 68 |
|
|---|
| 69 | async 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 |
|
|---|
| 79 | function getValidatedPath (fileURLOrPath) {
|
|---|
| 80 | const path = fileURLOrPath != null && fileURLOrPath.href
|
|---|
| 81 | && fileURLOrPath.origin
|
|---|
| 82 | ? fileURLToPath(fileURLOrPath)
|
|---|
| 83 | : fileURLOrPath
|
|---|
| 84 | return path
|
|---|
| 85 | }
|
|---|
| 86 |
|
|---|
| 87 | async 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 |
|
|---|
| 104 | async 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 |
|
|---|
| 146 | function areIdentical (srcStat, destStat) {
|
|---|
| 147 | return destStat.ino && destStat.dev && destStat.ino === srcStat.ino &&
|
|---|
| 148 | destStat.dev === srcStat.dev
|
|---|
| 149 | }
|
|---|
| 150 |
|
|---|
| 151 | function 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 |
|
|---|
| 168 | async 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 |
|
|---|
| 178 | function 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.
|
|---|
| 189 | async 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 |
|
|---|
| 217 | const 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.
|
|---|
| 222 | function isSrcSubdir (src, dest) {
|
|---|
| 223 | const srcArr = normalizePathToArray(src)
|
|---|
| 224 | const destArr = normalizePathToArray(dest)
|
|---|
| 225 | return srcArr.every((cur, i) => destArr[i] === cur)
|
|---|
| 226 | }
|
|---|
| 227 |
|
|---|
| 228 | async 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 |
|
|---|
| 235 | function 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 |
|
|---|
| 242 | async 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 |
|
|---|
| 285 | function 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 |
|
|---|
| 292 | async 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 |
|
|---|
| 306 | async 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 |
|
|---|
| 314 | async 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 |
|
|---|
| 325 | function fileIsNotWritable (srcMode) {
|
|---|
| 326 | return (srcMode & 0o200) === 0
|
|---|
| 327 | }
|
|---|
| 328 |
|
|---|
| 329 | function makeFileWritable (dest, srcMode) {
|
|---|
| 330 | return setDestMode(dest, srcMode | 0o200)
|
|---|
| 331 | }
|
|---|
| 332 |
|
|---|
| 333 | async function setDestTimestampsAndMode (srcMode, src, dest) {
|
|---|
| 334 | await setDestTimestamps(src, dest)
|
|---|
| 335 | return setDestMode(dest, srcMode)
|
|---|
| 336 | }
|
|---|
| 337 |
|
|---|
| 338 | function setDestMode (dest, srcMode) {
|
|---|
| 339 | return chmod(dest, srcMode)
|
|---|
| 340 | }
|
|---|
| 341 |
|
|---|
| 342 | async 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 |
|
|---|
| 350 | function 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 |
|
|---|
| 357 | async function mkDirAndCopy (srcMode, src, dest, opts) {
|
|---|
| 358 | await mkdir(dest)
|
|---|
| 359 | await copyDir(src, dest, opts)
|
|---|
| 360 | return setDestMode(dest, srcMode)
|
|---|
| 361 | }
|
|---|
| 362 |
|
|---|
| 363 | async 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 |
|
|---|
| 374 | async 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 |
|
|---|
| 423 | async function copyLink (resolvedSrc, dest) {
|
|---|
| 424 | await unlink(dest)
|
|---|
| 425 | return symlink(resolvedSrc, dest)
|
|---|
| 426 | }
|
|---|
| 427 |
|
|---|
| 428 | module.exports = cp
|
|---|