summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock47
-rw-r--r--cli/standalone/file_system.rs11
-rw-r--r--ext/fs/Cargo.toml1
-rw-r--r--ext/fs/interface.rs3
-rw-r--r--ext/fs/std_fs.rs158
-rw-r--r--ext/node/lib.rs15
-rw-r--r--ext/node/ops/fs.rs54
-rw-r--r--ext/node/polyfills/_fs/_fs_cp.js41
-rw-r--r--ext/node/polyfills/fs.ts6
-rw-r--r--runtime/permissions/mod.rs9
-rw-r--r--runtime/snapshot.rs7
11 files changed, 350 insertions, 2 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 70569fb40..f4f6c0060 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -758,6 +758,28 @@ dependencies = [
]
[[package]]
+name = "crossbeam-deque"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751"
+dependencies = [
+ "cfg-if",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "crossbeam-utils",
+]
+
+[[package]]
name = "crossbeam-queue"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -769,9 +791,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
-version = "0.8.16"
+version = "0.8.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
+checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c"
dependencies = [
"cfg-if",
]
@@ -1298,6 +1320,7 @@ dependencies = [
"log",
"nix 0.26.2",
"rand",
+ "rayon",
"serde",
"tokio",
"winapi",
@@ -4669,6 +4692,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
[[package]]
+name = "rayon"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/cli/standalone/file_system.rs b/cli/standalone/file_system.rs
index 69e95a97f..f1ea570b5 100644
--- a/cli/standalone/file_system.rs
+++ b/cli/standalone/file_system.rs
@@ -175,6 +175,17 @@ impl FileSystem for DenoCompileFileSystem {
}
}
+ fn cp_sync(&self, from: &Path, to: &Path) -> FsResult<()> {
+ self.error_if_in_vfs(to)?;
+
+ RealFs.cp_sync(from, to)
+ }
+ async fn cp_async(&self, from: PathBuf, to: PathBuf) -> FsResult<()> {
+ self.error_if_in_vfs(&to)?;
+
+ RealFs.cp_async(from, to).await
+ }
+
fn stat_sync(&self, path: &Path) -> FsResult<FsStat> {
if self.0.is_path_within(path) {
Ok(self.0.stat(path)?)
diff --git a/ext/fs/Cargo.toml b/ext/fs/Cargo.toml
index 2d5919c9d..4a10fac37 100644
--- a/ext/fs/Cargo.toml
+++ b/ext/fs/Cargo.toml
@@ -25,6 +25,7 @@ fs3.workspace = true
libc.workspace = true
log.workspace = true
rand.workspace = true
+rayon = "1.8.0"
serde.workspace = true
tokio.workspace = true
diff --git a/ext/fs/interface.rs b/ext/fs/interface.rs
index e69e80c6b..8ffa61481 100644
--- a/ext/fs/interface.rs
+++ b/ext/fs/interface.rs
@@ -131,6 +131,9 @@ pub trait FileSystem: std::fmt::Debug + MaybeSend + MaybeSync {
newpath: PathBuf,
) -> FsResult<()>;
+ fn cp_sync(&self, path: &Path, new_path: &Path) -> FsResult<()>;
+ async fn cp_async(&self, path: PathBuf, new_path: PathBuf) -> FsResult<()>;
+
fn stat_sync(&self, path: &Path) -> FsResult<FsStat>;
async fn stat_async(&self, path: PathBuf) -> FsResult<FsStat>;
diff --git a/ext/fs/std_fs.rs b/ext/fs/std_fs.rs
index d8e2f3085..d52879394 100644
--- a/ext/fs/std_fs.rs
+++ b/ext/fs/std_fs.rs
@@ -150,6 +150,13 @@ impl FileSystem for RealFs {
spawn_blocking(move || copy_file(&from, &to)).await?
}
+ fn cp_sync(&self, fro: &Path, to: &Path) -> FsResult<()> {
+ cp(fro, to)
+ }
+ async fn cp_async(&self, fro: PathBuf, to: PathBuf) -> FsResult<()> {
+ spawn_blocking(move || cp(&fro, &to)).await?
+ }
+
fn stat_sync(&self, path: &Path) -> FsResult<FsStat> {
stat(path).map(Into::into)
}
@@ -469,6 +476,157 @@ fn copy_file(from: &Path, to: &Path) -> FsResult<()> {
Ok(())
}
+fn cp(from: &Path, to: &Path) -> FsResult<()> {
+ fn cp_(source_meta: fs::Metadata, from: &Path, to: &Path) -> FsResult<()> {
+ use rayon::prelude::IntoParallelIterator;
+ use rayon::prelude::ParallelIterator;
+
+ let ty = source_meta.file_type();
+ if ty.is_dir() {
+ #[allow(unused_mut)]
+ let mut builder = fs::DirBuilder::new();
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::DirBuilderExt;
+ use std::os::unix::fs::PermissionsExt;
+ builder.mode(fs::symlink_metadata(from)?.permissions().mode());
+ }
+ builder.create(to)?;
+
+ let mut entries: Vec<_> = fs::read_dir(from)?
+ .map(|res| res.map(|e| e.file_name()))
+ .collect::<Result<_, _>>()?;
+
+ entries.shrink_to_fit();
+ entries
+ .into_par_iter()
+ .map(|file_name| {
+ cp_(
+ fs::symlink_metadata(from.join(&file_name)).unwrap(),
+ &from.join(&file_name),
+ &to.join(&file_name),
+ )
+ .map_err(|err| {
+ io::Error::new(
+ err.kind(),
+ format!(
+ "failed to copy '{}' to '{}': {:?}",
+ from.join(&file_name).display(),
+ to.join(&file_name).display(),
+ err
+ ),
+ )
+ })
+ })
+ .collect::<Result<Vec<_>, _>>()?;
+
+ return Ok(());
+ } else if ty.is_symlink() {
+ let from = std::fs::read_link(from)?;
+
+ #[cfg(unix)]
+ std::os::unix::fs::symlink(from, to)?;
+ #[cfg(windows)]
+ std::os::windows::fs::symlink_file(from, to)?;
+
+ return Ok(());
+ }
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::FileTypeExt;
+ if ty.is_socket() {
+ return Err(
+ io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "sockets cannot be copied",
+ )
+ .into(),
+ );
+ }
+ }
+ copy_file(from, to)
+ }
+
+ #[cfg(target_os = "macos")]
+ {
+ // Just clonefile()
+ use libc::clonefile;
+ use libc::unlink;
+ use std::ffi::CString;
+ use std::os::unix::ffi::OsStrExt;
+
+ let from_str = CString::new(from.as_os_str().as_bytes()).unwrap();
+ let to_str = CString::new(to.as_os_str().as_bytes()).unwrap();
+
+ // SAFETY: `from` and `to` are valid C strings.
+ unsafe {
+ // Try unlink. If it fails, we are going to try clonefile() anyway.
+ let _ = unlink(to_str.as_ptr());
+
+ if clonefile(from_str.as_ptr(), to_str.as_ptr(), 0) == 0 {
+ return Ok(());
+ }
+ }
+ }
+
+ let source_meta = fs::symlink_metadata(from)?;
+
+ #[inline]
+ fn is_identical(
+ source_meta: &fs::Metadata,
+ dest_meta: &fs::Metadata,
+ ) -> bool {
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::MetadataExt;
+ source_meta.ino() == dest_meta.ino()
+ }
+ #[cfg(windows)]
+ {
+ use std::os::windows::fs::MetadataExt;
+ // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/ns-fileapi-by_handle_file_information
+ //
+ // The identifier (low and high parts) and the volume serial number uniquely identify a file on a single computer.
+ // To determine whether two open handles represent the same file, combine the identifier and the volume serial
+ // number for each file and compare them.
+ //
+ // Use this code once file_index() and volume_serial_number() is stabalized
+ // See: https://github.com/rust-lang/rust/issues/63010
+ //
+ // source_meta.file_index() == dest_meta.file_index()
+ // && source_meta.volume_serial_number()
+ // == dest_meta.volume_serial_number()
+ source_meta.last_write_time() == dest_meta.last_write_time()
+ && source_meta.creation_time() == dest_meta.creation_time()
+ }
+ }
+
+ match (fs::metadata(to), fs::symlink_metadata(to)) {
+ (Ok(m), _) if m.is_dir() => cp_(
+ source_meta,
+ from,
+ &to.join(from.file_name().ok_or_else(|| {
+ io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "the source path is not a valid file",
+ )
+ })?),
+ )?,
+ (_, Ok(m)) if is_identical(&source_meta, &m) => {
+ return Err(
+ io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "the source and destination are the same file",
+ )
+ .into(),
+ )
+ }
+ _ => cp_(source_meta, from, to)?,
+ }
+
+ Ok(())
+}
+
#[cfg(not(windows))]
fn stat(path: &Path) -> FsResult<FsStat> {
let metadata = fs::metadata(path)?;
diff --git a/ext/node/lib.rs b/ext/node/lib.rs
index 2aac49754..de56285fd 100644
--- a/ext/node/lib.rs
+++ b/ext/node/lib.rs
@@ -63,6 +63,11 @@ pub trait NodePermissions {
api_name: Option<&str>,
) -> Result<(), AnyError>;
fn check_sys(&self, kind: &str, api_name: &str) -> Result<(), AnyError>;
+ fn check_write_with_api_name(
+ &self,
+ path: &Path,
+ api_name: Option<&str>,
+ ) -> Result<(), AnyError>;
}
pub(crate) struct AllowAllNodePermissions;
@@ -82,6 +87,13 @@ impl NodePermissions for AllowAllNodePermissions {
) -> Result<(), AnyError> {
Ok(())
}
+ fn check_write_with_api_name(
+ &self,
+ _path: &Path,
+ _api_name: Option<&str>,
+ ) -> Result<(), AnyError> {
+ Ok(())
+ }
fn check_sys(&self, _kind: &str, _api_name: &str) -> Result<(), AnyError> {
Ok(())
}
@@ -238,6 +250,8 @@ deno_core::extension!(deno_node,
ops::crypto::x509::op_node_x509_get_serial_number,
ops::crypto::x509::op_node_x509_key_usage,
ops::fs::op_node_fs_exists_sync<P>,
+ ops::fs::op_node_cp_sync<P>,
+ ops::fs::op_node_cp<P>,
ops::winerror::op_node_sys_to_uv_error,
ops::v8::op_v8_cached_data_version_tag,
ops::v8::op_v8_get_heap_statistics,
@@ -329,6 +343,7 @@ deno_core::extension!(deno_node,
"_fs/_fs_common.ts",
"_fs/_fs_constants.ts",
"_fs/_fs_copy.ts",
+ "_fs/_fs_cp.js",
"_fs/_fs_dir.ts",
"_fs/_fs_dirent.ts",
"_fs/_fs_exists.ts",
diff --git a/ext/node/ops/fs.rs b/ext/node/ops/fs.rs
index 8e4805f6c..c5ae2371e 100644
--- a/ext/node/ops/fs.rs
+++ b/ext/node/ops/fs.rs
@@ -1,6 +1,9 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+use std::cell::RefCell;
+use std::path::Path;
use std::path::PathBuf;
+use std::rc::Rc;
use deno_core::error::AnyError;
use deno_core::op2;
@@ -24,3 +27,54 @@ where
let fs = state.borrow::<FileSystemRc>();
Ok(fs.lstat_sync(&path).is_ok())
}
+
+#[op2(fast)]
+pub fn op_node_cp_sync<P>(
+ state: &mut OpState,
+ #[string] path: &str,
+ #[string] new_path: &str,
+) -> Result<(), AnyError>
+where
+ P: NodePermissions + 'static,
+{
+ let path = Path::new(path);
+ let new_path = Path::new(new_path);
+
+ state
+ .borrow_mut::<P>()
+ .check_read_with_api_name(path, Some("node:fs.cpSync"))?;
+ state
+ .borrow_mut::<P>()
+ .check_write_with_api_name(new_path, Some("node:fs.cpSync"))?;
+
+ let fs = state.borrow::<FileSystemRc>();
+ fs.cp_sync(path, new_path)?;
+ Ok(())
+}
+
+#[op2(async)]
+pub async fn op_node_cp<P>(
+ state: Rc<RefCell<OpState>>,
+ #[string] path: String,
+ #[string] new_path: String,
+) -> Result<(), AnyError>
+where
+ P: NodePermissions + 'static,
+{
+ let path = PathBuf::from(path);
+ let new_path = PathBuf::from(new_path);
+
+ let fs = {
+ let mut state = state.borrow_mut();
+ state
+ .borrow_mut::<P>()
+ .check_read_with_api_name(&path, Some("node:fs.cpSync"))?;
+ state
+ .borrow_mut::<P>()
+ .check_write_with_api_name(&new_path, Some("node:fs.cpSync"))?;
+ state.borrow::<FileSystemRc>().clone()
+ };
+
+ fs.cp_async(path, new_path).await?;
+ Ok(())
+}
diff --git a/ext/node/polyfills/_fs/_fs_cp.js b/ext/node/polyfills/_fs/_fs_cp.js
new file mode 100644
index 000000000..dbe327974
--- /dev/null
+++ b/ext/node/polyfills/_fs/_fs_cp.js
@@ -0,0 +1,41 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+// deno-lint-ignore-file prefer-primordials
+
+import {
+ getValidatedPath,
+ validateCpOptions,
+} from "ext:deno_node/internal/fs/utils.mjs";
+import { promisify } from "ext:deno_node/internal/util.mjs";
+
+const core = globalThis.__bootstrap.core;
+const ops = core.ops;
+const { op_node_cp } = core.ensureFastOps();
+
+export function cpSync(src, dest, options) {
+ validateCpOptions(options);
+ const srcPath = getValidatedPath(src, "src");
+ const destPath = getValidatedPath(dest, "dest");
+
+ ops.op_node_cp_sync(srcPath, destPath);
+}
+
+export function cp(src, dest, options, callback) {
+ if (typeof options === "function") {
+ callback = options;
+ options = {};
+ }
+ validateCpOptions(options);
+ const srcPath = getValidatedPath(src, "src");
+ const destPath = getValidatedPath(dest, "dest");
+
+ op_node_cp(
+ srcPath,
+ destPath,
+ ).then(
+ (res) => callback(null, res),
+ (err) => callback(err, null),
+ );
+}
+
+export const cpPromise = promisify(cp);
diff --git a/ext/node/polyfills/fs.ts b/ext/node/polyfills/fs.ts
index 881f0c139..01ac9912e 100644
--- a/ext/node/polyfills/fs.ts
+++ b/ext/node/polyfills/fs.ts
@@ -18,6 +18,7 @@ import {
copyFilePromise,
copyFileSync,
} from "ext:deno_node/_fs/_fs_copy.ts";
+import { cp, cpPromise, cpSync } from "ext:deno_node/_fs/_fs_cp.js";
import Dir from "ext:deno_node/_fs/_fs_dir.ts";
import Dirent from "ext:deno_node/_fs/_fs_dirent.ts";
import { exists, existsSync } from "ext:deno_node/_fs/_fs_exists.ts";
@@ -137,6 +138,7 @@ const {
const promises = {
access: accessPromise,
copyFile: copyFilePromise,
+ cp: cpPromise,
open: openPromise,
opendir: opendirPromise,
rename: renamePromise,
@@ -179,6 +181,8 @@ export default {
constants,
copyFile,
copyFileSync,
+ cp,
+ cpSync,
createReadStream,
createWriteStream,
Dir,
@@ -280,6 +284,8 @@ export {
constants,
copyFile,
copyFileSync,
+ cp,
+ cpSync,
createReadStream,
createWriteStream,
Dir,
diff --git a/runtime/permissions/mod.rs b/runtime/permissions/mod.rs
index 7740a8e31..89adab361 100644
--- a/runtime/permissions/mod.rs
+++ b/runtime/permissions/mod.rs
@@ -1384,6 +1384,15 @@ impl deno_node::NodePermissions for PermissionsContainer {
self.0.lock().read.check(path, api_name)
}
+ #[inline(always)]
+ fn check_write_with_api_name(
+ &self,
+ path: &Path,
+ api_name: Option<&str>,
+ ) -> Result<(), AnyError> {
+ self.0.lock().write.check(path, api_name)
+ }
+
fn check_sys(&self, kind: &str, api_name: &str) -> Result<(), AnyError> {
self.0.lock().sys.check(kind, Some(api_name))
}
diff --git a/runtime/snapshot.rs b/runtime/snapshot.rs
index 978f3d70e..a50f0773a 100644
--- a/runtime/snapshot.rs
+++ b/runtime/snapshot.rs
@@ -84,6 +84,13 @@ impl deno_node::NodePermissions for Permissions {
) -> Result<(), deno_core::error::AnyError> {
unreachable!("snapshotting!")
}
+ fn check_write_with_api_name(
+ &self,
+ _p: &Path,
+ _api_name: Option<&str>,
+ ) -> Result<(), deno_core::error::AnyError> {
+ unreachable!("snapshotting!")
+ }
fn check_sys(
&self,
_kind: &str,