From 5ab855cbb2198740de9e14a1ec8fa67f628f4e9a Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Sat, 21 Feb 2026 22:03:27 +0000 Subject: [PATCH 01/29] wip utils --- core/services/opfs/src/backend.rs | 8 +---- core/services/opfs/src/utils.rs | 59 +++++++++++++++++++++++-------- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/core/services/opfs/src/backend.rs b/core/services/opfs/src/backend.rs index 9e28868a507c..6e53514433f8 100644 --- a/core/services/opfs/src/backend.rs +++ b/core/services/opfs/src/backend.rs @@ -15,11 +15,8 @@ // specific language governing permissions and limitations // under the License. -use std::fmt::Debug; use std::sync::Arc; -use web_sys::FileSystemGetDirectoryOptions; - use super::OPFS_SCHEME; use super::config::OpfsConfig; use super::utils::*; @@ -66,10 +63,7 @@ impl Access for OpfsBackend { } async fn create_dir(&self, path: &str, _: OpCreateDir) -> Result { - let opt = FileSystemGetDirectoryOptions::new(); - opt.set_create(true); - get_directory_handle(path, &opt).await?; - + get_directory_handle(path, true).await?; Ok(RpCreateDir::default()) } } diff --git a/core/services/opfs/src/utils.rs b/core/services/opfs/src/utils.rs index 4c2997a73015..df0c971198a4 100644 --- a/core/services/opfs/src/utils.rs +++ b/core/services/opfs/src/utils.rs @@ -26,6 +26,7 @@ use web_sys::window; use super::error::*; +/// Get the OPFS root directory handle. pub(crate) async fn get_root_directory_handle() -> Result { let navigator = window().unwrap().navigator(); let storage_manager = navigator.storage(); @@ -35,15 +36,24 @@ pub(crate) async fn get_root_directory_handle() -> Result Result { - let dirs: Vec<&str> = dir.trim_matches('/').split('/').collect(); + let opt = FileSystemGetDirectoryOptions::new(); + opt.set_create(create); + + let trimmed = path.trim_matches('/'); + if trimmed.is_empty() { + return get_root_directory_handle().await; + } let mut handle = get_root_directory_handle().await?; - for dir in dirs { - handle = JsFuture::from(handle.get_directory_handle_with_options(dir, dir_opt)) + for segment in trimmed.split('/') { + handle = JsFuture::from(handle.get_directory_handle_with_options(segment, &opt)) .await .and_then(JsCast::dyn_into) .map_err(parse_js_error)?; @@ -52,18 +62,39 @@ pub(crate) async fn get_directory_handle( Ok(handle) } -pub(crate) async fn get_handle_by_filename(filename: &str) -> Result { - let navigator = window().unwrap().navigator(); - let storage_manager = navigator.storage(); - let root: FileSystemDirectoryHandle = JsFuture::from(storage_manager.get_directory()) - .await - .and_then(JsCast::dyn_into) - .map_err(parse_js_error)?; +/// Split a file path into its parent directory handle and filename. +/// +/// For example, `"foo/bar/file.txt"` returns the handle for `"foo/bar/"` and `"file.txt"`. +/// For a root-level file like `"file.txt"`, returns the root handle and `"file.txt"`. +/// +/// When `create` is true, intermediate directories are created as needed. +pub(crate) async fn get_parent_dir_and_name<'a>( + path: &'a str, + create: bool, +) -> Result<(FileSystemDirectoryHandle, &'a str)> { + let trimmed = path.trim_matches('/'); + match trimmed.rsplit_once('/') { + Some((parent, name)) => { + let dir = get_directory_handle(parent, create).await?; + Ok((dir, name)) + } + None => { + let root = get_root_directory_handle().await?; + Ok((root, trimmed)) + } + } +} + +/// Get a file handle by its full path. +/// +/// When `create` is true, intermediate directories and the file itself are created as needed. +pub(crate) async fn get_file_handle(path: &str, create: bool) -> Result { + let (dir, name) = get_parent_dir_and_name(path, create).await?; let opt = FileSystemGetFileOptions::new(); - opt.set_create(true); + opt.set_create(create); - JsFuture::from(root.get_file_handle_with_options(filename, &opt)) + JsFuture::from(dir.get_file_handle_with_options(name, &opt)) .await .and_then(JsCast::dyn_into) .map_err(parse_js_error) From 1947b146726a660f0459e61b2bb36afa3f13e940 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Sat, 21 Feb 2026 22:29:13 +0000 Subject: [PATCH 02/29] wip remove redundant funcs --- core/services/opfs/src/backend.rs | 1 + core/services/opfs/src/utils.rs | 48 ++----------------------------- 2 files changed, 4 insertions(+), 45 deletions(-) diff --git a/core/services/opfs/src/backend.rs b/core/services/opfs/src/backend.rs index 6e53514433f8..73333d0c606f 100644 --- a/core/services/opfs/src/backend.rs +++ b/core/services/opfs/src/backend.rs @@ -63,6 +63,7 @@ impl Access for OpfsBackend { } async fn create_dir(&self, path: &str, _: OpCreateDir) -> Result { + debug_assert!(path != "/", "root path should be handled upstream"); get_directory_handle(path, true).await?; Ok(RpCreateDir::default()) } diff --git a/core/services/opfs/src/utils.rs b/core/services/opfs/src/utils.rs index df0c971198a4..d71c7b5a18ce 100644 --- a/core/services/opfs/src/utils.rs +++ b/core/services/opfs/src/utils.rs @@ -19,9 +19,7 @@ use opendal_core::Result; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; use web_sys::FileSystemDirectoryHandle; -use web_sys::FileSystemFileHandle; use web_sys::FileSystemGetDirectoryOptions; -use web_sys::FileSystemGetFileOptions; use web_sys::window; use super::error::*; @@ -47,13 +45,10 @@ pub(crate) async fn get_directory_handle( opt.set_create(create); let trimmed = path.trim_matches('/'); - if trimmed.is_empty() { - return get_root_directory_handle().await; - } - let mut handle = get_root_directory_handle().await?; - for segment in trimmed.split('/') { - handle = JsFuture::from(handle.get_directory_handle_with_options(segment, &opt)) + let dirs = trimmed.split('/'); + for dir in dirs { + handle = JsFuture::from(handle.get_directory_handle_with_options(dir, &opt)) .await .and_then(JsCast::dyn_into) .map_err(parse_js_error)?; @@ -62,40 +57,3 @@ pub(crate) async fn get_directory_handle( Ok(handle) } -/// Split a file path into its parent directory handle and filename. -/// -/// For example, `"foo/bar/file.txt"` returns the handle for `"foo/bar/"` and `"file.txt"`. -/// For a root-level file like `"file.txt"`, returns the root handle and `"file.txt"`. -/// -/// When `create` is true, intermediate directories are created as needed. -pub(crate) async fn get_parent_dir_and_name<'a>( - path: &'a str, - create: bool, -) -> Result<(FileSystemDirectoryHandle, &'a str)> { - let trimmed = path.trim_matches('/'); - match trimmed.rsplit_once('/') { - Some((parent, name)) => { - let dir = get_directory_handle(parent, create).await?; - Ok((dir, name)) - } - None => { - let root = get_root_directory_handle().await?; - Ok((root, trimmed)) - } - } -} - -/// Get a file handle by its full path. -/// -/// When `create` is true, intermediate directories and the file itself are created as needed. -pub(crate) async fn get_file_handle(path: &str, create: bool) -> Result { - let (dir, name) = get_parent_dir_and_name(path, create).await?; - - let opt = FileSystemGetFileOptions::new(); - opt.set_create(create); - - JsFuture::from(dir.get_file_handle_with_options(name, &opt)) - .await - .and_then(JsCast::dyn_into) - .map_err(parse_js_error) -} From 05bdf929b01b737298504708eb9efa865382f49a Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Sat, 21 Feb 2026 22:34:51 +0000 Subject: [PATCH 03/29] wip --- core/services/opfs/src/utils.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/services/opfs/src/utils.rs b/core/services/opfs/src/utils.rs index d71c7b5a18ce..64d72f2a0b96 100644 --- a/core/services/opfs/src/utils.rs +++ b/core/services/opfs/src/utils.rs @@ -28,6 +28,7 @@ use super::error::*; pub(crate) async fn get_root_directory_handle() -> Result { let navigator = window().unwrap().navigator(); let storage_manager = navigator.storage(); + // This may fail if not secure (not: HTTPS or localhost) JsFuture::from(storage_manager.get_directory()) .await .and_then(JsCast::dyn_into) @@ -44,9 +45,8 @@ pub(crate) async fn get_directory_handle( let opt = FileSystemGetDirectoryOptions::new(); opt.set_create(create); - let trimmed = path.trim_matches('/'); + let dirs = path.trim_matches('/').split('/'); let mut handle = get_root_directory_handle().await?; - let dirs = trimmed.split('/'); for dir in dirs { handle = JsFuture::from(handle.get_directory_handle_with_options(dir, &opt)) .await @@ -56,4 +56,3 @@ pub(crate) async fn get_directory_handle( Ok(handle) } - From f809a6bdb24773a5ff1a8d02e46bd37886f8e585 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Sat, 21 Feb 2026 22:45:15 +0000 Subject: [PATCH 04/29] wip error --- core/services/opfs/Cargo.toml | 3 ++- core/services/opfs/src/error.rs | 25 +++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/core/services/opfs/Cargo.toml b/core/services/opfs/Cargo.toml index b82b11271a27..872dbd09a0a3 100644 --- a/core/services/opfs/Cargo.toml +++ b/core/services/opfs/Cargo.toml @@ -37,7 +37,7 @@ serde = { workspace = true, features = ["derive"] } wasm-bindgen = "0.2.100" wasm-bindgen-futures = "0.4.50" web-sys = { version = "0.3.77", features = [ - "Window", + "DomException", "File", "FileSystemDirectoryHandle", "FileSystemFileHandle", @@ -46,4 +46,5 @@ web-sys = { version = "0.3.77", features = [ "FileSystemWritableFileStream", "Navigator", "StorageManager", + "Window", ] } diff --git a/core/services/opfs/src/error.rs b/core/services/opfs/src/error.rs index 5f2a1a0fe343..c5c1b2c2ef01 100644 --- a/core/services/opfs/src/error.rs +++ b/core/services/opfs/src/error.rs @@ -15,14 +15,35 @@ // specific language governing permissions and limitations // under the License. +use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; +use web_sys::DomException; use opendal_core::Error; use opendal_core::ErrorKind; -pub(crate) fn parse_js_error(msg: JsValue) -> Error { +pub(crate) fn parse_js_error(value: JsValue) -> Error { + if let Some(exc) = value.dyn_ref::() { + let kind = match exc.name().as_str() { + "NotFoundError" => ErrorKind::NotFound, + "TypeMismatchError" => ErrorKind::NotFound, + "NotAllowedError" => ErrorKind::PermissionDenied, + _ => ErrorKind::Unexpected, + }; + return Error::new(kind, exc.message()); + } + + // FIXME: how to map `TypeError`` from `getDirectoryHandle`: + // "name specified is not a valid string or contains characters that + // would interfere with the native file system" + if let Some(err) = value.dyn_ref::() { + return Error::new(ErrorKind::Unexpected, err.message().into()); + } + Error::new( ErrorKind::Unexpected, - msg.as_string().unwrap_or_else(String::new), + value + .as_string() + .unwrap_or_else(|| "unknown JS error".to_string()), ) } From f29c0a42f49a8f79debd1e69e66225a90d631912 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Sat, 21 Feb 2026 22:58:49 +0000 Subject: [PATCH 05/29] wip remove unused code --- core/Cargo.lock | 11 +++++++ core/services/opfs/src/core.rs | 57 --------------------------------- core/services/opfs/src/error.rs | 2 +- 3 files changed, 12 insertions(+), 58 deletions(-) diff --git a/core/Cargo.lock b/core/Cargo.lock index b4359c518160..07c39fc24e09 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -2799,6 +2799,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "edge_test_opfs_wasm32" +version = "0.55.0" +dependencies = [ + "opendal", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", +] + [[package]] name = "edge_test_s3_read_on_wasm" version = "0.55.0" diff --git a/core/services/opfs/src/core.rs b/core/services/opfs/src/core.rs index 10cd1da91996..b248758bc120 100644 --- a/core/services/opfs/src/core.rs +++ b/core/services/opfs/src/core.rs @@ -14,60 +14,3 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. - -use std::fmt::Debug; - -use wasm_bindgen::JsCast; -use wasm_bindgen_futures::JsFuture; -use web_sys::File; -use web_sys::FileSystemWritableFileStream; - -use opendal_core::Error; -use opendal_core::Result; - -use super::error::*; -use super::utils::*; - -#[derive(Default, Debug)] -pub struct OpfsCore {} - -impl OpfsCore { - #[allow(unused)] - async fn store_file(&self, file_name: &str, content: &[u8]) -> Result<(), Error> { - let handle = get_handle_by_filename(file_name).await?; - - let writable: FileSystemWritableFileStream = JsFuture::from(handle.create_writable()) - .await - .and_then(JsCast::dyn_into) - .map_err(parse_js_error)?; - - JsFuture::from( - writable - .write_with_u8_array(content) - .map_err(parse_js_error)?, - ) - .await - .map_err(parse_js_error)?; - - JsFuture::from(writable.close()) - .await - .map_err(parse_js_error)?; - - Ok(()) - } - - #[allow(unused)] - async fn read_file(&self, file_name: &str) -> Result, Error> { - let handle = get_handle_by_filename(file_name).await?; - - let file: File = JsFuture::from(handle.get_file()) - .await - .and_then(JsCast::dyn_into) - .map_err(parse_js_error)?; - let array_buffer = JsFuture::from(file.array_buffer()) - .await - .map_err(parse_js_error)?; - - Ok(js_sys::Uint8Array::new(&array_buffer).to_vec()) - } -} diff --git a/core/services/opfs/src/error.rs b/core/services/opfs/src/error.rs index c5c1b2c2ef01..b0de1150d93f 100644 --- a/core/services/opfs/src/error.rs +++ b/core/services/opfs/src/error.rs @@ -37,7 +37,7 @@ pub(crate) fn parse_js_error(value: JsValue) -> Error { // "name specified is not a valid string or contains characters that // would interfere with the native file system" if let Some(err) = value.dyn_ref::() { - return Error::new(ErrorKind::Unexpected, err.message().into()); + return Error::new(ErrorKind::Unexpected, String::from(err.message())); } Error::new( From 3396d5a60c2fa968a5ce91d45cad6734556bfea2 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Sat, 21 Feb 2026 22:59:33 +0000 Subject: [PATCH 06/29] Add OPFS for wasm32 backend edge test --- core/edge/opfs_wasm32/Cargo.toml | 39 ++++++++++++++++++++++++++++ core/edge/opfs_wasm32/README.md | 27 ++++++++++++++++++++ core/edge/opfs_wasm32/src/lib.rs | 44 ++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 core/edge/opfs_wasm32/Cargo.toml create mode 100644 core/edge/opfs_wasm32/README.md create mode 100644 core/edge/opfs_wasm32/src/lib.rs diff --git a/core/edge/opfs_wasm32/Cargo.toml b/core/edge/opfs_wasm32/Cargo.toml new file mode 100644 index 000000000000..562b03431524 --- /dev/null +++ b/core/edge/opfs_wasm32/Cargo.toml @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +[package] +name = "edge_test_opfs_wasm32" + +edition.workspace = true +license.workspace = true +publish = false +rust-version.workspace = true +version.workspace = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +opendal = { path = "../..", default-features = false, features = [ + "services-opfs", +] } +wasm-bindgen = "0.2.89" +wasm-bindgen-futures = "0.4.39" +web-sys = { version = "0.3.77", features = ["console"] } + +[dev-dependencies] +wasm-bindgen-test = "0.3.41" diff --git a/core/edge/opfs_wasm32/README.md b/core/edge/opfs_wasm32/README.md new file mode 100644 index 000000000000..409c2f25de25 --- /dev/null +++ b/core/edge/opfs_wasm32/README.md @@ -0,0 +1,27 @@ +# OPFS on WASM + +This test verifies the OpenDAL OPFS service works in a browser environment. + +## Install + +```shell +cargo install wasm-pack +``` + +## Build + +```shell +wasm-pack build +``` + +## Test + +NOTE: + +- You need to have Chrome installed. +- OPFS requires a browser context (no Node.js support). +- Headless Chrome may not work for OPFS tests. + +```shell +wasm-pack test --chrome +``` diff --git a/core/edge/opfs_wasm32/src/lib.rs b/core/edge/opfs_wasm32/src/lib.rs new file mode 100644 index 000000000000..90bdac1d46a0 --- /dev/null +++ b/core/edge/opfs_wasm32/src/lib.rs @@ -0,0 +1,44 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +macro_rules! console_log { + ($($arg:tt)*) => { + web_sys::console::log_1(&format!($($arg)*).into()) + }; +} + +#[cfg(test)] +mod tests { + use console_log; + use opendal::Operator; + use opendal::services::OpfsConfig; + use wasm_bindgen_test::wasm_bindgen_test; + use wasm_bindgen_test::wasm_bindgen_test_configure; + + wasm_bindgen_test_configure!(run_in_browser); + + fn new_operator() -> Operator { + Operator::from_config(OpfsConfig::default()) + .expect("failed to create opfs operator") + .finish() + } + + #[wasm_bindgen_test] + async fn test_get_directory_handle() { + let op = new_operator(); + } +} From 58a0387277623219da82d52ca3b526d0eb20043b Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Sat, 21 Feb 2026 23:22:40 +0000 Subject: [PATCH 07/29] create_dir test --- core/edge/opfs_wasm32/src/lib.rs | 21 ++++++++++++++------- core/services/opfs/src/error.rs | 6 +++++- core/services/opfs/src/lib.rs | 8 ++++++++ core/services/opfs/src/utils.rs | 2 ++ 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/core/edge/opfs_wasm32/src/lib.rs b/core/edge/opfs_wasm32/src/lib.rs index 90bdac1d46a0..391b9163e905 100644 --- a/core/edge/opfs_wasm32/src/lib.rs +++ b/core/edge/opfs_wasm32/src/lib.rs @@ -15,20 +15,20 @@ // specific language governing permissions and limitations // under the License. -macro_rules! console_log { - ($($arg:tt)*) => { - web_sys::console::log_1(&format!($($arg)*).into()) - }; -} - #[cfg(test)] mod tests { - use console_log; + use opendal::ErrorKind; use opendal::Operator; use opendal::services::OpfsConfig; use wasm_bindgen_test::wasm_bindgen_test; use wasm_bindgen_test::wasm_bindgen_test_configure; + macro_rules! console_log { + ($($arg:tt)*) => { + web_sys::console::log_1(&format!($($arg)*).into()) + }; + } + wasm_bindgen_test_configure!(run_in_browser); fn new_operator() -> Operator { @@ -40,5 +40,12 @@ mod tests { #[wasm_bindgen_test] async fn test_get_directory_handle() { let op = new_operator(); + op.create_dir("/dir/").await.expect("directory"); + op.create_dir("/dir///").await.expect("directory"); + op.create_dir("/dir:/").await.expect("directory"); + op.create_dir("/dir<>/").await.expect("directory"); + assert_eq!(op.create_dir("/a/b/../x/y/z/").await.unwrap_err().kind(), ErrorKind::Unexpected); + // this works on Chrome, but fails on macOS + // assert_eq!(op.create_dir("/dir\0/").await.unwrap_err().kind(), ErrorKind::Unexpected); } } diff --git a/core/services/opfs/src/error.rs b/core/services/opfs/src/error.rs index b0de1150d93f..908e7a794cbf 100644 --- a/core/services/opfs/src/error.rs +++ b/core/services/opfs/src/error.rs @@ -23,12 +23,16 @@ use opendal_core::Error; use opendal_core::ErrorKind; pub(crate) fn parse_js_error(value: JsValue) -> Error { + console_debug!("parse_js_error: {:?}", value); if let Some(exc) = value.dyn_ref::() { let kind = match exc.name().as_str() { "NotFoundError" => ErrorKind::NotFound, "TypeMismatchError" => ErrorKind::NotFound, "NotAllowedError" => ErrorKind::PermissionDenied, - _ => ErrorKind::Unexpected, + e => { + console_debug!("Got exception {e:?}"); + ErrorKind::Unexpected + } }; return Error::new(kind, exc.message()); } diff --git a/core/services/opfs/src/lib.rs b/core/services/opfs/src/lib.rs index 06887f57ffcf..db4500c1b0ad 100644 --- a/core/services/opfs/src/lib.rs +++ b/core/services/opfs/src/lib.rs @@ -25,6 +25,14 @@ pub fn register_opfs_service(registry: &opendal_core::OperatorRegistry) { registry.register::(OPFS_SCHEME); } +macro_rules! console_debug { + ($($arg:tt)*) => { + #[cfg(debug_assertions)] + web_sys::console::log_1(&format!($($arg)*).into()) + }; +} + + mod backend; mod config; mod core; diff --git a/core/services/opfs/src/utils.rs b/core/services/opfs/src/utils.rs index 64d72f2a0b96..9d05ad968cb1 100644 --- a/core/services/opfs/src/utils.rs +++ b/core/services/opfs/src/utils.rs @@ -26,6 +26,7 @@ use super::error::*; /// Get the OPFS root directory handle. pub(crate) async fn get_root_directory_handle() -> Result { + console_debug!("get_root_directory_handle"); let navigator = window().unwrap().navigator(); let storage_manager = navigator.storage(); // This may fail if not secure (not: HTTPS or localhost) @@ -42,6 +43,7 @@ pub(crate) async fn get_directory_handle( path: &str, create: bool, ) -> Result { + console_debug!("get_directory_handle path={path:?} create={create:?}"); let opt = FileSystemGetDirectoryOptions::new(); opt.set_create(create); From 181ae7047c02572039477509298c74a7a9b64862 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Sun, 22 Feb 2026 10:08:21 +0000 Subject: [PATCH 08/29] wip write --- .vscode/settings.json | 3 +- core/edge/opfs_wasm32/src/lib.rs | 45 ++++++++++++++---- core/services/opfs/src/backend.rs | 24 +++++++++- core/services/opfs/src/error.rs | 3 +- core/services/opfs/src/lib.rs | 1 + core/services/opfs/src/utils.rs | 37 +++++++++++++++ core/services/opfs/src/writer.rs | 79 +++++++++++++++++++++++++++++++ 7 files changed, 181 insertions(+), 11 deletions(-) create mode 100644 core/services/opfs/src/writer.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 2a5e9c7a9b99..98c7ff5ca7fa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { - "rust-analyzer.cargo.allTargets": true, + "rust-analyzer.cargo.target": "wasm32-unknown-unknown", //////////////// DO NOT MERGE + "rust-analyzer.cargo.allTargets": false, ///////// DO NOT MERGE "rust-analyzer.cargo.features": "all", "rust-analyzer.procMacro.ignored": { "napi-derive": [ diff --git a/core/edge/opfs_wasm32/src/lib.rs b/core/edge/opfs_wasm32/src/lib.rs index 391b9163e905..a7b65e984e14 100644 --- a/core/edge/opfs_wasm32/src/lib.rs +++ b/core/edge/opfs_wasm32/src/lib.rs @@ -37,15 +37,44 @@ mod tests { .finish() } + // #[wasm_bindgen_test] + // async fn test_get_directory_handle() { + // let op = new_operator(); + // op.create_dir("/dir/").await.expect("directory"); + // op.create_dir("/dir///").await.expect("directory"); + // op.create_dir("/dir:/").await.expect("directory"); + // op.create_dir("/dir<>/").await.expect("directory"); + // assert_eq!(op.create_dir("/a/b/../x/y/z/").await.unwrap_err().kind(), ErrorKind::Unexpected); + // // this works on Chrome, but fails on macOS + // // assert_eq!(op.create_dir("/dir\0/").await.unwrap_err().kind(), ErrorKind::Unexpected); + // } + #[wasm_bindgen_test] - async fn test_get_directory_handle() { + async fn test_write() { let op = new_operator(); - op.create_dir("/dir/").await.expect("directory"); - op.create_dir("/dir///").await.expect("directory"); - op.create_dir("/dir:/").await.expect("directory"); - op.create_dir("/dir<>/").await.expect("directory"); - assert_eq!(op.create_dir("/a/b/../x/y/z/").await.unwrap_err().kind(), ErrorKind::Unexpected); - // this works on Chrome, but fails on macOS - // assert_eq!(op.create_dir("/dir\0/").await.unwrap_err().kind(), ErrorKind::Unexpected); + + // this does not even go to OPFS backend, short-circuited + assert_eq!( + op.write("/", "should_not_work").await.unwrap_err().kind(), + ErrorKind::IsADirectory + ); + + { + let content = "Content of the file to write"; + let meta = op.write("/test_file", content).await.expect("write"); + console_log!("{:?}", meta); + assert_eq!(meta.content_length(), content.len() as u64); + assert!(meta.last_modified().is_some()); + } + + { + let mut w = op.writer("big_file").await.expect("writer"); + let chunk = vec![0u8; 1024 * 1024]; // 1MB + for _ in 0..1024 { + w.write(chunk.clone()).await.expect("write chunk"); + } + let meta = w.close().await.expect("close"); + assert_eq!(meta.content_length(), 1024 * 1024 * 1024); // 1GB + } } } diff --git a/core/services/opfs/src/backend.rs b/core/services/opfs/src/backend.rs index 73333d0c606f..624c6c4ab71e 100644 --- a/core/services/opfs/src/backend.rs +++ b/core/services/opfs/src/backend.rs @@ -17,9 +17,15 @@ use std::sync::Arc; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::JsFuture; +use web_sys::FileSystemWritableFileStream; + use super::OPFS_SCHEME; use super::config::OpfsConfig; +use super::error::*; use super::utils::*; +use super::writer::OpfsWriter; use opendal_core::raw::*; use opendal_core::*; @@ -44,7 +50,7 @@ pub struct OpfsBackend {} impl Access for OpfsBackend { type Reader = (); - type Writer = (); + type Writer = OpfsWriter; type Lister = (); @@ -57,6 +63,11 @@ impl Access for OpfsBackend { info.set_root("/"); info.set_native_capability(Capability { create_dir: true, + + write: true, + write_can_empty: true, + write_can_multi: true, + ..Default::default() }); Arc::new(info) @@ -67,4 +78,15 @@ impl Access for OpfsBackend { get_directory_handle(path, true).await?; Ok(RpCreateDir::default()) } + + async fn write(&self, path: &str, _args: OpWrite) -> Result<(RpWrite, Self::Writer)> { + let handle = get_file_handle(path, true).await?; + + let stream: FileSystemWritableFileStream = JsFuture::from(handle.create_writable()) + .await + .and_then(JsCast::dyn_into) + .map_err(parse_js_error)?; + + Ok((RpWrite::default(), OpfsWriter::new(stream))) + } } diff --git a/core/services/opfs/src/error.rs b/core/services/opfs/src/error.rs index 908e7a794cbf..355ac0e42660 100644 --- a/core/services/opfs/src/error.rs +++ b/core/services/opfs/src/error.rs @@ -29,8 +29,9 @@ pub(crate) fn parse_js_error(value: JsValue) -> Error { "NotFoundError" => ErrorKind::NotFound, "TypeMismatchError" => ErrorKind::NotFound, "NotAllowedError" => ErrorKind::PermissionDenied, + "QuotaExceededError" => ErrorKind::Unexpected, e => { - console_debug!("Got exception {e:?}"); + console_debug!("Got unhandled DOM exception {e:?}"); ErrorKind::Unexpected } }; diff --git a/core/services/opfs/src/lib.rs b/core/services/opfs/src/lib.rs index db4500c1b0ad..28b2b063d0ac 100644 --- a/core/services/opfs/src/lib.rs +++ b/core/services/opfs/src/lib.rs @@ -38,6 +38,7 @@ mod config; mod core; mod error; mod utils; +mod writer; pub use backend::OpfsBuilder as Opfs; pub use config::OpfsConfig; diff --git a/core/services/opfs/src/utils.rs b/core/services/opfs/src/utils.rs index 9d05ad968cb1..4916db8d86eb 100644 --- a/core/services/opfs/src/utils.rs +++ b/core/services/opfs/src/utils.rs @@ -19,7 +19,9 @@ use opendal_core::Result; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; use web_sys::FileSystemDirectoryHandle; +use web_sys::FileSystemFileHandle; use web_sys::FileSystemGetDirectoryOptions; +use web_sys::FileSystemGetFileOptions; use web_sys::window; use super::error::*; @@ -58,3 +60,38 @@ pub(crate) async fn get_directory_handle( Ok(handle) } + +/// Split a file path into its parent directory handle and filename. +/// +/// When `create` is true, intermediate directories are created as needed. +pub(crate) async fn get_parent_dir_and_name<'a>( + path: &'a str, + create: bool, +) -> Result<(FileSystemDirectoryHandle, &'a str)> { + let trimmed = path.trim_matches('/'); + match trimmed.rsplit_once('/') { + Some((parent, name)) => { + let dir = get_directory_handle(parent, create).await?; + Ok((dir, name)) + } + None => { + let root = get_root_directory_handle().await?; + Ok((root, trimmed)) + } + } +} + +/// Get a file handle by its full path. +/// +/// When `create` is true, intermediate directories and the file itself are created as needed. +pub(crate) async fn get_file_handle(path: &str, create: bool) -> Result { + let (dir, name) = get_parent_dir_and_name(path, create).await?; + + let opt = FileSystemGetFileOptions::new(); + opt.set_create(create); + + JsFuture::from(dir.get_file_handle_with_options(name, &opt)) + .await + .and_then(JsCast::dyn_into) + .map_err(parse_js_error) +} diff --git a/core/services/opfs/src/writer.rs b/core/services/opfs/src/writer.rs new file mode 100644 index 000000000000..fb672157d34b --- /dev/null +++ b/core/services/opfs/src/writer.rs @@ -0,0 +1,79 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use wasm_bindgen_futures::JsFuture; +use web_sys::FileSystemWritableFileStream; + +use opendal_core::raw::*; +use opendal_core::*; + +use super::error::*; + +pub struct OpfsWriter { + stream: FileSystemWritableFileStream, + bytes_written: u64, +} + +/// Safety: wasm32 is single-threaded, `Send` and `Sync` are meaningless. +unsafe impl Send for OpfsWriter {} +/// Safety: wasm32 is single-threaded, `Send` and `Sync` are meaningless. +unsafe impl Sync for OpfsWriter {} + +impl OpfsWriter { + pub fn new(stream: FileSystemWritableFileStream) -> Self { + Self { + stream, + bytes_written: 0, + } + } +} + +impl oio::Write for OpfsWriter { + async fn write(&mut self, bs: Buffer) -> Result<()> { + console_debug!("write!!!!!!!!!!!!!!!!!!!!!"); + let bytes = bs.to_bytes(); + JsFuture::from( + self.stream + .write_with_u8_array(&bytes) + .map_err(parse_js_error)?, + ) + .await + .map_err(parse_js_error)?; + + self.bytes_written += bytes.len() as u64; + Ok(()) + } + + async fn close(&mut self) -> Result { + console_debug!("close!!!!!!!!!!!!!!!!!!!!!"); + JsFuture::from(self.stream.close()) + .await + .map_err(parse_js_error)?; + + let mut meta = Metadata::new(EntryMode::FILE); + meta.set_content_length(self.bytes_written); + Ok(meta) + } + + async fn abort(&mut self) -> Result<()> { + console_debug!("abort!!!!!!!!!!!!!!!!!!!!!"); + JsFuture::from(self.stream.abort()) + .await + .map_err(parse_js_error)?; + Ok(()) + } +} From fc1844f51246223bb40803e083622cf5dbfbe47f Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Sun, 22 Feb 2026 11:07:51 +0000 Subject: [PATCH 09/29] wip write --- core/edge/opfs_wasm32/src/lib.rs | 70 +++++++++++++++++++++++++------- core/services/opfs/src/writer.rs | 1 + 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/core/edge/opfs_wasm32/src/lib.rs b/core/edge/opfs_wasm32/src/lib.rs index a7b65e984e14..577e7a618001 100644 --- a/core/edge/opfs_wasm32/src/lib.rs +++ b/core/edge/opfs_wasm32/src/lib.rs @@ -58,23 +58,65 @@ mod tests { op.write("/", "should_not_work").await.unwrap_err().kind(), ErrorKind::IsADirectory ); + } - { - let content = "Content of the file to write"; - let meta = op.write("/test_file", content).await.expect("write"); - console_log!("{:?}", meta); - assert_eq!(meta.content_length(), content.len() as u64); - assert!(meta.last_modified().is_some()); - } + #[wasm_bindgen_test] + async fn test_write_simple() { + let op = new_operator(); + let content = "Content of the file to write"; + let meta = op.write("/test_file", content).await.expect("write"); + console_log!("{:?}", meta); + assert_eq!(meta.content_length(), content.len() as u64); + + // This is None - we have to use stat + assert!(meta.last_modified().is_none()); + } + + #[wasm_bindgen_test] + async fn test_write_write_twice_same_file() { + let op = new_operator(); + let content = "Content of the file to write"; + let meta = op.write("/test_file", content).await.expect("write"); + let meta = op.write("/test_file", content).await.expect("write"); + assert_eq!(meta.content_length(), content.len() as u64); + assert!(meta.last_modified().is_none()); + } + + #[wasm_bindgen_test] + async fn test_write_like_append_three_times() { + let op = new_operator(); + let content = "Content of the file to write"; + let mut w = op.writer("/test_file_writte_twice").await.expect("writer"); + w.write(content).await.expect("write"); + w.write(content).await.expect("write"); + w.write(content).await.expect("write"); + let meta = w.close().await.expect("close"); + assert_eq!(meta.content_length(), (content.len() as u64) * 3); + assert!(meta.last_modified().is_none()); + } - { - let mut w = op.writer("big_file").await.expect("writer"); - let chunk = vec![0u8; 1024 * 1024]; // 1MB - for _ in 0..1024 { - w.write(chunk.clone()).await.expect("write chunk"); + #[wasm_bindgen_test] + async fn test_write_large_file_quota() { + // you can simulate a lower disk space in Chrome + let op = new_operator(); + let mut w = op.writer("big_file").await.expect("writer"); + let chunk = vec![0u8; 1024 * 1024]; // 1MB + for _ in 0..1024 { + let res = w.write(chunk.clone()).await; + match res { + Ok(()) => (), + Err(e) => { + // OPFS filled up (you can simulate this in Chrome by setting a lower limit) + // parse_js_error: JsValue(TypeError: Cannot close a ERRORED writable stream + // TypeError: Cannot close a ERRORED writable stream + console_log!("got {e:?}"); + console_log!("message = {}", e.message()); + assert_eq!(e.kind(), ErrorKind::Unexpected); + return; + } } - let meta = w.close().await.expect("close"); - assert_eq!(meta.content_length(), 1024 * 1024 * 1024); // 1GB } + let meta = w.close().await.expect("close"); + assert_eq!(meta.content_length(), 1024 * 1024 * 1024); // 1GB } } diff --git a/core/services/opfs/src/writer.rs b/core/services/opfs/src/writer.rs index fb672157d34b..d5bc2edd8931 100644 --- a/core/services/opfs/src/writer.rs +++ b/core/services/opfs/src/writer.rs @@ -64,6 +64,7 @@ impl oio::Write for OpfsWriter { .await .map_err(parse_js_error)?; + // We cannot set LastModified here - stream does not have such metadata let mut meta = Metadata::new(EntryMode::FILE); meta.set_content_length(self.bytes_written); Ok(meta) From e15a94588124302d9a5a996f5e2b7cd09d295860 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Sun, 22 Feb 2026 11:09:37 +0000 Subject: [PATCH 10/29] uncomment test --- core/edge/opfs_wasm32/src/lib.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/core/edge/opfs_wasm32/src/lib.rs b/core/edge/opfs_wasm32/src/lib.rs index 577e7a618001..c29835d36523 100644 --- a/core/edge/opfs_wasm32/src/lib.rs +++ b/core/edge/opfs_wasm32/src/lib.rs @@ -37,17 +37,17 @@ mod tests { .finish() } - // #[wasm_bindgen_test] - // async fn test_get_directory_handle() { - // let op = new_operator(); - // op.create_dir("/dir/").await.expect("directory"); - // op.create_dir("/dir///").await.expect("directory"); - // op.create_dir("/dir:/").await.expect("directory"); - // op.create_dir("/dir<>/").await.expect("directory"); - // assert_eq!(op.create_dir("/a/b/../x/y/z/").await.unwrap_err().kind(), ErrorKind::Unexpected); - // // this works on Chrome, but fails on macOS - // // assert_eq!(op.create_dir("/dir\0/").await.unwrap_err().kind(), ErrorKind::Unexpected); - // } + #[wasm_bindgen_test] + async fn test_get_directory_handle() { + let op = new_operator(); + op.create_dir("/dir/").await.expect("directory"); + op.create_dir("/dir///").await.expect("directory"); + op.create_dir("/dir:/").await.expect("directory"); + op.create_dir("/dir<>/").await.expect("directory"); + assert_eq!(op.create_dir("/a/b/../x/y/z/").await.unwrap_err().kind(), ErrorKind::Unexpected); + // this works on Chrome, but fails on macOS + // assert_eq!(op.create_dir("/dir\0/").await.unwrap_err().kind(), ErrorKind::Unexpected); + } #[wasm_bindgen_test] async fn test_write() { From 225ae90664ea45ecf0f2ca66974f8defae796330 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Sun, 22 Feb 2026 19:00:03 +0000 Subject: [PATCH 11/29] Add console feature to web-sys dep in Cargo.toml --- core/services/opfs/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/core/services/opfs/Cargo.toml b/core/services/opfs/Cargo.toml index 872dbd09a0a3..5dd4e755ca4f 100644 --- a/core/services/opfs/Cargo.toml +++ b/core/services/opfs/Cargo.toml @@ -37,6 +37,7 @@ serde = { workspace = true, features = ["derive"] } wasm-bindgen = "0.2.100" wasm-bindgen-futures = "0.4.50" web-sys = { version = "0.3.77", features = [ + "console", "DomException", "File", "FileSystemDirectoryHandle", From e91cd21dc11bf0757976b31454478db2e03fa615 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Sun, 22 Feb 2026 19:18:10 +0000 Subject: [PATCH 12/29] Add root in OpfsConfig --- core/edge/opfs_wasm32/src/lib.rs | 19 +++++++++++++++++-- core/services/opfs/src/backend.rs | 23 +++++++++++++++++++---- core/services/opfs/src/config.rs | 9 +++++++-- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/core/edge/opfs_wasm32/src/lib.rs b/core/edge/opfs_wasm32/src/lib.rs index c29835d36523..99197ee95c2b 100644 --- a/core/edge/opfs_wasm32/src/lib.rs +++ b/core/edge/opfs_wasm32/src/lib.rs @@ -19,6 +19,7 @@ mod tests { use opendal::ErrorKind; use opendal::Operator; + use opendal::services::Opfs; use opendal::services::OpfsConfig; use wasm_bindgen_test::wasm_bindgen_test; use wasm_bindgen_test::wasm_bindgen_test_configure; @@ -38,17 +39,31 @@ mod tests { } #[wasm_bindgen_test] - async fn test_get_directory_handle() { + async fn test_create_directory_handle() { let op = new_operator(); op.create_dir("/dir/").await.expect("directory"); op.create_dir("/dir///").await.expect("directory"); op.create_dir("/dir:/").await.expect("directory"); op.create_dir("/dir<>/").await.expect("directory"); - assert_eq!(op.create_dir("/a/b/../x/y/z/").await.unwrap_err().kind(), ErrorKind::Unexpected); + assert_eq!( + op.create_dir("/a/b/../x/y/z/").await.unwrap_err().kind(), + ErrorKind::Unexpected + ); // this works on Chrome, but fails on macOS // assert_eq!(op.create_dir("/dir\0/").await.unwrap_err().kind(), ErrorKind::Unexpected); } + #[wasm_bindgen_test] + async fn test_create_directory_handle_with_root() { + // let op = Operator::new(Opfs::default()).expect("REASON")..finish(); + let op = Operator::new(Opfs::default().root("/myapp")) + .expect("config") + .finish(); + op.write("somefile", "content") + .await + .expect("write under root"); + } + #[wasm_bindgen_test] async fn test_write() { let op = new_operator(); diff --git a/core/services/opfs/src/backend.rs b/core/services/opfs/src/backend.rs index 624c6c4ab71e..331e21fc65c4 100644 --- a/core/services/opfs/src/backend.rs +++ b/core/services/opfs/src/backend.rs @@ -35,17 +35,32 @@ pub struct OpfsBuilder { pub(super) config: OpfsConfig, } +impl OpfsBuilder { + /// Set root directory for this backend. + pub fn root(mut self, root: &str) -> Self { + self.config.root = if root.is_empty() { + None + } else { + Some(root.to_string()) + }; + self + } +} + impl Builder for OpfsBuilder { type Config = OpfsConfig; fn build(self) -> Result { - Ok(OpfsBackend {}) + let root = normalize_root(&self.config.root.unwrap_or_default()); + Ok(OpfsBackend { root }) } } /// OPFS Service backend -#[derive(Default, Debug, Clone)] -pub struct OpfsBackend {} +#[derive(Debug, Clone)] +pub struct OpfsBackend { + root: String, +} impl Access for OpfsBackend { type Reader = (); @@ -60,7 +75,7 @@ impl Access for OpfsBackend { let info = AccessorInfo::default(); info.set_scheme(OPFS_SCHEME); info.set_name("opfs"); - info.set_root("/"); + info.set_root(&self.root); info.set_native_capability(Capability { create_dir: true, diff --git a/core/services/opfs/src/config.rs b/core/services/opfs/src/config.rs index 38bd72c21be7..1201dcb00a5d 100644 --- a/core/services/opfs/src/config.rs +++ b/core/services/opfs/src/config.rs @@ -26,11 +26,16 @@ use super::backend::OpfsBuilder; #[derive(Default, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(default)] #[non_exhaustive] -pub struct OpfsConfig {} +pub struct OpfsConfig { + /// Root directory for this backend. + pub root: Option, +} impl Debug for OpfsConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("OpfsConfig").finish_non_exhaustive() + f.debug_struct("OpfsConfig") + .field("root", &self.root) + .finish_non_exhaustive() } } From 73bbc013914851ffe86472056edf34c45d73de94 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Sun, 22 Feb 2026 20:01:30 +0000 Subject: [PATCH 13/29] Add stat, fix test, fix root --- core/edge/opfs_wasm32/src/lib.rs | 46 ++++++++++++++++++++++++++----- core/services/opfs/src/backend.rs | 37 +++++++++++++++++++++++-- core/services/opfs/src/lib.rs | 1 - 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/core/edge/opfs_wasm32/src/lib.rs b/core/edge/opfs_wasm32/src/lib.rs index 99197ee95c2b..0b5ad8ebe617 100644 --- a/core/edge/opfs_wasm32/src/lib.rs +++ b/core/edge/opfs_wasm32/src/lib.rs @@ -17,6 +17,7 @@ #[cfg(test)] mod tests { + use opendal::EntryMode; use opendal::ErrorKind; use opendal::Operator; use opendal::services::Opfs; @@ -56,12 +57,27 @@ mod tests { #[wasm_bindgen_test] async fn test_create_directory_handle_with_root() { // let op = Operator::new(Opfs::default()).expect("REASON")..finish(); - let op = Operator::new(Opfs::default().root("/myapp")) + let op_rooted = Operator::new(Opfs::default().root("/myapp/subdir1/subdir2/")) .expect("config") .finish(); - op.write("somefile", "content") + op_rooted + .write("subdir3/somefile", "content") .await .expect("write under root"); + + let stat_rooted = op_rooted.stat("subdir3/somefile").await.expect("stat"); + + let op = new_operator(); + let stat = op + .stat("/myapp/subdir1/subdir2/subdir3/somefile") + .await + .expect("stat"); + assert_eq!(stat_rooted, stat_rooted); + let stat = op + .stat("myapp/subdir1/subdir2/subdir3/somefile") + .await + .expect("stat"); + assert_eq!(stat_rooted, stat_rooted); } #[wasm_bindgen_test] @@ -79,12 +95,19 @@ mod tests { async fn test_write_simple() { let op = new_operator(); let content = "Content of the file to write"; - let meta = op.write("/test_file", content).await.expect("write"); + let path = "/test_file"; + let meta = op.write(path, content).await.expect("write"); console_log!("{:?}", meta); assert_eq!(meta.content_length(), content.len() as u64); // This is None - we have to use stat assert!(meta.last_modified().is_none()); + + let stat = op.stat(path).await.expect("stat"); + console_log!("stat = {:?}", stat); + assert_eq!(stat.mode(), EntryMode::FILE); + assert_eq!(stat.content_length(), content.len() as u64); + assert!(stat.last_modified().is_some()); } #[wasm_bindgen_test] @@ -101,20 +124,25 @@ mod tests { async fn test_write_like_append_three_times() { let op = new_operator(); let content = "Content of the file to write"; - let mut w = op.writer("/test_file_writte_twice").await.expect("writer"); + let path = "/test_file_write_multiple"; + let mut w = op.writer(path).await.expect("writer"); w.write(content).await.expect("write"); w.write(content).await.expect("write"); w.write(content).await.expect("write"); let meta = w.close().await.expect("close"); - assert_eq!(meta.content_length(), (content.len() as u64) * 3); + let expected_file_size = (content.len() as u64) * 3; + assert_eq!(meta.content_length(), expected_file_size); assert!(meta.last_modified().is_none()); + let stat = op.stat(path).await.expect("stat"); + assert_eq!(stat.content_length(), expected_file_size) } #[wasm_bindgen_test] async fn test_write_large_file_quota() { // you can simulate a lower disk space in Chrome let op = new_operator(); - let mut w = op.writer("big_file").await.expect("writer"); + let path = "big_file"; + let mut w = op.writer(path).await.expect("writer"); let chunk = vec![0u8; 1024 * 1024]; // 1MB for _ in 0..1024 { let res = w.write(chunk.clone()).await; @@ -131,7 +159,11 @@ mod tests { } } } + + let expected_file_size = 1024 * 1024 * 1024; // 1GB let meta = w.close().await.expect("close"); - assert_eq!(meta.content_length(), 1024 * 1024 * 1024); // 1GB + assert_eq!(meta.content_length(), expected_file_size); + let stat = op.stat(path).await.expect("stat"); + assert_eq!(stat.content_length(), expected_file_size) } } diff --git a/core/services/opfs/src/backend.rs b/core/services/opfs/src/backend.rs index 331e21fc65c4..e4bdc1898a2f 100644 --- a/core/services/opfs/src/backend.rs +++ b/core/services/opfs/src/backend.rs @@ -19,6 +19,7 @@ use std::sync::Arc; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; +use web_sys::File; use web_sys::FileSystemWritableFileStream; use super::OPFS_SCHEME; @@ -77,6 +78,8 @@ impl Access for OpfsBackend { info.set_name("opfs"); info.set_root(&self.root); info.set_native_capability(Capability { + stat: true, + create_dir: true, write: true, @@ -88,15 +91,43 @@ impl Access for OpfsBackend { Arc::new(info) } + async fn stat(&self, path: &str, _args: OpStat) -> Result { + let p = build_abs_path(&self.root, path); + + if p.ends_with('/') { + get_directory_handle(&p, false).await?; + return Ok(RpStat::new(Metadata::new(EntryMode::DIR))); + } + + // File: get metadata via getFile(). + let handle = get_file_handle(&p, false).await?; + let file: File = JsFuture::from(handle.get_file()) + .await + .and_then(JsCast::dyn_into) + .map_err(parse_js_error)?; + + let mut meta = Metadata::new(EntryMode::FILE); + meta.set_content_length(file.size() as u64); + if let Ok(t) = Timestamp::from_millisecond(file.last_modified() as i64) { + meta.set_last_modified(t); + } + + Ok(RpStat::new(meta)) + } + async fn create_dir(&self, path: &str, _: OpCreateDir) -> Result { debug_assert!(path != "/", "root path should be handled upstream"); - get_directory_handle(path, true).await?; + let p = build_abs_path(&self.root, path); + get_directory_handle(&p, true).await?; + Ok(RpCreateDir::default()) } async fn write(&self, path: &str, _args: OpWrite) -> Result<(RpWrite, Self::Writer)> { - let handle = get_file_handle(path, true).await?; - + let p = build_abs_path(&self.root, path); + let handle = get_file_handle(&p, true).await?; + console_debug!("write: handle = {:?}", handle); + console_debug!("write: path = {:?}", p); let stream: FileSystemWritableFileStream = JsFuture::from(handle.create_writable()) .await .and_then(JsCast::dyn_into) diff --git a/core/services/opfs/src/lib.rs b/core/services/opfs/src/lib.rs index 28b2b063d0ac..c1ba2d2eeb9d 100644 --- a/core/services/opfs/src/lib.rs +++ b/core/services/opfs/src/lib.rs @@ -32,7 +32,6 @@ macro_rules! console_debug { }; } - mod backend; mod config; mod core; From 1c1d11a1ce3d7e644a910cf7f21e0be5eff65d05 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Sun, 22 Feb 2026 20:04:53 +0000 Subject: [PATCH 14/29] revert vs code settings.json --- .vscode/settings.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 98c7ff5ca7fa..2a5e9c7a9b99 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,5 @@ { - "rust-analyzer.cargo.target": "wasm32-unknown-unknown", //////////////// DO NOT MERGE - "rust-analyzer.cargo.allTargets": false, ///////// DO NOT MERGE + "rust-analyzer.cargo.allTargets": true, "rust-analyzer.cargo.features": "all", "rust-analyzer.procMacro.ignored": { "napi-derive": [ From e68f15f09c99e309c9b18e9f7e784472029b165d Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Sun, 22 Feb 2026 23:49:00 +0000 Subject: [PATCH 15/29] Add reader --- core/edge/opfs_wasm32/src/lib.rs | 57 ++++++++++++++++------ core/services/opfs/Cargo.toml | 1 + core/services/opfs/src/backend.rs | 12 ++++- core/services/opfs/src/lib.rs | 1 + core/services/opfs/src/reader.rs | 79 +++++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 16 deletions(-) create mode 100644 core/services/opfs/src/reader.rs diff --git a/core/edge/opfs_wasm32/src/lib.rs b/core/edge/opfs_wasm32/src/lib.rs index 0b5ad8ebe617..7433635e2ebf 100644 --- a/core/edge/opfs_wasm32/src/lib.rs +++ b/core/edge/opfs_wasm32/src/lib.rs @@ -92,22 +92,32 @@ mod tests { } #[wasm_bindgen_test] - async fn test_write_simple() { - let op = new_operator(); - let content = "Content of the file to write"; + async fn test_write_read_simple() { let path = "/test_file"; - let meta = op.write(path, content).await.expect("write"); - console_log!("{:?}", meta); - assert_eq!(meta.content_length(), content.len() as u64); - - // This is None - we have to use stat - assert!(meta.last_modified().is_none()); + let content = "Content of the file to write"; + { + let op = new_operator(); + let meta = op.write(path, content).await.expect("write"); + console_log!("{:?}", meta); + assert_eq!(meta.content_length(), content.len() as u64); + + // This is None - we have to use stat + assert!(meta.last_modified().is_none()); + + let stat = op.stat(path).await.expect("stat"); + console_log!("stat = {:?}", stat); + assert_eq!(stat.mode(), EntryMode::FILE); + assert_eq!(stat.content_length(), content.len() as u64); + assert!(stat.last_modified().is_some()); + } - let stat = op.stat(path).await.expect("stat"); - console_log!("stat = {:?}", stat); - assert_eq!(stat.mode(), EntryMode::FILE); - assert_eq!(stat.content_length(), content.len() as u64); - assert!(stat.last_modified().is_some()); + { + // read back and compare + let op = new_operator(); + let buffer = op.read(path).await.expect("read"); + console_log!("read = {:?}", buffer); + assert_eq!(buffer.to_bytes(), content.as_bytes()); + } } #[wasm_bindgen_test] @@ -164,6 +174,23 @@ mod tests { let meta = w.close().await.expect("close"); assert_eq!(meta.content_length(), expected_file_size); let stat = op.stat(path).await.expect("stat"); - assert_eq!(stat.content_length(), expected_file_size) + assert_eq!(stat.content_length(), expected_file_size); + + { + // read and compare + let op = new_operator(); + let buffer = op.read(path).await.expect("read"); + assert_eq!(buffer.to_bytes().len(), expected_file_size as usize); + } + } + + #[wasm_bindgen_test] + async fn test_write_and_read_with_range() { + let op = new_operator(); + let path = "numbers.txt"; + let content = "0123456789"; + let meta = op.write(path, content).await.expect("write"); + let buffer = op.read_with(path).range(3..5).await.expect("read"); + assert_eq!(buffer.to_bytes(), "34".as_bytes()); } } diff --git a/core/services/opfs/Cargo.toml b/core/services/opfs/Cargo.toml index 5dd4e755ca4f..ad9f437d64da 100644 --- a/core/services/opfs/Cargo.toml +++ b/core/services/opfs/Cargo.toml @@ -37,6 +37,7 @@ serde = { workspace = true, features = ["derive"] } wasm-bindgen = "0.2.100" wasm-bindgen-futures = "0.4.50" web-sys = { version = "0.3.77", features = [ + "Blob", "console", "DomException", "File", diff --git a/core/services/opfs/src/backend.rs b/core/services/opfs/src/backend.rs index e4bdc1898a2f..9f892d941bbe 100644 --- a/core/services/opfs/src/backend.rs +++ b/core/services/opfs/src/backend.rs @@ -25,6 +25,7 @@ use web_sys::FileSystemWritableFileStream; use super::OPFS_SCHEME; use super::config::OpfsConfig; use super::error::*; +use super::reader::OpfsReader; use super::utils::*; use super::writer::OpfsWriter; use opendal_core::raw::*; @@ -64,7 +65,7 @@ pub struct OpfsBackend { } impl Access for OpfsBackend { - type Reader = (); + type Reader = OpfsReader; type Writer = OpfsWriter; @@ -80,6 +81,8 @@ impl Access for OpfsBackend { info.set_native_capability(Capability { stat: true, + read: true, + create_dir: true, write: true, @@ -115,6 +118,13 @@ impl Access for OpfsBackend { Ok(RpStat::new(meta)) } + async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::Reader)> { + let p = build_abs_path(&self.root, path); + let handle = get_file_handle(&p, false).await?; + + Ok((RpRead::new(), OpfsReader::new(handle, args.range()))) + } + async fn create_dir(&self, path: &str, _: OpCreateDir) -> Result { debug_assert!(path != "/", "root path should be handled upstream"); let p = build_abs_path(&self.root, path); diff --git a/core/services/opfs/src/lib.rs b/core/services/opfs/src/lib.rs index c1ba2d2eeb9d..8a9177d65cea 100644 --- a/core/services/opfs/src/lib.rs +++ b/core/services/opfs/src/lib.rs @@ -36,6 +36,7 @@ mod backend; mod config; mod core; mod error; +mod reader; mod utils; mod writer; diff --git a/core/services/opfs/src/reader.rs b/core/services/opfs/src/reader.rs new file mode 100644 index 000000000000..eb3a3ea96ffa --- /dev/null +++ b/core/services/opfs/src/reader.rs @@ -0,0 +1,79 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::JsFuture; +use web_sys::FileSystemFileHandle; + +use super::error::*; +use opendal_core::raw::*; +use opendal_core::*; + +pub struct OpfsReader { + handle: FileSystemFileHandle, + range: BytesRange, + done: bool, +} + +/// Safety: wasm32 is single-threaded, `Send` and `Sync` are meaningless. +unsafe impl Send for OpfsReader {} +/// Safety: wasm32 is single-threaded, `Send` and `Sync` are meaningless. +unsafe impl Sync for OpfsReader {} + +impl OpfsReader { + pub fn new(handle: FileSystemFileHandle, range: BytesRange) -> Self { + Self { + handle, + range, + done: false, + } + } +} + +impl oio::Read for OpfsReader { + async fn read(&mut self) -> Result { + if self.done { + return Ok(Buffer::new()); + } + self.done = true; + + let file: web_sys::File = JsFuture::from(self.handle.get_file()) + .await + .and_then(JsCast::dyn_into) + .map_err(parse_js_error)?; + + let blob: &web_sys::Blob = file.as_ref(); + let blob = if self.range.is_full() { + blob.clone() + } else { + let offset = self.range.offset() as f64; + let end = match self.range.size() { + Some(size) => offset + size as f64, + None => blob.size(), + }; + blob.slice_with_f64_and_f64(offset, end) + .map_err(parse_js_error)? + }; + + let array_buffer = JsFuture::from(blob.array_buffer()) + .await + .map_err(parse_js_error)?; + let uint8_array = js_sys::Uint8Array::new(&array_buffer); + + Ok(Buffer::from(uint8_array.to_vec())) + } +} From 4faf73f1dd90d8fe5c7d6ee454a51e6d874b4fff Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Mon, 23 Feb 2026 00:33:11 +0000 Subject: [PATCH 16/29] Use SendWrapper --- core/Cargo.lock | 1 + core/services/opfs/Cargo.toml | 1 + core/services/opfs/src/reader.rs | 10 +++------- core/services/opfs/src/writer.rs | 10 +++------- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/core/Cargo.lock b/core/Cargo.lock index 07c39fc24e09..953040778991 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -6874,6 +6874,7 @@ version = "0.55.0" dependencies = [ "js-sys", "opendal-core", + "send_wrapper", "serde", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/core/services/opfs/Cargo.toml b/core/services/opfs/Cargo.toml index ad9f437d64da..8207d57348fb 100644 --- a/core/services/opfs/Cargo.toml +++ b/core/services/opfs/Cargo.toml @@ -35,6 +35,7 @@ js-sys = "0.3.77" opendal-core = { path = "../../core", version = "0.55.0", default-features = false } serde = { workspace = true, features = ["derive"] } wasm-bindgen = "0.2.100" +send_wrapper = "0.6" wasm-bindgen-futures = "0.4.50" web-sys = { version = "0.3.77", features = [ "Blob", diff --git a/core/services/opfs/src/reader.rs b/core/services/opfs/src/reader.rs index eb3a3ea96ffa..418a281afee6 100644 --- a/core/services/opfs/src/reader.rs +++ b/core/services/opfs/src/reader.rs @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +use send_wrapper::SendWrapper; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; use web_sys::FileSystemFileHandle; @@ -24,20 +25,15 @@ use opendal_core::raw::*; use opendal_core::*; pub struct OpfsReader { - handle: FileSystemFileHandle, + handle: SendWrapper, range: BytesRange, done: bool, } -/// Safety: wasm32 is single-threaded, `Send` and `Sync` are meaningless. -unsafe impl Send for OpfsReader {} -/// Safety: wasm32 is single-threaded, `Send` and `Sync` are meaningless. -unsafe impl Sync for OpfsReader {} - impl OpfsReader { pub fn new(handle: FileSystemFileHandle, range: BytesRange) -> Self { Self { - handle, + handle: SendWrapper::new(handle), range, done: false, } diff --git a/core/services/opfs/src/writer.rs b/core/services/opfs/src/writer.rs index d5bc2edd8931..7924559dba32 100644 --- a/core/services/opfs/src/writer.rs +++ b/core/services/opfs/src/writer.rs @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +use send_wrapper::SendWrapper; use wasm_bindgen_futures::JsFuture; use web_sys::FileSystemWritableFileStream; @@ -24,19 +25,14 @@ use opendal_core::*; use super::error::*; pub struct OpfsWriter { - stream: FileSystemWritableFileStream, + stream: SendWrapper, bytes_written: u64, } -/// Safety: wasm32 is single-threaded, `Send` and `Sync` are meaningless. -unsafe impl Send for OpfsWriter {} -/// Safety: wasm32 is single-threaded, `Send` and `Sync` are meaningless. -unsafe impl Sync for OpfsWriter {} - impl OpfsWriter { pub fn new(stream: FileSystemWritableFileStream) -> Self { Self { - stream, + stream: SendWrapper::new(stream), bytes_written: 0, } } From 5dd14516ffe287b1d4c25d6b497aee10d3669d9d Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Mon, 23 Feb 2026 00:20:23 +0000 Subject: [PATCH 17/29] wip list broken --- .vscode/settings.json | 11 +++++++++-- core/Cargo.lock | 1 + core/edge/opfs_wasm32/Cargo.toml | 1 + core/edge/opfs_wasm32/src/lib.rs | 8 +++++++- core/services/opfs/src/backend.rs | 12 +++++++++++- core/services/opfs/src/lib.rs | 1 + 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 2a5e9c7a9b99..51ed362921bd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,13 @@ { - "rust-analyzer.cargo.allTargets": true, - "rust-analyzer.cargo.features": "all", + // "rust-analyzer.cargo.allTargets": true, + // "rust-analyzer.cargo.features": "all", + "rust-analyzer.cargo.target": "wasm32-unknown-unknown", //////////////// DO NOT MERGE + "rust-analyzer.cargo.allTargets": false, ///////// DO NOT MERGE + // "rust-analyzer.cargo.features": "all", + // "rust-analyzer.cargo.features": ["opfs"], + "rust-analyzer.cargo.features": [ + "services-opfs" + ], "rust-analyzer.procMacro.ignored": { "napi-derive": [ "napi" diff --git a/core/Cargo.lock b/core/Cargo.lock index 953040778991..a5b1c8a10bc4 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -2803,6 +2803,7 @@ dependencies = [ name = "edge_test_opfs_wasm32" version = "0.55.0" dependencies = [ + "futures", "opendal", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/core/edge/opfs_wasm32/Cargo.toml b/core/edge/opfs_wasm32/Cargo.toml index 562b03431524..dfc2cffb0c98 100644 --- a/core/edge/opfs_wasm32/Cargo.toml +++ b/core/edge/opfs_wasm32/Cargo.toml @@ -34,6 +34,7 @@ opendal = { path = "../..", default-features = false, features = [ wasm-bindgen = "0.2.89" wasm-bindgen-futures = "0.4.39" web-sys = { version = "0.3.77", features = ["console"] } +futures = { workspace = true, features = ["std", "async-await"] } [dev-dependencies] wasm-bindgen-test = "0.3.41" diff --git a/core/edge/opfs_wasm32/src/lib.rs b/core/edge/opfs_wasm32/src/lib.rs index 7433635e2ebf..27ee4d4957a5 100644 --- a/core/edge/opfs_wasm32/src/lib.rs +++ b/core/edge/opfs_wasm32/src/lib.rs @@ -17,13 +17,14 @@ #[cfg(test)] mod tests { + use futures::TryStreamExt; use opendal::EntryMode; use opendal::ErrorKind; use opendal::Operator; use opendal::services::Opfs; use opendal::services::OpfsConfig; use wasm_bindgen_test::wasm_bindgen_test; - use wasm_bindgen_test::wasm_bindgen_test_configure; + use wasm_bindgen_test::wasm_bindgen_test_configure; // Required for Next() macro_rules! console_log { ($($arg:tt)*) => { @@ -128,6 +129,11 @@ mod tests { let meta = op.write("/test_file", content).await.expect("write"); assert_eq!(meta.content_length(), content.len() as u64); assert!(meta.last_modified().is_none()); + + let mut entries = op.lister("").await.expect("list"); + while let Some(entry) = entries.try_next().await.expect("next") { + console_log!("entry: {} {:?}", entry.path(), entry.metadata().mode()); + } } #[wasm_bindgen_test] diff --git a/core/services/opfs/src/backend.rs b/core/services/opfs/src/backend.rs index 9f892d941bbe..bec33e008c82 100644 --- a/core/services/opfs/src/backend.rs +++ b/core/services/opfs/src/backend.rs @@ -25,6 +25,7 @@ use web_sys::FileSystemWritableFileStream; use super::OPFS_SCHEME; use super::config::OpfsConfig; use super::error::*; +use super::lister::OpfsLister; use super::reader::OpfsReader; use super::utils::*; use super::writer::OpfsWriter; @@ -69,7 +70,7 @@ impl Access for OpfsBackend { type Writer = OpfsWriter; - type Lister = (); + type Lister = OpfsLister; type Deleter = (); @@ -83,6 +84,8 @@ impl Access for OpfsBackend { read: true, + list: true, + create_dir: true, write: true, @@ -125,6 +128,13 @@ impl Access for OpfsBackend { Ok((RpRead::new(), OpfsReader::new(handle, args.range()))) } + async fn list(&self, path: &str, _args: OpList) -> Result<(RpList, Self::Lister)> { + let p = build_abs_path(&self.root, path); + let dir = get_directory_handle(&p, false).await?; + + Ok((RpList::default(), OpfsLister::new(dir, path.to_string()))) + } + async fn create_dir(&self, path: &str, _: OpCreateDir) -> Result { debug_assert!(path != "/", "root path should be handled upstream"); let p = build_abs_path(&self.root, path); diff --git a/core/services/opfs/src/lib.rs b/core/services/opfs/src/lib.rs index 8a9177d65cea..b6aca7ec52e3 100644 --- a/core/services/opfs/src/lib.rs +++ b/core/services/opfs/src/lib.rs @@ -36,6 +36,7 @@ mod backend; mod config; mod core; mod error; +mod lister; mod reader; mod utils; mod writer; From 7f4ae14cb422eef5ad87a34aa931dba80ff53087 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Mon, 23 Feb 2026 00:59:27 +0000 Subject: [PATCH 18/29] Fix path in get_parent_dir_and_name --- core/edge/opfs_wasm32/src/lib.rs | 83 +++++++++++++++++++++----------- core/services/opfs/src/utils.rs | 7 ++- 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/core/edge/opfs_wasm32/src/lib.rs b/core/edge/opfs_wasm32/src/lib.rs index 27ee4d4957a5..c2201ac0ce32 100644 --- a/core/edge/opfs_wasm32/src/lib.rs +++ b/core/edge/opfs_wasm32/src/lib.rs @@ -56,29 +56,63 @@ mod tests { } #[wasm_bindgen_test] - async fn test_create_directory_handle_with_root() { - // let op = Operator::new(Opfs::default()).expect("REASON")..finish(); - let op_rooted = Operator::new(Opfs::default().root("/myapp/subdir1/subdir2/")) - .expect("config") - .finish(); - op_rooted - .write("subdir3/somefile", "content") - .await - .expect("write under root"); - - let stat_rooted = op_rooted.stat("subdir3/somefile").await.expect("stat"); + async fn test_create_directory_handle_with_root_and_list() { + { + // create files and dirs + let op_rooted = Operator::new(Opfs::default().root("/myapp/subdir1/subdir2/")) + .expect("config") + .finish(); + op_rooted + .write("subdir3/somefile", "content") + .await + .expect("write under root"); - let op = new_operator(); - let stat = op - .stat("/myapp/subdir1/subdir2/subdir3/somefile") - .await - .expect("stat"); - assert_eq!(stat_rooted, stat_rooted); - let stat = op - .stat("myapp/subdir1/subdir2/subdir3/somefile") - .await - .expect("stat"); - assert_eq!(stat_rooted, stat_rooted); + let stat_rooted = op_rooted.stat("subdir3/somefile").await.expect("stat"); + + let op = new_operator(); + let stat = op + .stat("/myapp/subdir1/subdir2/subdir3/somefile") + .await + .expect("stat"); + assert_eq!(stat_rooted, stat_rooted); + let stat = op + .stat("myapp/subdir1/subdir2/subdir3/somefile") + .await + .expect("stat"); + assert_eq!(stat_rooted, stat_rooted); + } + + { + // simple list + let op = new_operator(); + let mut entries = op.lister("").await.expect("list"); + while let Some(entry) = entries.try_next().await.expect("next") { + console_log!("entry: {} {:?}", entry.path(), entry.metadata().mode()); + } + } + + // list test added here so we are sure there are dirs and files to list + { + // simple list + let op = new_operator(); + let mut entries = op.lister("myapp/").await.expect("list"); + while let Some(entry) = entries.try_next().await.expect("next") { + console_log!("entry: {} {:?}", entry.path(), entry.metadata().mode()); + } + } + + { + // recursive list + let op = new_operator(); + let mut entries = op + .lister_with("") + .recursive(true) + .await + .expect("recursive list"); + while let Some(entry) = entries.try_next().await.expect("next") { + console_log!("rec entry: {} {:?}", entry.path(), entry.metadata().mode()); + } + } } #[wasm_bindgen_test] @@ -129,11 +163,6 @@ mod tests { let meta = op.write("/test_file", content).await.expect("write"); assert_eq!(meta.content_length(), content.len() as u64); assert!(meta.last_modified().is_none()); - - let mut entries = op.lister("").await.expect("list"); - while let Some(entry) = entries.try_next().await.expect("next") { - console_log!("entry: {} {:?}", entry.path(), entry.metadata().mode()); - } } #[wasm_bindgen_test] diff --git a/core/services/opfs/src/utils.rs b/core/services/opfs/src/utils.rs index 4916db8d86eb..d45685898ffd 100644 --- a/core/services/opfs/src/utils.rs +++ b/core/services/opfs/src/utils.rs @@ -49,9 +49,12 @@ pub(crate) async fn get_directory_handle( let opt = FileSystemGetDirectoryOptions::new(); opt.set_create(create); - let dirs = path.trim_matches('/').split('/'); + let trimmed = path.trim_matches('/'); let mut handle = get_root_directory_handle().await?; - for dir in dirs { + if trimmed.is_empty() { + return Ok(handle); + } + for dir in trimmed.split('/') { handle = JsFuture::from(handle.get_directory_handle_with_options(dir, &opt)) .await .and_then(JsCast::dyn_into) From 14485e130a8a0d43d30a93e3783047e0b751cead Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Mon, 23 Feb 2026 01:05:05 +0000 Subject: [PATCH 19/29] Use write_with_write_params (due to Safari) --- core/services/opfs/Cargo.toml | 2 ++ core/services/opfs/src/writer.rs | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/core/services/opfs/Cargo.toml b/core/services/opfs/Cargo.toml index 8207d57348fb..d203006fd2ed 100644 --- a/core/services/opfs/Cargo.toml +++ b/core/services/opfs/Cargo.toml @@ -48,6 +48,8 @@ web-sys = { version = "0.3.77", features = [ "FileSystemGetFileOptions", "FileSystemWritableFileStream", "Navigator", + "WriteCommandType", + "WriteParams", "StorageManager", "Window", ] } diff --git a/core/services/opfs/src/writer.rs b/core/services/opfs/src/writer.rs index 7924559dba32..068fcd8e7041 100644 --- a/core/services/opfs/src/writer.rs +++ b/core/services/opfs/src/writer.rs @@ -16,8 +16,11 @@ // under the License. use send_wrapper::SendWrapper; +use wasm_bindgen::JsValue; use wasm_bindgen_futures::JsFuture; use web_sys::FileSystemWritableFileStream; +use web_sys::WriteCommandType; +use web_sys::WriteParams; use opendal_core::raw::*; use opendal_core::*; @@ -42,9 +45,13 @@ impl oio::Write for OpfsWriter { async fn write(&mut self, bs: Buffer) -> Result<()> { console_debug!("write!!!!!!!!!!!!!!!!!!!!!"); let bytes = bs.to_bytes(); + let params = WriteParams::new(WriteCommandType::Write); + params.set_size(Some(bytes.len() as f64)); + let data: JsValue = js_sys::Uint8Array::from(bytes.as_ref()).into(); + params.set_data(&data); JsFuture::from( self.stream - .write_with_u8_array(&bytes) + .write_with_write_params(¶ms.into()) .map_err(parse_js_error)?, ) .await From 467faed74a5146e5c1ff8b7941b94029c37a45ef Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Mon, 23 Feb 2026 01:06:26 +0000 Subject: [PATCH 20/29] add lister.rs file --- core/services/opfs/src/lister.rs | 81 ++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 core/services/opfs/src/lister.rs diff --git a/core/services/opfs/src/lister.rs b/core/services/opfs/src/lister.rs new file mode 100644 index 000000000000..b7a72ed2d042 --- /dev/null +++ b/core/services/opfs/src/lister.rs @@ -0,0 +1,81 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use send_wrapper::SendWrapper; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; +use web_sys::FileSystemDirectoryHandle; + +use super::error::*; +use opendal_core::raw::*; +use opendal_core::*; + +pub struct OpfsLister { + iter: SendWrapper, + path: String, +} + +impl OpfsLister { + pub fn new(dir: FileSystemDirectoryHandle, path: String) -> Self { + // Entry paths must not start with '/'. + // For root listing, path is "/" — normalize to "". + let path = if path == "/" { String::new() } else { path }; + Self { + iter: SendWrapper::new(dir.entries()), + path, + } + } +} + +impl oio::List for OpfsLister { + async fn next(&mut self) -> Result> { + let result = JsFuture::from( + self.iter + .next() + .map_err(parse_js_error)?, + ) + .await + .map_err(parse_js_error)?; + + let done = js_sys::Reflect::get(&result, &"done".into()) + .unwrap_or(JsValue::TRUE) + .as_bool() + .unwrap_or(true); + if done { + return Ok(None); + } + + let value = js_sys::Reflect::get(&result, &"value".into()) + .map_err(parse_js_error)?; + let pair: js_sys::Array = value.unchecked_into(); + let name = pair.get(0).as_string().unwrap_or_default(); + let handle = pair.get(1); + + let kind = js_sys::Reflect::get(&handle, &"kind".into()) + .ok() + .and_then(|v| v.as_string()) + .unwrap_or_default(); + + let (entry_path, mode) = if kind == "directory" { + (format!("{}{}/", self.path, name), EntryMode::DIR) + } else { + (format!("{}{}", self.path, name), EntryMode::FILE) + }; + + Ok(Some(oio::Entry::new(&entry_path, Metadata::new(mode)))) + } +} From e72e16621de229d593009c7c580c81dc0cd87c25 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Mon, 23 Feb 2026 01:34:29 +0000 Subject: [PATCH 21/29] delete --- core/edge/opfs_wasm32/src/lib.rs | 12 ++++-- core/services/opfs/Cargo.toml | 1 + core/services/opfs/src/backend.rs | 48 +++++++++-------------- core/services/opfs/src/core.rs | 43 +++++++++++++++++++++ core/services/opfs/src/deleter.rs | 64 +++++++++++++++++++++++++++++++ core/services/opfs/src/lib.rs | 1 + 6 files changed, 137 insertions(+), 32 deletions(-) create mode 100644 core/services/opfs/src/deleter.rs diff --git a/core/edge/opfs_wasm32/src/lib.rs b/core/edge/opfs_wasm32/src/lib.rs index c2201ac0ce32..cd4a818ba5e0 100644 --- a/core/edge/opfs_wasm32/src/lib.rs +++ b/core/edge/opfs_wasm32/src/lib.rs @@ -152,6 +152,7 @@ mod tests { let buffer = op.read(path).await.expect("read"); console_log!("read = {:?}", buffer); assert_eq!(buffer.to_bytes(), content.as_bytes()); + op.delete(path).await.expect("delete"); } } @@ -159,10 +160,12 @@ mod tests { async fn test_write_write_twice_same_file() { let op = new_operator(); let content = "Content of the file to write"; - let meta = op.write("/test_file", content).await.expect("write"); - let meta = op.write("/test_file", content).await.expect("write"); + let path = "/test_file"; + let meta = op.write(path, content).await.expect("write"); + let meta = op.write(path, content).await.expect("write"); assert_eq!(meta.content_length(), content.len() as u64); assert!(meta.last_modified().is_none()); + op.delete(path).await.expect("delete"); } #[wasm_bindgen_test] @@ -179,7 +182,8 @@ mod tests { assert_eq!(meta.content_length(), expected_file_size); assert!(meta.last_modified().is_none()); let stat = op.stat(path).await.expect("stat"); - assert_eq!(stat.content_length(), expected_file_size) + assert_eq!(stat.content_length(), expected_file_size); + op.delete(path).await.expect("delete"); } #[wasm_bindgen_test] @@ -217,6 +221,7 @@ mod tests { let buffer = op.read(path).await.expect("read"); assert_eq!(buffer.to_bytes().len(), expected_file_size as usize); } + op.delete(path).await.expect("delete"); } #[wasm_bindgen_test] @@ -227,5 +232,6 @@ mod tests { let meta = op.write(path, content).await.expect("write"); let buffer = op.read_with(path).range(3..5).await.expect("read"); assert_eq!(buffer.to_bytes(), "34".as_bytes()); + op.delete(path).await.expect("delete"); } } diff --git a/core/services/opfs/Cargo.toml b/core/services/opfs/Cargo.toml index d203006fd2ed..8b5ee481146c 100644 --- a/core/services/opfs/Cargo.toml +++ b/core/services/opfs/Cargo.toml @@ -46,6 +46,7 @@ web-sys = { version = "0.3.77", features = [ "FileSystemFileHandle", "FileSystemGetDirectoryOptions", "FileSystemGetFileOptions", + "FileSystemRemoveOptions", "FileSystemWritableFileStream", "Navigator", "WriteCommandType", diff --git a/core/services/opfs/src/backend.rs b/core/services/opfs/src/backend.rs index bec33e008c82..3ee6d37d709d 100644 --- a/core/services/opfs/src/backend.rs +++ b/core/services/opfs/src/backend.rs @@ -22,8 +22,9 @@ use wasm_bindgen_futures::JsFuture; use web_sys::File; use web_sys::FileSystemWritableFileStream; -use super::OPFS_SCHEME; use super::config::OpfsConfig; +use super::core::OpfsCore; +use super::deleter::OpfsDeleter; use super::error::*; use super::lister::OpfsLister; use super::reader::OpfsReader; @@ -55,14 +56,15 @@ impl Builder for OpfsBuilder { fn build(self) -> Result { let root = normalize_root(&self.config.root.unwrap_or_default()); - Ok(OpfsBackend { root }) + let core = Arc::new(OpfsCore::new(root)); + Ok(OpfsBackend { core }) } } /// OPFS Service backend #[derive(Debug, Clone)] pub struct OpfsBackend { - root: String, + core: Arc, } impl Access for OpfsBackend { @@ -72,33 +74,14 @@ impl Access for OpfsBackend { type Lister = OpfsLister; - type Deleter = (); + type Deleter = oio::OneShotDeleter; fn info(&self) -> Arc { - let info = AccessorInfo::default(); - info.set_scheme(OPFS_SCHEME); - info.set_name("opfs"); - info.set_root(&self.root); - info.set_native_capability(Capability { - stat: true, - - read: true, - - list: true, - - create_dir: true, - - write: true, - write_can_empty: true, - write_can_multi: true, - - ..Default::default() - }); - Arc::new(info) + self.core.info.clone() } async fn stat(&self, path: &str, _args: OpStat) -> Result { - let p = build_abs_path(&self.root, path); + let p = build_abs_path(&self.core.root, path); if p.ends_with('/') { get_directory_handle(&p, false).await?; @@ -122,14 +105,14 @@ impl Access for OpfsBackend { } async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::Reader)> { - let p = build_abs_path(&self.root, path); + let p = build_abs_path(&self.core.root, path); let handle = get_file_handle(&p, false).await?; Ok((RpRead::new(), OpfsReader::new(handle, args.range()))) } async fn list(&self, path: &str, _args: OpList) -> Result<(RpList, Self::Lister)> { - let p = build_abs_path(&self.root, path); + let p = build_abs_path(&self.core.root, path); let dir = get_directory_handle(&p, false).await?; Ok((RpList::default(), OpfsLister::new(dir, path.to_string()))) @@ -137,14 +120,14 @@ impl Access for OpfsBackend { async fn create_dir(&self, path: &str, _: OpCreateDir) -> Result { debug_assert!(path != "/", "root path should be handled upstream"); - let p = build_abs_path(&self.root, path); + let p = build_abs_path(&self.core.root, path); get_directory_handle(&p, true).await?; Ok(RpCreateDir::default()) } async fn write(&self, path: &str, _args: OpWrite) -> Result<(RpWrite, Self::Writer)> { - let p = build_abs_path(&self.root, path); + let p = build_abs_path(&self.core.root, path); let handle = get_file_handle(&p, true).await?; console_debug!("write: handle = {:?}", handle); console_debug!("write: path = {:?}", p); @@ -155,4 +138,11 @@ impl Access for OpfsBackend { Ok((RpWrite::default(), OpfsWriter::new(stream))) } + + async fn delete(&self) -> Result<(RpDelete, Self::Deleter)> { + Ok(( + RpDelete::default(), + oio::OneShotDeleter::new(OpfsDeleter::new(self.core.clone())), + )) + } } diff --git a/core/services/opfs/src/core.rs b/core/services/opfs/src/core.rs index b248758bc120..943accb34238 100644 --- a/core/services/opfs/src/core.rs +++ b/core/services/opfs/src/core.rs @@ -14,3 +14,46 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. + +use std::sync::Arc; + +use super::OPFS_SCHEME; +use opendal_core::raw::*; +use opendal_core::*; + +#[derive(Debug, Clone)] +pub struct OpfsCore { + pub root: String, + pub info: Arc, +} + +impl OpfsCore { + pub fn new(root: String) -> Self { + let info = AccessorInfo::default(); + info.set_scheme(OPFS_SCHEME); + info.set_name("opfs"); + info.set_root(&root); + info.set_native_capability(Capability { + stat: true, + + read: true, + + list: true, + + create_dir: true, + + write: true, + write_can_empty: true, + write_can_multi: true, + + delete: true, + + ..Default::default() + }); + + Self { + root, + info: Arc::new(info), + } + } +} diff --git a/core/services/opfs/src/deleter.rs b/core/services/opfs/src/deleter.rs new file mode 100644 index 000000000000..8b0a56c1487c --- /dev/null +++ b/core/services/opfs/src/deleter.rs @@ -0,0 +1,64 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::sync::Arc; + +use wasm_bindgen_futures::JsFuture; +use web_sys::FileSystemRemoveOptions; + +use super::core::OpfsCore; +use super::error::*; +use super::utils::*; +use opendal_core::raw::*; +use opendal_core::*; + +pub struct OpfsDeleter { + core: Arc, +} + +impl OpfsDeleter { + pub fn new(core: Arc) -> Self { + Self { core } + } +} + +impl oio::OneShotDelete for OpfsDeleter { + async fn delete_once(&self, path: String, args: OpDelete) -> Result<()> { + console_debug!("delete!"); + let p = build_abs_path(&self.core.root, &path); + let (dir, name) = get_parent_dir_and_name(&p, false).await?; + + let opt = FileSystemRemoveOptions::new(); + opt.set_recursive(args.recursive()); + + match JsFuture::from(dir.remove_entry_with_options(name, &opt)).await { + Ok(_) => { + console_debug!("delete ok!"); + Ok(()) + } + Err(e) => { + let err = parse_js_error(e); + // Deleting a non-existent entry is not an error. + if err.kind() == ErrorKind::NotFound { + Ok(()) + } else { + Err(err) + } + } + } + } +} diff --git a/core/services/opfs/src/lib.rs b/core/services/opfs/src/lib.rs index b6aca7ec52e3..42ba6a7ab206 100644 --- a/core/services/opfs/src/lib.rs +++ b/core/services/opfs/src/lib.rs @@ -35,6 +35,7 @@ macro_rules! console_debug { mod backend; mod config; mod core; +mod deleter; mod error; mod lister; mod reader; From 9101721d2935efc956c4413f52b56e1ded42e5d6 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Mon, 23 Feb 2026 01:35:16 +0000 Subject: [PATCH 22/29] revert .vscode/settings.json --- .vscode/settings.json | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 51ed362921bd..2a5e9c7a9b99 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,13 +1,6 @@ { - // "rust-analyzer.cargo.allTargets": true, - // "rust-analyzer.cargo.features": "all", - "rust-analyzer.cargo.target": "wasm32-unknown-unknown", //////////////// DO NOT MERGE - "rust-analyzer.cargo.allTargets": false, ///////// DO NOT MERGE - // "rust-analyzer.cargo.features": "all", - // "rust-analyzer.cargo.features": ["opfs"], - "rust-analyzer.cargo.features": [ - "services-opfs" - ], + "rust-analyzer.cargo.allTargets": true, + "rust-analyzer.cargo.features": "all", "rust-analyzer.procMacro.ignored": { "napi-derive": [ "napi" From e098e9a289525b7b342d67be4e638e70f43c71c1 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Mon, 23 Feb 2026 01:46:31 +0000 Subject: [PATCH 23/29] cargo fmt --- core/services/opfs/src/lister.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/core/services/opfs/src/lister.rs b/core/services/opfs/src/lister.rs index b7a72ed2d042..e188bf6f826f 100644 --- a/core/services/opfs/src/lister.rs +++ b/core/services/opfs/src/lister.rs @@ -43,13 +43,9 @@ impl OpfsLister { impl oio::List for OpfsLister { async fn next(&mut self) -> Result> { - let result = JsFuture::from( - self.iter - .next() - .map_err(parse_js_error)?, - ) - .await - .map_err(parse_js_error)?; + let result = JsFuture::from(self.iter.next().map_err(parse_js_error)?) + .await + .map_err(parse_js_error)?; let done = js_sys::Reflect::get(&result, &"done".into()) .unwrap_or(JsValue::TRUE) @@ -59,8 +55,7 @@ impl oio::List for OpfsLister { return Ok(None); } - let value = js_sys::Reflect::get(&result, &"value".into()) - .map_err(parse_js_error)?; + let value = js_sys::Reflect::get(&result, &"value".into()).map_err(parse_js_error)?; let pair: js_sys::Array = value.unchecked_into(); let name = pair.get(0).as_string().unwrap_or_default(); let handle = pair.get(1); From 2674361c07bb3a4346ed3a5decaf2c8cfe86c178 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Mon, 23 Feb 2026 01:56:04 +0000 Subject: [PATCH 24/29] trying fixing gh actions --- core/edge/opfs_wasm32/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/core/edge/opfs_wasm32/src/lib.rs b/core/edge/opfs_wasm32/src/lib.rs index cd4a818ba5e0..b56c3f6d7ce6 100644 --- a/core/edge/opfs_wasm32/src/lib.rs +++ b/core/edge/opfs_wasm32/src/lib.rs @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +#![cfg(target_arch = "wasm32")] #[cfg(test)] mod tests { use futures::TryStreamExt; From 85bff83d53977cc16e220aed8791a23e458b4c15 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Mon, 23 Feb 2026 02:52:48 +0000 Subject: [PATCH 25/29] Format core/services/opfs/Cargo.toml --- core/services/opfs/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/services/opfs/Cargo.toml b/core/services/opfs/Cargo.toml index 8b5ee481146c..ac33c17e9386 100644 --- a/core/services/opfs/Cargo.toml +++ b/core/services/opfs/Cargo.toml @@ -33,9 +33,9 @@ all-features = true [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = "0.3.77" opendal-core = { path = "../../core", version = "0.55.0", default-features = false } +send_wrapper = "0.6" serde = { workspace = true, features = ["derive"] } wasm-bindgen = "0.2.100" -send_wrapper = "0.6" wasm-bindgen-futures = "0.4.50" web-sys = { version = "0.3.77", features = [ "Blob", From f0a3b211a3822302173a3ce65267ea35d8e9fc3a Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Mon, 23 Feb 2026 02:53:17 +0000 Subject: [PATCH 26/29] Format core/edge/opfs_wasm32/Cargo.toml --- core/edge/opfs_wasm32/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/edge/opfs_wasm32/Cargo.toml b/core/edge/opfs_wasm32/Cargo.toml index dfc2cffb0c98..cb6adcb04d21 100644 --- a/core/edge/opfs_wasm32/Cargo.toml +++ b/core/edge/opfs_wasm32/Cargo.toml @@ -28,13 +28,13 @@ version.workspace = true crate-type = ["cdylib"] [dependencies] +futures = { workspace = true, features = ["std", "async-await"] } opendal = { path = "../..", default-features = false, features = [ "services-opfs", ] } wasm-bindgen = "0.2.89" wasm-bindgen-futures = "0.4.39" web-sys = { version = "0.3.77", features = ["console"] } -futures = { workspace = true, features = ["std", "async-await"] } [dev-dependencies] wasm-bindgen-test = "0.3.41" From 2674969f69b59e18812812bf0780df27f6c6cce1 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Mon, 23 Feb 2026 02:58:57 +0000 Subject: [PATCH 27/29] update capabilities in docs.md --- core/services/opfs/src/docs.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/services/opfs/src/docs.md b/core/services/opfs/src/docs.md index a529c2ff3b5e..fef5f71f343c 100644 --- a/core/services/opfs/src/docs.md +++ b/core/services/opfs/src/docs.md @@ -2,12 +2,12 @@ This service can be used to: -- [ ] create_dir -- [ ] stat -- [ ] read -- [ ] write -- [ ] delete -- [ ] list +- [x] create_dir +- [x] stat +- [x] read +- [x] write +- [x] delete +- [x] list - [ ] copy - [ ] rename - [ ] presign From d8eeacf39eb8f07bc8abed5d0c59a0bf9c8a1a68 Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Mon, 23 Feb 2026 09:21:06 +0000 Subject: [PATCH 28/29] Remove debug logs --- core/services/opfs/src/backend.rs | 2 -- core/services/opfs/src/deleter.rs | 2 -- core/services/opfs/src/error.rs | 1 - core/services/opfs/src/utils.rs | 2 -- core/services/opfs/src/writer.rs | 3 --- 5 files changed, 10 deletions(-) diff --git a/core/services/opfs/src/backend.rs b/core/services/opfs/src/backend.rs index 3ee6d37d709d..ab50212a5735 100644 --- a/core/services/opfs/src/backend.rs +++ b/core/services/opfs/src/backend.rs @@ -129,8 +129,6 @@ impl Access for OpfsBackend { async fn write(&self, path: &str, _args: OpWrite) -> Result<(RpWrite, Self::Writer)> { let p = build_abs_path(&self.core.root, path); let handle = get_file_handle(&p, true).await?; - console_debug!("write: handle = {:?}", handle); - console_debug!("write: path = {:?}", p); let stream: FileSystemWritableFileStream = JsFuture::from(handle.create_writable()) .await .and_then(JsCast::dyn_into) diff --git a/core/services/opfs/src/deleter.rs b/core/services/opfs/src/deleter.rs index 8b0a56c1487c..ed6c819fd027 100644 --- a/core/services/opfs/src/deleter.rs +++ b/core/services/opfs/src/deleter.rs @@ -38,7 +38,6 @@ impl OpfsDeleter { impl oio::OneShotDelete for OpfsDeleter { async fn delete_once(&self, path: String, args: OpDelete) -> Result<()> { - console_debug!("delete!"); let p = build_abs_path(&self.core.root, &path); let (dir, name) = get_parent_dir_and_name(&p, false).await?; @@ -47,7 +46,6 @@ impl oio::OneShotDelete for OpfsDeleter { match JsFuture::from(dir.remove_entry_with_options(name, &opt)).await { Ok(_) => { - console_debug!("delete ok!"); Ok(()) } Err(e) => { diff --git a/core/services/opfs/src/error.rs b/core/services/opfs/src/error.rs index 355ac0e42660..b50a223c3a0f 100644 --- a/core/services/opfs/src/error.rs +++ b/core/services/opfs/src/error.rs @@ -23,7 +23,6 @@ use opendal_core::Error; use opendal_core::ErrorKind; pub(crate) fn parse_js_error(value: JsValue) -> Error { - console_debug!("parse_js_error: {:?}", value); if let Some(exc) = value.dyn_ref::() { let kind = match exc.name().as_str() { "NotFoundError" => ErrorKind::NotFound, diff --git a/core/services/opfs/src/utils.rs b/core/services/opfs/src/utils.rs index d45685898ffd..ec49caefd426 100644 --- a/core/services/opfs/src/utils.rs +++ b/core/services/opfs/src/utils.rs @@ -28,7 +28,6 @@ use super::error::*; /// Get the OPFS root directory handle. pub(crate) async fn get_root_directory_handle() -> Result { - console_debug!("get_root_directory_handle"); let navigator = window().unwrap().navigator(); let storage_manager = navigator.storage(); // This may fail if not secure (not: HTTPS or localhost) @@ -45,7 +44,6 @@ pub(crate) async fn get_directory_handle( path: &str, create: bool, ) -> Result { - console_debug!("get_directory_handle path={path:?} create={create:?}"); let opt = FileSystemGetDirectoryOptions::new(); opt.set_create(create); diff --git a/core/services/opfs/src/writer.rs b/core/services/opfs/src/writer.rs index 068fcd8e7041..551131ae345f 100644 --- a/core/services/opfs/src/writer.rs +++ b/core/services/opfs/src/writer.rs @@ -43,7 +43,6 @@ impl OpfsWriter { impl oio::Write for OpfsWriter { async fn write(&mut self, bs: Buffer) -> Result<()> { - console_debug!("write!!!!!!!!!!!!!!!!!!!!!"); let bytes = bs.to_bytes(); let params = WriteParams::new(WriteCommandType::Write); params.set_size(Some(bytes.len() as f64)); @@ -62,7 +61,6 @@ impl oio::Write for OpfsWriter { } async fn close(&mut self) -> Result { - console_debug!("close!!!!!!!!!!!!!!!!!!!!!"); JsFuture::from(self.stream.close()) .await .map_err(parse_js_error)?; @@ -74,7 +72,6 @@ impl oio::Write for OpfsWriter { } async fn abort(&mut self) -> Result<()> { - console_debug!("abort!!!!!!!!!!!!!!!!!!!!!"); JsFuture::from(self.stream.abort()) .await .map_err(parse_js_error)?; From 5658d2df48fd802b10e6e7c47692178dae465f8c Mon Sep 17 00:00:00 2001 From: Jean-Charles Campagne Date: Mon, 23 Feb 2026 10:13:52 +0000 Subject: [PATCH 29/29] cargo fmt --- core/services/opfs/src/deleter.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/services/opfs/src/deleter.rs b/core/services/opfs/src/deleter.rs index ed6c819fd027..2d2399743495 100644 --- a/core/services/opfs/src/deleter.rs +++ b/core/services/opfs/src/deleter.rs @@ -45,9 +45,7 @@ impl oio::OneShotDelete for OpfsDeleter { opt.set_recursive(args.recursive()); match JsFuture::from(dir.remove_entry_with_options(name, &opt)).await { - Ok(_) => { - Ok(()) - } + Ok(_) => Ok(()), Err(e) => { let err = parse_js_error(e); // Deleting a non-existent entry is not an error.