Skip to content

Commit 31286c9

Browse files
committed
fs: improve cpSync performance
1 parent 8e33f20 commit 31286c9

File tree

5 files changed

+237
-1
lines changed

5 files changed

+237
-1
lines changed

benchmark/fs/bench-cpSync.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const fs = require('fs');
5+
const path = require('path');
6+
const tmpdir = require('../../test/common/tmpdir');
7+
tmpdir.refresh();
8+
9+
const bench = common.createBenchmark(main, {
10+
n: [1, 100, 10_000],
11+
});
12+
13+
function main({ n }) {
14+
tmpdir.refresh();
15+
const options = { force: true, recursive: true };
16+
const src = path.join(__dirname, '../../test/fixtures/copy');
17+
const dest = tmpdir.resolve(`${process.pid}/subdir/cp-bench-${process.pid}`);
18+
bench.start();
19+
for (let i = 0; i < n; i++) {
20+
fs.cpSync(src, dest, options);
21+
}
22+
bench.end(n);
23+
}

lib/internal/fs/cp/cp-sync.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,40 @@ const {
4646
resolve,
4747
} = require('path');
4848
const { isPromise } = require('util/types');
49+
const internalFsBinding = internalBinding('fs');
4950

51+
/**
52+
*
53+
* @param {string} src
54+
* @param {string} dest
55+
* @param {{
56+
* preserveTimestamps?: boolean,
57+
* filter?: (src: string, dest: string) => boolean,
58+
* dereference?: boolean,
59+
* errorOnExist?: boolean,
60+
* force?: boolean,
61+
* recursive?: boolean,
62+
* mode?: number
63+
* verbatimSymlinks?: boolean
64+
* }} opts
65+
*/
5066
function cpSyncFn(src, dest, opts) {
67+
// Calling a JavaScript function from C++ is costly. Therefore, we don't support it.
68+
// TODO(@anonrig): Support `mode` option.
69+
if (opts.filter == null && (opts.mode == null || opts.mode === 0)) {
70+
return internalFsBinding.cpSync(
71+
src,
72+
dest,
73+
opts.preserveTimestamps,
74+
opts.dereference,
75+
opts.errorOnExist,
76+
opts.force,
77+
opts.recursive,
78+
opts.verbatimSymlinks,
79+
);
80+
}
81+
82+
5183
// Warn about using preserveTimestamps on 32-bit node
5284
if (opts.preserveTimestamps && process.arch === 'ia32') {
5385
const warning = 'Using the preserveTimestamps option in 32-bit ' +

src/node_errors.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
7070
V(ERR_DLOPEN_FAILED, Error) \
7171
V(ERR_ENCODING_INVALID_ENCODED_DATA, TypeError) \
7272
V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, Error) \
73+
V(ERR_FS_CP_EINVAL, Error) \
74+
V(ERR_FS_CP_DIR_TO_NON_DIR, Error) \
75+
V(ERR_FS_CP_FIFO_PIPE, Error) \
76+
V(ERR_FS_CP_EEXIST, Error) \
77+
V(ERR_FS_CP_NON_DIR_TO_DIR, Error) \
78+
V(ERR_FS_CP_SOCKET, Error) \
79+
V(ERR_FS_CP_UNKNOWN, Error) \
80+
V(ERR_FS_EISDIR, Error) \
7381
V(ERR_ILLEGAL_CONSTRUCTOR, Error) \
7482
V(ERR_INVALID_ADDRESS, Error) \
7583
V(ERR_INVALID_ARG_VALUE, TypeError) \

src/node_file.cc

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2106,6 +2106,165 @@ static void OpenFileHandle(const FunctionCallbackInfo<Value>& args) {
21062106
}
21072107
}
21082108

