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+©_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+33933612BindingData::FilePathIsFileReturnType BindingData::FilePathIsFile(
33943613 Environment* env, const std::string& file_path) {
33953614THROW_IF_INSUFFICIENT_PERMISSIONS(
@@ -3726,6 +3945,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
3726394537273946SetMethod(isolate, target, "cpSyncCheckPaths", CpSyncCheckPaths);
37283947SetMethod(isolate, target, "cpSyncOverrideFile", CpSyncOverrideFile);
3948+SetMethod(isolate, target, "cpSyncCopyDir", CpSyncCopyDir);
3729394937303950StatWatcher::CreatePerIsolateProperties(isolate_data, target);
37313951BindingData::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);