• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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