diff --git a/cli/tests/unit/write_file_test.ts b/cli/tests/unit/write_file_test.ts index b46502eefb..9cbc0b272c 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 ed92b0f35b..c835349284 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 e8f25136cf..cf338174da 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 FusedFuture for Cancelable { } } +impl Resource for CancelHandle { + fn name(&self) -> Cow { + "cancellation".into() + } + + fn close(self: Rc) { + self.cancel(); + } +} + #[pin_project(project = TryCancelableProjection)] #[derive(Debug)] pub struct TryCancelable { diff --git a/ext/web/lib.rs b/ext/web/lib.rs index 423e53c510..d14a4a5d5a 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( compression::op_compression_finish::decl(), op_now::decl::

(), op_timer_handle::decl(), + op_cancel_handle::decl(), op_sleep::decl(), op_sleep_sync::decl::

(), ]) @@ -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 { + 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 8eac953d49..462d712665 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); - } + 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 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); - } - - 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 a3d316ae7c..3e174e00b7 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::(); - 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 { - 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,18 +183,112 @@ async fn op_open_async( state: Rc>, args: OpenArgs, ) -> Result { - 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) } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WriteFileArgs { + path: String, + mode: Option, + append: bool, + create: bool, + data: ZeroCopyBuf, + cancel_rid: Option, +} + +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>, + args: WriteFileArgs, +) -> Result<(), AnyError> { + let cancel_handle = match args.cancel_rid { + Some(cancel_rid) => state + .borrow_mut() + .resource_table + .get::(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 { @@ -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::().write.check(&path)?; + state.borrow_mut::().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 {