fs: improve cpSync no-filter copyDir performance · nodejs/node@fcef56c

@@ -43,6 +43,8 @@

4343

#include "uv.h"

4444

#include "v8-fast-api-calls.h"

454546+

#include <errno.h>

47+

#include <cerrno>

4648

#include <cstdio>

4749

#include <filesystem>

4850

@@ -3390,6 +3392,223 @@ static void CpSyncOverrideFile(const FunctionCallbackInfo<Value>& args) {

33903392

}

33913393

}

339233943395+

std::vector<std::string> normalizePathToArray(

3396+

const std::filesystem::path& path) {

3397+

std::vector<std::string> parts;

3398+

std::filesystem::path absPath = std::filesystem::absolute(path);

3399+

for (const auto& part : absPath) {

3400+

if (!part.empty()) parts.push_back(part.string());

3401+

}

3402+

return parts;

3403+

}

3404+3405+

bool isInsideDir(const std::filesystem::path& src,

3406+

const std::filesystem::path& dest) {

3407+

auto srcArr = normalizePathToArray(src);

3408+

auto destArr = normalizePathToArray(dest);

3409+

if (srcArr.size() > destArr.size()) return false;

3410+

return std::equal(srcArr.begin(), srcArr.end(), destArr.begin());

3411+

}

3412+3413+

