summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2022-04-18 18:00:14 -0400
committerGitHub <noreply@github.com>2022-04-18 18:00:14 -0400
commita64e63c3614b98aa2b51fb6b7ef4e30251e03111 (patch)
treebe380bdc2d7c61126c594708aa97cacede9b6316
parentca3b20df3c270f1bf5f1a7980c083c22a590420d (diff)
perf: move Deno.writeTextFile and like functions to Rust (#14221)
Co-authored-by: Luca Casonato <hello@lcas.dev>
-rw-r--r--cli/tests/unit/write_file_test.ts54
-rw-r--r--cli/tests/unit/write_text_file_test.ts24
-rw-r--r--core/async_cancel.rs12
-rw-r--r--ext/web/lib.rs9
-rw-r--r--runtime/js/40_write_file.js93
-rw-r--r--runtime/ops/fs.rs168
6 files changed, 222 insertions, 138 deletions
diff --git a/cli/tests/unit/write_file_test.ts b/cli/tests/unit/write_file_test.ts
index b46502eef..9cbc0b272 100644
--- a/cli/tests/unit/write_file_test.ts
+++ b/cli/tests/unit/write_file_test.ts
@@ -17,7 +17,7 @@ Deno.test(
const dataRead = Deno.readFileSync(filename);
const dec = new TextDecoder("utf-8");
const actual = dec.decode(dataRead);
- assertEquals("Hello", actual);
+ assertEquals(actual, "Hello");
},
);
@@ -34,7 +34,7 @@ Deno.test(
const dataRead = Deno.readFileSync(fileUrl);
const dec = new TextDecoder("utf-8");
const actual = dec.decode(dataRead);
- assertEquals("Hello", actual);
+ assertEquals(actual, "Hello");
Deno.removeSync(tempDir, { recursive: true });
},
@@ -92,7 +92,7 @@ Deno.test(
const dataRead = Deno.readFileSync(filename);
const dec = new TextDecoder("utf-8");
const actual = dec.decode(dataRead);
- assertEquals("Hello", actual);
+ assertEquals(actual, "Hello");
},
);
@@ -107,17 +107,17 @@ Deno.test(
let dataRead = Deno.readFileSync(filename);
const dec = new TextDecoder("utf-8");
let actual = dec.decode(dataRead);
- assertEquals("HelloHello", actual);
+ assertEquals(actual, "HelloHello");
// Now attempt overwrite
Deno.writeFileSync(filename, data, { append: false });
dataRead = Deno.readFileSync(filename);
actual = dec.decode(dataRead);
- assertEquals("Hello", actual);
+ assertEquals(actual, "Hello");
// append not set should also overwrite
Deno.writeFileSync(filename, data);
dataRead = Deno.readFileSync(filename);
actual = dec.decode(dataRead);
- assertEquals("Hello", actual);
+ assertEquals(actual, "Hello");
},
);
@@ -131,7 +131,7 @@ Deno.test(
const dataRead = Deno.readFileSync(filename);
const dec = new TextDecoder("utf-8");
const actual = dec.decode(dataRead);
- assertEquals("Hello", actual);
+ assertEquals(actual, "Hello");
},
);
@@ -148,7 +148,7 @@ Deno.test(
const dataRead = Deno.readFileSync(fileUrl);
const dec = new TextDecoder("utf-8");
const actual = dec.decode(dataRead);
- assertEquals("Hello", actual);
+ assertEquals(actual, "Hello");
Deno.removeSync(tempDir, { recursive: true });
},
@@ -212,7 +212,7 @@ Deno.test(
const dataRead = Deno.readFileSync(filename);
const dec = new TextDecoder("utf-8");
const actual = dec.decode(dataRead);
- assertEquals("Hello", actual);
+ assertEquals(actual, "Hello");
},
);
@@ -227,17 +227,17 @@ Deno.test(
let dataRead = Deno.readFileSync(filename);
const dec = new TextDecoder("utf-8");
let actual = dec.decode(dataRead);
- assertEquals("HelloHello", actual);
+ assertEquals(actual, "HelloHello");
// Now attempt overwrite
await Deno.writeFile(filename, data, { append: false });
dataRead = Deno.readFileSync(filename);
actual = dec.decode(dataRead);
- assertEquals("Hello", actual);
+ assertEquals(actual, "Hello");
// append not set should also overwrite
await Deno.writeFile(filename, data);
dataRead = Deno.readFileSync(filename);
actual = dec.decode(dataRead);
- assertEquals("Hello", actual);
+ assertEquals(actual, "Hello");
},
);
@@ -256,8 +256,6 @@ Deno.test(
assert(e instanceof Error);
assertEquals(e.name, "AbortError");
}
- const stat = Deno.statSync(filename);
- assertEquals(stat.size, 0);
},
);
@@ -276,8 +274,6 @@ Deno.test(
} catch (e) {
assertEquals(e, abortReason);
}
- const stat = Deno.statSync(filename);
- assertEquals(stat.size, 0);
},
);
@@ -295,8 +291,6 @@ Deno.test(
} catch (e) {
assertEquals(e, "Some string");
}
- const stat = Deno.statSync(filename);
- assertEquals(stat.size, 0);
},
);
@@ -315,8 +309,7 @@ Deno.test(
assert(e instanceof Error);
assertEquals(e.name, "AbortError");
}
- const stat = Deno.statSync(filename);
- assertEquals(stat.size, 0);
+ assertNotExists(filename);
},
);
@@ -335,8 +328,7 @@ Deno.test(
} catch (e) {
assertEquals(e, abortReason);
}
- const stat = Deno.statSync(filename);
- assertEquals(stat.size, 0);
+ assertNotExists(filename);
},
);
@@ -356,7 +348,21 @@ Deno.test(
} catch (e) {
assertEquals(e, "Some string");
}
- const stat = Deno.statSync(filename);
- assertEquals(stat.size, 0);
+ assertNotExists(filename);
},
);
+
+function assertNotExists(filename: string | URL) {
+ if (pathExists(filename)) {
+ throw new Error(`The file ${filename} exists.`);
+ }
+}
+
+function pathExists(path: string | URL) {
+ try {
+ Deno.statSync(path);
+ return true;
+ } catch {
+ return false;
+ }
+}
diff --git a/cli/tests/unit/write_text_file_test.ts b/cli/tests/unit/write_text_file_test.ts
index ed92b0f35..c83534928 100644
--- a/cli/tests/unit/write_text_file_test.ts
+++ b/cli/tests/unit/write_text_file_test.ts
@@ -11,7 +11,7 @@ Deno.test(
const filename = Deno.makeTempDirSync() + "/test.txt";
Deno.writeTextFileSync(filename, "Hello");
const dataRead = Deno.readTextFileSync(filename);
- assertEquals("Hello", dataRead);
+ assertEquals(dataRead, "Hello");
},
);
@@ -24,7 +24,7 @@ Deno.test(
);
Deno.writeTextFileSync(fileUrl, "Hello");
const dataRead = Deno.readTextFileSync(fileUrl);
- assertEquals("Hello", dataRead);
+ assertEquals(dataRead, "Hello");
Deno.removeSync(fileUrl, { recursive: true });
},
@@ -78,7 +78,7 @@ Deno.test(
// Turn on create, should have no error
Deno.writeTextFileSync(filename, data, { create: true });
Deno.writeTextFileSync(filename, data, { create: false });
- assertEquals("Hello", Deno.readTextFileSync(filename));
+ assertEquals(Deno.readTextFileSync(filename), "Hello");
},
);
@@ -89,13 +89,13 @@ Deno.test(
const filename = Deno.makeTempDirSync() + "/test.txt";
Deno.writeTextFileSync(filename, data);
Deno.writeTextFileSync(filename, data, { append: true });
- assertEquals("HelloHello", Deno.readTextFileSync(filename));
+ assertEquals(Deno.readTextFileSync(filename), "HelloHello");
// Now attempt overwrite
Deno.writeTextFileSync(filename, data, { append: false });
- assertEquals("Hello", Deno.readTextFileSync(filename));
+ assertEquals(Deno.readTextFileSync(filename), "Hello");
// append not set should also overwrite
Deno.writeTextFileSync(filename, data);
- assertEquals("Hello", Deno.readTextFileSync(filename));
+ assertEquals(Deno.readTextFileSync(filename), "Hello");
},
);
@@ -105,7 +105,7 @@ Deno.test(
const filename = Deno.makeTempDirSync() + "/test.txt";
await Deno.writeTextFile(filename, "Hello");
const dataRead = Deno.readTextFileSync(filename);
- assertEquals("Hello", dataRead);
+ assertEquals(dataRead, "Hello");
},
);
@@ -118,7 +118,7 @@ Deno.test(
);
await Deno.writeTextFile(fileUrl, "Hello");
const dataRead = Deno.readTextFileSync(fileUrl);
- assertEquals("Hello", dataRead);
+ assertEquals(dataRead, "Hello");
Deno.removeSync(fileUrl, { recursive: true });
},
@@ -178,7 +178,7 @@ Deno.test(
// Turn on create, should have no error
await Deno.writeTextFile(filename, data, { create: true });
await Deno.writeTextFile(filename, data, { create: false });
- assertEquals("Hello", Deno.readTextFileSync(filename));
+ assertEquals(Deno.readTextFileSync(filename), "Hello");
},
);
@@ -189,12 +189,12 @@ Deno.test(
const filename = Deno.makeTempDirSync() + "/test.txt";
await Deno.writeTextFile(filename, data);
await Deno.writeTextFile(filename, data, { append: true });
- assertEquals("HelloHello", Deno.readTextFileSync(filename));
+ assertEquals(Deno.readTextFileSync(filename), "HelloHello");
// Now attempt overwrite
await Deno.writeTextFile(filename, data, { append: false });
- assertEquals("Hello", Deno.readTextFileSync(filename));
+ assertEquals(Deno.readTextFileSync(filename), "Hello");
// append not set should also overwrite
await Deno.writeTextFile(filename, data);
- assertEquals("Hello", Deno.readTextFileSync(filename));
+ assertEquals(Deno.readTextFileSync(filename), "Hello");
},
);
diff --git a/core/async_cancel.rs b/core/async_cancel.rs
index e8f25136c..cf338174d 100644
--- a/core/async_cancel.rs
+++ b/core/async_cancel.rs
@@ -1,6 +1,7 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
use crate::RcLike;
+use crate::Resource;
use futures::future::FusedFuture;
use futures::future::Future;
use futures::future::TryFuture;
@@ -8,6 +9,7 @@ use futures::task::Context;
use futures::task::Poll;
use pin_project::pin_project;
use std::any::type_name;
+use std::borrow::Cow;
use std::error::Error;
use std::fmt;
use std::fmt::Display;
@@ -84,6 +86,16 @@ impl<F: Future> FusedFuture for Cancelable<F> {
}
}
+impl Resource for CancelHandle {
+ fn name(&self) -> Cow<str> {
+ "cancellation".into()
+ }
+
+ fn close(self: Rc<Self>) {
+ self.cancel();
+ }
+}
+
#[pin_project(project = TryCancelableProjection)]
#[derive(Debug)]
pub struct TryCancelable<F> {
diff --git a/ext/web/lib.rs b/ext/web/lib.rs
index 423e53c51..d14a4a5d5 100644
--- a/ext/web/lib.rs
+++ b/ext/web/lib.rs
@@ -12,6 +12,7 @@ use deno_core::include_js_files;
use deno_core::op;
use deno_core::url::Url;
use deno_core::ByteString;
+use deno_core::CancelHandle;
use deno_core::Extension;
use deno_core::OpState;
use deno_core::Resource;
@@ -107,6 +108,7 @@ pub fn init<P: TimersPermission + 'static>(
compression::op_compression_finish::decl(),
op_now::decl::<P>(),
op_timer_handle::decl(),
+ op_cancel_handle::decl(),
op_sleep::decl(),
op_sleep_sync::decl::<P>(),
])
@@ -352,6 +354,13 @@ fn op_encoding_encode_into(
})
}
+/// Creates a [`CancelHandle`] resource that can be used to cancel invocations of certain ops.
+#[op]
+pub fn op_cancel_handle(state: &mut OpState) -> Result<ResourceId, AnyError> {
+ let rid = state.resource_table.add(CancelHandle::new());
+ Ok(rid)
+}
+
pub fn get_declaration() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_web.d.ts")
}
diff --git a/runtime/js/40_write_file.js b/runtime/js/40_write_file.js
index 8eac953d4..462d71266 100644
--- a/runtime/js/40_write_file.js
+++ b/runtime/js/40_write_file.js
@@ -1,12 +1,9 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
"use strict";
((window) => {
- const { stat, statSync, chmod, chmodSync } = window.__bootstrap.fs;
- const { open, openSync } = window.__bootstrap.files;
- const { build } = window.__bootstrap.build;
- const {
- TypedArrayPrototypeSubarray,
- } = window.__bootstrap.primordials;
+ const core = window.__bootstrap.core;
+ const { abortSignal } = window.__bootstrap;
+ const { pathFromURL } = window.__bootstrap.util;
function writeFileSync(
path,
@@ -14,33 +11,13 @@
options = {},
) {
options.signal?.throwIfAborted();
- if (options.create !== undefined) {
- const create = !!options.create;
- if (!create) {
- // verify that file exists
- statSync(path);
- }
- }
-
- const openOptions = options.append
- ? { write: true, create: true, append: true }
- : { write: true, create: true, truncate: true };
- const file = openSync(path, openOptions);
-
- if (
- options.mode !== undefined &&
- options.mode !== null &&
- build.os !== "windows"
- ) {
- chmodSync(path, options.mode);
- }
-
- let nwritten = 0;
- while (nwritten < data.length) {
- nwritten += file.writeSync(TypedArrayPrototypeSubarray(data, nwritten));
- }
-
- file.close();
+ core.opSync("op_write_file_sync", {
+ path: pathFromURL(path),
+ data,
+ mode: options.mode,
+ append: options.append ?? false,
+ create: options.create ?? true,
+ });
}
async function writeFile(
@@ -48,38 +25,30 @@
data,
options = {},
) {
- if (options.create !== undefined) {
- const create = !!options.create;
- if (!create) {
- // verify that file exists
- await stat(path);
- }
- }
-
- const openOptions = options.append
- ? { write: true, create: true, append: true }
- : { write: true, create: true, truncate: true };
- const file = await open(path, openOptions);
-
- if (
- options.mode !== undefined &&
- options.mode !== null &&
- build.os !== "windows"
- ) {
- await chmod(path, options.mode);
+ let cancelRid;
+ let abortHandler;
+ if (options.signal) {
+ options.signal.throwIfAborted();
+ cancelRid = core.opSync("op_cancel_handle");
+ abortHandler = () => core.tryClose(cancelRid);
+ options.signal[abortSignal.add](abortHandler);
}
-
- const signal = options?.signal ?? null;
- let nwritten = 0;
try {
- while (nwritten < data.length) {
- signal?.throwIfAborted();
- nwritten += await file.write(
- TypedArrayPrototypeSubarray(data, nwritten),
- );
- }
+ await core.opAsync("op_write_file_async", {
+ path: pathFromURL(path),
+ data,
+ mode: options.mode,
+ append: options.append ?? false,
+ create: options.create ?? true,
+ cancelRid,
+ });
} finally {
- file.close();
+ if (options.signal) {
+ options.signal[abortSignal.remove](abortHandler);
+
+ // always throw the abort error when aborted
+ options.signal.throwIfAborted();
+ }
}
}
diff --git a/runtime/ops/fs.rs b/runtime/ops/fs.rs
index a3d316ae7..3e174e00b 100644
--- a/runtime/ops/fs.rs
+++ b/runtime/ops/fs.rs
@@ -9,6 +9,9 @@ use deno_core::error::custom_error;
use deno_core::error::type_error;
use deno_core::error::AnyError;
use deno_core::op;
+use deno_core::CancelFuture;
+use deno_core::CancelHandle;
+use deno_core::ZeroCopyBuf;
use deno_core::Extension;
use deno_core::OpState;
@@ -23,6 +26,7 @@ use std::cell::RefCell;
use std::convert::From;
use std::env::{current_dir, set_current_dir, temp_dir};
use std::io;
+use std::io::Write;
use std::io::{Error, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::rc::Rc;
@@ -40,6 +44,8 @@ pub fn init() -> Extension {
.ops(vec![
op_open_sync::decl(),
op_open_async::decl(),
+ op_write_file_sync::decl(),
+ op_write_file_async::decl(),
op_seek_sync::decl(),
op_seek_async::decl(),
op_fdatasync_sync::decl(),
@@ -117,7 +123,7 @@ pub struct OpenOptions {
fn open_helper(
state: &mut OpState,
- args: OpenArgs,
+ args: &OpenArgs,
) -> Result<(PathBuf, std::fs::OpenOptions), AnyError> {
let path = Path::new(&args.path).to_path_buf();
@@ -136,7 +142,7 @@ fn open_helper(
}
let permissions = state.borrow_mut::<Permissions>();
- let options = args.options;
+ let options = &args.options;
if options.read {
permissions.read.check(&path)?;
@@ -162,7 +168,7 @@ fn op_open_sync(
state: &mut OpState,
args: OpenArgs,
) -> Result<ResourceId, AnyError> {
- let (path, open_options) = open_helper(state, args)?;
+ let (path, open_options) = open_helper(state, &args)?;
let std_file = open_options.open(&path).map_err(|err| {
Error::new(err.kind(), format!("{}, open '{}'", err, path.display()))
})?;
@@ -177,13 +183,14 @@ async fn op_open_async(
state: Rc<RefCell<OpState>>,
args: OpenArgs,
) -> Result<ResourceId, AnyError> {
- let (path, open_options) = open_helper(&mut state.borrow_mut(), args)?;
+ let (path, open_options) = open_helper(&mut state.borrow_mut(), &args)?;
let tokio_file = tokio::fs::OpenOptions::from(open_options)
.open(&path)
.await
.map_err(|err| {
Error::new(err.kind(), format!("{}, open '{}'", err, path.display()))
})?;
+
let resource = StdFileResource::fs_file(tokio_file);
let rid = state.borrow_mut().resource_table.add(resource);
Ok(rid)
@@ -191,6 +198,99 @@ async fn op_open_async(
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
+pub struct WriteFileArgs {
+ path: String,
+ mode: Option<u32>,
+ append: bool,
+ create: bool,
+ data: ZeroCopyBuf,
+ cancel_rid: Option<ResourceId>,
+}
+
+impl WriteFileArgs {
+ fn into_open_args_and_data(self) -> (OpenArgs, ZeroCopyBuf) {
+ (
+ OpenArgs {
+ path: self.path,
+ mode: self.mode,
+ options: OpenOptions {
+ read: false,
+ write: true,
+ create: self.create,
+ truncate: !self.append,
+ append: self.append,
+ create_new: false,
+ },
+ },
+ self.data,
+ )
+ }
+}
+
+#[op]
+fn op_write_file_sync(
+ state: &mut OpState,
+ args: WriteFileArgs,
+) -> Result<(), AnyError> {
+ let (open_args, data) = args.into_open_args_and_data();
+ let (path, open_options) = open_helper(state, &open_args)?;
+ write_file(&path, open_options, &open_args, data)
+}
+
+#[op]
+async fn op_write_file_async(
+ state: Rc<RefCell<OpState>>,
+ args: WriteFileArgs,
+) -> Result<(), AnyError> {
+ let cancel_handle = match args.cancel_rid {
+ Some(cancel_rid) => state
+ .borrow_mut()
+ .resource_table
+ .get::<CancelHandle>(cancel_rid)
+ .ok(),
+ None => None,
+ };
+ let (open_args, data) = args.into_open_args_and_data();
+ let (path, open_options) = open_helper(&mut *state.borrow_mut(), &open_args)?;
+ let write_future = tokio::task::spawn_blocking(move || {
+ write_file(&path, open_options, &open_args, data)
+ });
+ if let Some(cancel_handle) = cancel_handle {
+ write_future.or_cancel(cancel_handle).await???;
+ } else {
+ write_future.await??;
+ }
+ Ok(())
+}
+
+fn write_file(
+ path: &Path,
+ open_options: std::fs::OpenOptions,
+ _open_args: &OpenArgs,
+ data: ZeroCopyBuf,
+) -> Result<(), AnyError> {
+ let mut std_file = open_options.open(path).map_err(|err| {
+ Error::new(err.kind(), format!("{}, open '{}'", err, path.display()))
+ })?;
+
+ // need to chmod the file if it already exists and a mode is specified
+ #[cfg(unix)]
+ if let Some(mode) = &_open_args.mode {
+ use std::os::unix::fs::PermissionsExt;
+ let permissions = PermissionsExt::from_mode(mode & 0o777);
+ std_file
+ .set_permissions(permissions)
+ .map_err(|err: Error| {
+ Error::new(err.kind(), format!("{}, chmod '{}'", err, path.display()))
+ })?;
+ }
+
+ std_file.write_all(&data)?;
+ Ok(())
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
pub struct SeekArgs {
rid: ResourceId,
offset: i64,
@@ -571,28 +671,12 @@ pub struct ChmodArgs {
#[op]
fn op_chmod_sync(state: &mut OpState, args: ChmodArgs) -> Result<(), AnyError> {
- let path = Path::new(&args.path).to_path_buf();
+ let path = Path::new(&args.path);
let mode = args.mode & 0o777;
- let err_mapper = |err: Error| {
- Error::new(err.kind(), format!("{}, chmod '{}'", err, path.display()))
- };
- state.borrow_mut::<Permissions>().write.check(&path)?;
+ state.borrow_mut::<Permissions>().write.check(path)?;
debug!("op_chmod_sync {} {:o}", path.display(), mode);
- #[cfg(unix)]
- {
- use std::os::unix::fs::PermissionsExt;
- let permissions = PermissionsExt::from_mode(mode);
- std::fs::set_permissions(&path, permissions).map_err(err_mapper)?;
- Ok(())
- }
- // TODO Implement chmod for Windows (#4357)
- #[cfg(not(unix))]
- {
- // Still check file/dir exists on Windows
- let _metadata = std::fs::metadata(&path).map_err(err_mapper)?;
- Err(generic_error("Not implemented"))
- }
+ raw_chmod(path, mode)
}
#[op]
@@ -610,28 +694,32 @@ async fn op_chmod_async(
tokio::task::spawn_blocking(move || {
debug!("op_chmod_async {} {:o}", path.display(), mode);
- let err_mapper = |err: Error| {
- Error::new(err.kind(), format!("{}, chmod '{}'", err, path.display()))
- };
- #[cfg(unix)]
- {
- use std::os::unix::fs::PermissionsExt;
- let permissions = PermissionsExt::from_mode(mode);
- std::fs::set_permissions(&path, permissions).map_err(err_mapper)?;
- Ok(())
- }
- // TODO Implement chmod for Windows (#4357)
- #[cfg(not(unix))]
- {
- // Still check file/dir exists on Windows
- let _metadata = std::fs::metadata(&path).map_err(err_mapper)?;
- Err(not_supported())
- }
+ raw_chmod(&path, mode)
})
.await
.unwrap()
}
+fn raw_chmod(path: &Path, _raw_mode: u32) -> Result<(), AnyError> {
+ let err_mapper = |err: Error| {
+ Error::new(err.kind(), format!("{}, chmod '{}'", err, path.display()))
+ };
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::PermissionsExt;
+ let permissions = PermissionsExt::from_mode(_raw_mode);
+ std::fs::set_permissions(&path, permissions).map_err(err_mapper)?;
+ Ok(())
+ }
+ // TODO Implement chmod for Windows (#4357)
+ #[cfg(not(unix))]
+ {
+ // Still check file/dir exists on Windows
+ let _metadata = std::fs::metadata(&path).map_err(err_mapper)?;
+ Err(not_supported())
+ }
+}
+
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChownArgs {