Skip to content

Commit

Permalink
fs: optimize fs.cpSync js calls
Browse files Browse the repository at this point in the history
  • Loading branch information
anonrig committed Jul 21, 2024
1 parent 1fb23f1 commit e2c27ae
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 133 deletions.
164 changes: 31 additions & 133 deletions lib/internal/fs/cp/cp-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,25 @@

// This file is a modified version of the fs-extra's copySync method.

const { areIdentical, isSrcSubdir } = require('internal/fs/cp/cp');
const fsBinding = internalBinding('fs');
const { isSrcSubdir } = require('internal/fs/cp/cp');
const { codes: {
ERR_FS_CP_DIR_TO_NON_DIR,
ERR_FS_CP_EEXIST,
ERR_FS_CP_EINVAL,
ERR_FS_CP_FIFO_PIPE,
ERR_FS_CP_NON_DIR_TO_DIR,
ERR_FS_CP_SOCKET,
ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY,
ERR_FS_CP_UNKNOWN,
ERR_FS_EISDIR,
ERR_INVALID_RETURN_VALUE,
} } = require('internal/errors');
const {
os: {
errno: {
EEXIST,
EISDIR,
EINVAL,
ENOTDIR,
},
},
} = internalBinding('constants');
const {
chmodSync,
copyFileSync,
existsSync,
lstatSync,
mkdirSync,
opendirSync,
Expand All @@ -42,7 +34,6 @@ const {
dirname,
isAbsolute,
join,
parse,
resolve,
} = require('path');
const { isPromise } = require('util/types');
Expand All @@ -54,152 +45,46 @@ function cpSyncFn(src, dest, opts) {
'node is not recommended';
process.emitWarning(warning, 'TimestampPrecisionWarning');
}
const { srcStat, destStat, skipped } = checkPathsSync(src, dest, opts);
if (skipped) return;
checkParentPathsSync(src, srcStat, dest);
return checkParentDir(destStat, src, dest, opts);
}

function checkPathsSync(src, dest, opts) {
if (opts.filter) {
const shouldCopy = opts.filter(src, dest);
if (isPromise(shouldCopy)) {
throw new ERR_INVALID_RETURN_VALUE('boolean', 'filter', shouldCopy);
}
if (!shouldCopy) return { __proto__: null, skipped: true };
if (!shouldCopy) return;
}
const { srcStat, destStat } = getStatsSync(src, dest, opts);

if (destStat) {
if (areIdentical(srcStat, destStat)) {
throw new ERR_FS_CP_EINVAL({
message: 'src and dest cannot be the same',
path: dest,
syscall: 'cp',
errno: EINVAL,
code: 'EINVAL',
});
}
if (srcStat.isDirectory() && !destStat.isDirectory()) {
throw new ERR_FS_CP_DIR_TO_NON_DIR({
message: `cannot overwrite non-directory ${dest} ` +
`with directory ${src}`,
path: dest,
syscall: 'cp',
errno: EISDIR,
code: 'EISDIR',
});
}
if (!srcStat.isDirectory() && destStat.isDirectory()) {
throw new ERR_FS_CP_NON_DIR_TO_DIR({
message: `cannot overwrite directory ${dest} ` +
`with non-directory ${src}`,
path: dest,
syscall: 'cp',
errno: ENOTDIR,
code: 'ENOTDIR',
});
}
}

if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
throw new ERR_FS_CP_EINVAL({
message: `cannot copy ${src} to a subdirectory of self ${dest}`,
path: dest,
syscall: 'cp',
errno: EINVAL,
code: 'EINVAL',
});
}
return { __proto__: null, srcStat, destStat, skipped: false };
}
fsBinding.cpSyncCheckPaths(src, dest, opts.dereference, opts.recursive);

function getStatsSync(src, dest, opts) {
const statFunc = opts.dereference ? statSync : lstatSync;
const srcStat = statFunc(src, { bigint: true, throwIfNoEntry: true });
const destStat = statFunc(dest, { bigint: true, throwIfNoEntry: false });
return { srcStat, destStat };
return getStats(src, dest, opts);
}

function checkParentPathsSync(src, srcStat, dest) {
const srcParent = resolve(dirname(src));
const destParent = resolve(dirname(dest));
if (destParent === srcParent || destParent === parse(destParent).root) return;
const destStat = statSync(destParent, { bigint: true, throwIfNoEntry: false });

if (destStat === undefined) {
return;
}

if (areIdentical(srcStat, destStat)) {
throw new ERR_FS_CP_EINVAL({
message: `cannot copy ${src} to a subdirectory of self ${dest}`,
path: dest,
syscall: 'cp',
errno: EINVAL,
code: 'EINVAL',
});
}
return checkParentPathsSync(src, srcStat, destParent);
}

function checkParentDir(destStat, src, dest, opts) {
const destParent = dirname(dest);
if (!existsSync(destParent)) mkdirSync(destParent, { recursive: true });
return getStats(destStat, src, dest, opts);
}