static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {

3414+

CHECK_EQ(args.Length(), 7); // src, dest, force, dereference, errorOnExist,

3415+

// verbatimSymlinks, preserveTimestamps

3416+3417+

Environment* env = Environment::GetCurrent(args);

3418+

Isolate* isolate = env->isolate();

3419+3420+

BufferValue src(isolate, args[0]);

3421+

CHECK_NOT_NULL(*src);

3422+

ToNamespacedPath(env, &src);

3423+3424+

BufferValue dest(isolate, args[1]);

3425+

CHECK_NOT_NULL(*dest);

3426+

ToNamespacedPath(env, &dest);

3427+3428+

bool force = args[2]->IsTrue();

3429+

bool dereference = args[3]->IsTrue();

3430+

bool error_on_exist = args[4]->IsTrue();

3431+

bool verbatim_symlinks = args[5]->IsTrue();

3432+

bool preserve_timestamps = args[6]->IsTrue();

3433+3434+

std::error_code error;

3435+

std::filesystem::create_directories(*dest, error);

3436+

if (error) {

3437+

return env->ThrowStdErrException(error, "cp", *dest);

3438+

}

3439+3440+

auto file_copy_opts = std::filesystem::copy_options::recursive;

3441+

if (force) {

3442+

file_copy_opts |= std::filesystem::copy_options::overwrite_existing;

3443+

} else if (error_on_exist) {

3444+

file_copy_opts |= std::filesystem::copy_options::none;

3445+

} else {

3446+

file_copy_opts |= std::filesystem::copy_options::skip_existing;

3447+

}

3448+3449+

std::function<bool(std::filesystem::path, std::filesystem::path)>

3450+

copy_dir_contents;

3451+

copy_dir_contents = [verbatim_symlinks,

3452+

&copy_dir_contents,

3453+

&env,

3454+

file_copy_opts,

3455+

preserve_timestamps,

3456+

force,

3457+

error_on_exist,

3458+

dereference,

3459+

&isolate](std::filesystem::path src,

3460+

std::filesystem::path dest) {

3461+

std::error_code error;

3462+

for (auto dir_entry : std::filesystem::directory_iterator(src)) {

3463+

auto dest_file_path = dest / dir_entry.path().filename();

3464+

auto dest_str = PathToString(dest);

3465+3466+

if (dir_entry.is_symlink()) {

3467+

if (verbatim_symlinks) {

3468+

std::filesystem::copy_symlink(

3469+

dir_entry.path(), dest_file_path, error);

3470+

if (error) {

3471+

env->ThrowStdErrException(error, "cp", dest_str.c_str());

3472+

return false;

3473+

}

3474+

} else {

3475+

auto symlink_target =

3476+

std::filesystem::read_symlink(dir_entry.path().c_str(), error);

3477+

if (error) {

3478+

env->ThrowStdErrException(error, "cp", dest_str.c_str());

3479+

return false;

3480+

}

3481+3482+

if (std::filesystem::exists(dest_file_path)) {

3483+

if (std::filesystem::is_symlink((dest_file_path.c_str()))) {

3484+

auto current_dest_symlink_target =

3485+

std::filesystem::read_symlink(dest_file_path.c_str(), error);

3486+

if (error) {

3487+

env->ThrowStdErrException(error, "cp", dest_str.c_str());

3488+

return false;

3489+

}

3490+3491+

if (!dereference &&

3492+

std::filesystem::is_directory(symlink_target) &&

3493+

isInsideDir(symlink_target, current_dest_symlink_target)) {

3494+

std::string message =

3495+

"Cannot copy %s to a subdirectory of self %s";

3496+

THROW_ERR_FS_CP_EINVAL(env,

3497+

message.c_str(),

3498+

symlink_target.c_str(),

3499+

current_dest_symlink_target.c_str());

3500+

return false;

3501+

}

3502+3503+

// Prevent copy if src is a subdir of dest since unlinking

3504+

// dest in this case would result in removing src contents

3505+

// and therefore a broken symlink would be created.

3506+

if (std::filesystem::is_directory(dest_file_path) &&

3507+

isInsideDir(current_dest_symlink_target, symlink_target)) {

3508+

std::string message = "cannot overwrite %s with %s";

3509+

THROW_ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY(

3510+

env,

3511+

message.c_str(),

3512+

current_dest_symlink_target.c_str(),

3513+

symlink_target.c_str());

3514+

return false;

3515+

}

3516+3517+

// symlinks get overridden by cp even if force: false, this is

3518+

// being applied here for backward compatibility, but is it

3519+

// correct? or is it a bug?

3520+

std::filesystem::remove(dest_file_path, error);

3521+

if (error) {

3522+

env->ThrowStdErrException(error, "cp", dest_str.c_str());

3523+

return false;

3524+

}

3525+

} else if (std::filesystem::is_regular_file(dest_file_path)) {

3526+

if (!dereference || (!force && error_on_exist)) {

3527+

auto dest_file_path_str = PathToString(dest_file_path);

3528+

env->ThrowStdErrException(

3529+

std::make_error_code(std::errc::file_exists),

3530+

"cp",

3531+

dest_file_path_str.c_str());

3532+

return false;

3533+

}

3534+

}

3535+

}

3536+

auto symlink_target_absolute = std::filesystem::weakly_canonical(

3537+

std::filesystem::absolute(src / symlink_target));

3538+

if (dir_entry.is_directory()) {

3539+

std::filesystem::create_directory_symlink(

3540+

symlink_target_absolute, dest_file_path, error);

3541+

} else {

3542+

std::filesystem::create_symlink(

3543+

symlink_target_absolute, dest_file_path, error);

3544+

}

3545+

if (error) {

3546+

env->ThrowStdErrException(error, "cp", dest_str.c_str());

3547+

return false;

3548+

}

3549+

}

3550+

} else if (dir_entry.is_directory()) {

3551+

auto entry_dir_path = src / dir_entry.path().filename();

3552+

std::filesystem::create_directory(dest_file_path);

3553+

auto success = copy_dir_contents(entry_dir_path, dest_file_path);

3554+

if (!success) {

3555+

return false;

3556+

}

3557+

} else if (dir_entry.is_regular_file()) {

3558+

std::filesystem::copy_file(

3559+

dir_entry.path(), dest_file_path, file_copy_opts, error);

3560+

if (error) {

3561+

if (error.value() == EEXIST) {

3562+

THROW_ERR_FS_CP_EEXIST(isolate,

3563+

"[ERR_FS_CP_EEXIST]: Target already exists: "

3564+

"cp returned EEXIST (%s already exists)",

3565+

dest_file_path.c_str());

3566+

return false;

3567+

}

3568+

env->ThrowStdErrException(error, "cp", dest_str.c_str());

3569+

return false;

3570+

}

3571+3572+

if (preserve_timestamps) {

3573+

uv_fs_t req;

3574+

auto cleanup = OnScopeLeave([&req]() { uv_fs_req_cleanup(&req); });

3575+3576+

auto dir_entry_path_str = PathToString(dir_entry.path());

3577+

int result =

3578+

uv_fs_stat(nullptr, &req, dir_entry_path_str.c_str(), nullptr);

3579+

if (is_uv_error(result)) {

3580+

env->ThrowUVException(

3581+

result, "stat", nullptr, dir_entry_path_str.c_str());

3582+

return false;

3583+

}

3584+3585+

const uv_stat_t* const s = static_cast<const uv_stat_t*>(req.ptr);

3586+

const double source_atime =

3587+

s->st_atim.tv_sec + s->st_atim.tv_nsec / 1e9;

3588+

const double source_mtime =

3589+

s->st_mtim.tv_sec + s->st_mtim.tv_nsec / 1e9;

3590+3591+

auto dest_file_path_str = PathToString(dest_file_path);

3592+

int utime_result = uv_fs_utime(nullptr,

3593+

&req,

3594+

dest_file_path_str.c_str(),

3595+

source_atime,

3596+

source_mtime,

3597+

nullptr);

3598+

if (is_uv_error(utime_result)) {

3599+

env->ThrowUVException(

3600+

utime_result, "utime", nullptr, dest_file_path_str.c_str());

3601+

return false;

3602+

}

3603+

}

3604+

}

3605+

}

3606+

return true;

3607+

};

3608+3609+

copy_dir_contents(std::filesystem::path(*src), std::filesystem::path(*dest));

3610+

}

3611+33933612

BindingData::FilePathIsFileReturnType BindingData::FilePathIsFile(

33943613

Environment* env, const std::string& file_path) {

33953614

THROW_IF_INSUFFICIENT_PERMISSIONS(

@@ -3726,6 +3945,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,

3726394537273946

SetMethod(isolate, target, "cpSyncCheckPaths", CpSyncCheckPaths);

37283947

SetMethod(isolate, target, "cpSyncOverrideFile", CpSyncOverrideFile);

3948+

SetMethod(isolate, target, "cpSyncCopyDir", CpSyncCopyDir);

3729394937303950

StatWatcher::CreatePerIsolateProperties(isolate_data, target);

37313951

BindingData::CreatePerIsolateProperties(isolate_data, target);

@@ -3837,6 +4057,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {

3837405738384058

registry->Register(CpSyncCheckPaths);

38394059

registry->Register(CpSyncOverrideFile);

4060+

registry->Register(CpSyncCopyDir);

3840406138414062

registry->Register(Chmod);

38424063

registry->Register(FChmod);