2109+
// TODO(@anonrig): Implement v8 fast APi calls for `cpSync`.
2110+
static void CpSync(const FunctionCallbackInfo<Value>& args) {
2111+
Environment* env = Environment::GetCurrent(args);
2112+
CHECK(args.Length() ==
2113+
8); // src, dest, preserveTimestamps, dereference, errorOnExist, force,
2114+
// recursive, verbatimSymlinks
2115+
BufferValue src(env->isolate(), args[0]);
2116+
CHECK_NOT_NULL(*src);
2117+
ToNamespacedPath(env, &src);
2118+
2119+
BufferValue dest(env->isolate(), args[1]);
2120+
CHECK_NOT_NULL(*dest);
2121+
ToNamespacedPath(env, &dest);
2122+
2123+
bool preserveTimestamps = args[2]->IsTrue();
2124+
bool dereference = args[3]->IsTrue();
2125+
bool errorOnExist = args[4]->IsTrue();
2126+
bool force = args[5]->IsTrue();
2127+
bool recursive = args[6]->IsTrue();
2128+
bool verbatimSymlinks = args[7]->IsTrue();
2129+
2130+
using copy_options = std::filesystem::copy_options;
2131+
using file_type = std::filesystem::file_type;
2132+
2133+
std::error_code error_code{};
2134+
copy_options options = copy_options ::skip_existing;
2135+
2136+
// When true timestamps from src will be preserved.
2137+
if (preserveTimestamps) options |= copy_options::create_hard_links;
2138+
// Dereference symbolic links.
2139+
if (dereference) options |= copy_options::copy_symlinks;
2140+
// Overwrite existing file or directory.
2141+
if (force) options |= copy_options::overwrite_existing;
2142+
// Copy directories recursively.
2143+
if (recursive) options |= copy_options::recursive;
2144+
// When true, path resolution for symlinks will be skipped.
2145+
if (verbatimSymlinks) options |= copy_options::skip_symlinks;
2146+
2147+
auto src_path = std::filesystem::path(src.ToStringView());
2148+
auto dest_path = std::filesystem::path(dest.ToStringView());
2149+
2150+
auto resolved_src = src_path.lexically_normal();
2151+
auto resolved_dest = dest_path.lexically_normal();
2152+
2153+
if (resolved_src == resolved_dest) {
2154+
std::string message =
2155+
"src and dest cannot be the same " + resolved_src.string();
2156+
return THROW_ERR_FS_CP_EINVAL(env, message.c_str());
2157+
}
2158+
2159+
auto get_stat = [](const std::filesystem::path& path)
2160+
-> std::optional<std::filesystem::file_status> {
2161+
std::error_code error_code{};
2162+
auto file_status = std::filesystem::status(path, error_code);
2163+
if (error_code) {
2164+
return std::nullopt;
2165+
}
2166+
return file_status;
2167+
};
2168+
2169+
auto src_type = get_stat(src_path);
2170+
auto dest_type = get_stat(dest_path);
2171+
2172+
if (!src_type.has_value()) {
2173+
std::string message = "Src path " + src_path.string() + " does not exist";
2174+
return THROW_ERR_FS_CP_EINVAL(env, message.c_str());
2175+
}
2176+
2177+
const bool src_is_dir = src_type->type() == file_type::directory;
2178+
2179+
if (dest_type.has_value()) {
2180+
// Check if src and dest are identical.
2181+
if (std::filesystem::equivalent(src_path, dest_path)) {
2182+
std::string message =
2183+
"src and dest cannot be the same " + dest_path.string();
2184+
return THROW_ERR_FS_CP_EINVAL(env, message.c_str());
2185+
}
2186+
2187+
const bool dest_is_dir = dest_type->type() == file_type::directory;
2188+
2189+
if (src_is_dir && !dest_is_dir) {
2190+
std::string message = "Cannot overwrite non-directory " +
2191+
src_path.string() + " with directory " +
2192+
dest_path.string();
2193+
return THROW_ERR_FS_CP_DIR_TO_NON_DIR(env, message.c_str());
2194+
}
2195+
2196+
if (!src_is_dir && dest_is_dir) {
2197+
std::string message = "Cannot overwrite directory " + dest_path.string() +
2198+
" with non-directory " + src_path.string();
2199+
return THROW_ERR_FS_CP_NON_DIR_TO_DIR(env, message.c_str());
2200+
}
2201+
}
2202+
2203+
if (src_is_dir && dest_path.string().starts_with(src_path.string())) {
2204+
std::string message = "Cannot copy " + src_path.string() +
2205+
" to a subdirectory of self " + dest_path.string();
2206+
return THROW_ERR_FS_CP_EINVAL(env, message.c_str());
2207+
}
2208+
2209+
auto dest_parent = dest_path.parent_path();
2210+
// "/" parent is itself. Therefore, we need to check if the parent is the same
2211+
// as itself.
2212+
while (src_path.parent_path() != dest_parent &&
2213+
dest_parent.has_parent_path() &&
2214+
dest_parent.parent_path() != dest_parent) {
2215+
if (std::filesystem::equivalent(
2216+
src_path, dest_path.parent_path(), error_code)) {
2217+
std::string message = "Cannot copy " + src_path.string() +
2218+
" to a subdirectory of self " + dest_path.string();
2219+
return THROW_ERR_FS_CP_EINVAL(env, message.c_str());
2220+
}
2221+
2222+
// If equivalent fails, it's highly likely that dest_parent does not exist
2223+
if (error_code) {
2224+
break;
2225+
}
2226+
2227+
dest_parent = dest_parent.parent_path();
2228+
}
2229+
2230+
if (src_is_dir && !recursive) {
2231+
std::string message = src_path.string() + " is a directory (not copied)";
2232+
return THROW_ERR_FS_EISDIR(env, message.c_str());
2233+
}
2234+
2235+
switch (src_type->type()) {
2236+
case file_type::socket: {
2237+
std::string message = "Cannot copy a socket file: " + dest_path.string();
2238+
return THROW_ERR_FS_CP_SOCKET(env, message.c_str());
2239+
}
2240+
case file_type::fifo: {
2241+
std::string message = "Cannot copy a FIFO pipe: " + dest_path.string();
2242+
return THROW_ERR_FS_CP_FIFO_PIPE(env, message.c_str());
2243+
}
2244+
case file_type::unknown: {
2245+
std::string message =
2246+
"Cannot copy an unknown file type: " + dest_path.string();
2247+
return THROW_ERR_FS_CP_UNKNOWN(env, message.c_str());
2248+
}
2249+
default:
2250+
break;
2251+
}
2252+
2253+
if (dest_type.has_value() && errorOnExist) {
2254+
std::string message = dest_path.string() + " already exists";
2255+
return THROW_ERR_FS_CP_EEXIST(env, message.c_str());
2256+
}
2257+
2258+
std::filesystem::create_directories(dest_path, error_code);
2259+
std::filesystem::copy(src_path, dest_path, options, error_code);
2260+
if (error_code) {
2261+
std::string message = "Unhandled error " +
2262+
std::to_string(error_code.value()) + ": " +
2263+
error_code.message();
2264+
return THROW_ERR_FS_CP_EINVAL(env, message.c_str());
2265+
}
2266+
}
2267+
21092268
static void CopyFile(const FunctionCallbackInfo<Value>& args) {
21102269
Environment* env = Environment::GetCurrent(args);
21112270
Isolate* isolate = env->isolate();
@@ -3344,6 +3503,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
33443503
SetMethod(isolate, target, "writeFileUtf8", WriteFileUtf8);
33453504
SetMethod(isolate, target, "realpath", RealPath);
33463505
SetMethod(isolate, target, "copyFile", CopyFile);
3506+
SetMethod(isolate, target, "cpSync", CpSync);
33473507

33483508
SetMethod(isolate, target, "chmod", Chmod);
33493509
SetMethod(isolate, target, "fchmod", FChmod);
@@ -3466,6 +3626,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
34663626
registry->Register(WriteFileUtf8);
34673627
registry->Register(RealPath);
34683628
registry->Register(CopyFile);
3629+
registry->Register(CpSync);
34693630

34703631
registry->Register(Chmod);
34713632
registry->Register(FChmod);

typings/internalBinding/fs.d.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,20 @@ declare namespace InternalFSBinding {
7373
function close(fd: number, req: undefined, ctx: FSSyncContext): void;
7474

7575
function copyFile(src: StringOrBuffer, dest: StringOrBuffer, mode: number, req: FSReqCallback): void;
76-
function copyFile(src: StringOrBuffer, dest: StringOrBuffer, mode: number, req: undefined, ctx: FSSyncContext): void;
76+
function copyFile(src: StringOrBuffer, dest: StringOrBuffer, mode: number): void;
7777
function copyFile(src: StringOrBuffer, dest: StringOrBuffer, mode: number, usePromises: typeof kUsePromises): Promise<void>;
7878

79+
function cpSync(
80+
src: StringOrBuffer,
81+
dest: StringOrBuffer,
82+
preserveTimestamps: boolean,
83+
dereference?: boolean,
84+
errorOnExist?: boolean,
85+
force?: boolean,
86+
recursive?: boolean,
87+
verbatimSymlinks?: boolean,
88+
): void;
89+
7990
function fchmod(fd: number, mode: number, req: FSReqCallback): void;
8091
function fchmod(fd: number, mode: number): void;
8192
function fchmod(fd: number, mode: number, usePromises: typeof kUsePromises): Promise<void>;
@@ -253,6 +264,7 @@ export interface FsBinding {
253264
chown: typeof InternalFSBinding.chown;
254265
close: typeof InternalFSBinding.close;
255266
copyFile: typeof InternalFSBinding.copyFile;
267+
cpSync: typeof InternalFSBinding.cpSync;
256268
fchmod: typeof InternalFSBinding.fchmod;
257269
fchown: typeof InternalFSBinding.fchown;
258270
fdatasync: typeof InternalFSBinding.fdatasync;

0 commit comments

Comments
 (0)