function getStats(destStat, src, dest, opts) {
function getStats(src, dest, opts) {
// TODO(@anonrig): Avoid making two stat calls.
const statSyncFn = opts.dereference ? statSync : lstatSync;
const srcStat = statSyncFn(src);
const destStat = statSyncFn(dest, { bigint: true, throwIfNoEntry: false });

if (srcStat.isDirectory() && opts.recursive) {
return onDir(srcStat, destStat, src, dest, opts);
} else if (srcStat.isDirectory()) {
throw new ERR_FS_EISDIR({
message: `${src} is a directory (not copied)`,
path: src,
syscall: 'cp',
errno: EINVAL,
code: 'EISDIR',
});
} else if (srcStat.isFile() ||
srcStat.isCharacterDevice() ||
srcStat.isBlockDevice()) {
return onFile(srcStat, destStat, src, dest, opts);
} else if (srcStat.isSymbolicLink()) {
return onLink(destStat, src, dest, opts);
} else if (srcStat.isSocket()) {
throw new ERR_FS_CP_SOCKET({
message: `cannot copy a socket file: ${dest}`,
path: dest,
syscall: 'cp',
errno: EINVAL,
code: 'EINVAL',
});
} else if (srcStat.isFIFO()) {
throw new ERR_FS_CP_FIFO_PIPE({
message: `cannot copy a FIFO pipe: ${dest}`,
path: dest,
syscall: 'cp',
errno: EINVAL,
code: 'EINVAL',
});
return onLink(destStat, src, dest, opts.verbatimSymlinks);
}
throw new ERR_FS_CP_UNKNOWN({
message: `cannot copy an unknown file type: ${dest}`,
path: dest,
syscall: 'cp',
errno: EINVAL,
code: 'EINVAL',
});

// It is not possible to get here because all possible cases are handled above.
const assert = require('internal/assert');
assert.fail('Unreachable code');
}

function onFile(srcStat, destStat, src, dest, opts) {
if (!destStat) return copyFile(srcStat, src, dest, opts);
return mayCopyFile(srcStat, src, dest, opts);
}

// TODO(@anonrig): Move this function to C++.
function mayCopyFile(srcStat, src, dest, opts) {
if (opts.force) {
unlinkSync(dest);
Expand Down Expand Up @@ -249,6 +134,7 @@ function setDestTimestamps(src, dest) {
return utimesSync(dest, updatedSrcStat.atime, updatedSrcStat.mtime);
}

// TODO(@anonrig): Move this function to C++.
function onDir(srcStat, destStat, src, dest, opts) {
if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts);
return copyDir(src, dest, opts);
Expand All @@ -260,6 +146,7 @@ function mkDirAndCopy(srcMode, src, dest, opts) {
return setDestMode(dest, srcMode);
}

// TODO(@anonrig): Move this function to C++.
function copyDir(src, dest, opts) {
const dir = opendirSync(src);

Expand All @@ -270,17 +157,28 @@ function copyDir(src, dest, opts) {
const { name } = dirent;
const srcItem = join(src, name);
const destItem = join(dest, name);
const { destStat, skipped } = checkPathsSync(srcItem, destItem, opts);
if (!skipped) getStats(destStat, srcItem, destItem, opts);
let shouldCopy = true;

if (opts.filter) {
shouldCopy = opts.filter(srcItem, destItem);
if (isPromise(shouldCopy)) {
throw new ERR_INVALID_RETURN_VALUE('boolean', 'filter', shouldCopy);
}
}

if (shouldCopy) {
getStats(srcItem, destItem, opts);
}
}
} finally {
dir.closeSync();
}
}

function onLink(destStat, src, dest, opts) {
// TODO(@anonrig): Move this function to C++.
function onLink(destStat, src, dest, verbatimSymlinks) {
let resolvedSrc = readlinkSync(src);
if (!opts.verbatimSymlinks && !isAbsolute(resolvedSrc)) {
if (!verbatimSymlinks && !isAbsolute(resolvedSrc)) {
resolvedSrc = resolve(dirname(src), resolvedSrc);
}
if (!destStat) {
Expand Down
6 changes: 6 additions & 0 deletions src/node_errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,13 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
V(ERR_DLOPEN_FAILED, Error) \
V(ERR_ENCODING_INVALID_ENCODED_DATA, TypeError) \
V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, Error) \
V(ERR_FS_CP_EINVAL, Error) \
V(ERR_FS_CP_DIR_TO_NON_DIR, Error) \
V(ERR_FS_CP_NON_DIR_TO_DIR, Error) \
V(ERR_FS_EISDIR, Error) \
V(ERR_FS_CP_SOCKET, Error) \
V(ERR_FS_CP_FIFO_PIPE, Error) \
V(ERR_FS_CP_UNKNOWN, Error) \
V(ERR_ILLEGAL_CONSTRUCTOR, Error) \
V(ERR_INVALID_ADDRESS, Error) \
V(ERR_INVALID_ARG_VALUE, TypeError) \
Expand Down
Loading

0 comments on commit e2c27ae

Please sign in to comment.