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