diff --git a/core/services/opfs/Cargo.toml b/core/services/opfs/Cargo.toml index 336b2e4225cd..6d35d02696b5 100644 --- a/core/services/opfs/Cargo.toml +++ b/core/services/opfs/Cargo.toml @@ -45,6 +45,7 @@ web-sys = { version = "0.3.77", features = [ "FileSystemFileHandle", "FileSystemGetDirectoryOptions", "FileSystemGetFileOptions", + "FileSystemRemoveOptions", "FileSystemWritableFileStream", "Navigator", "StorageManager", diff --git a/core/services/opfs/src/backend.rs b/core/services/opfs/src/backend.rs index 9e28868a507c..652901cf954e 100644 --- a/core/services/opfs/src/backend.rs +++ b/core/services/opfs/src/backend.rs @@ -18,11 +18,11 @@ use std::fmt::Debug; use std::sync::Arc; -use web_sys::FileSystemGetDirectoryOptions; - use super::OPFS_SCHEME; use super::config::OpfsConfig; -use super::utils::*; +use super::core::OpfsCore; +use super::delete::OpfsDeleter; + use opendal_core::raw::*; use opendal_core::*; @@ -32,17 +32,56 @@ pub struct OpfsBuilder { pub(super) config: OpfsConfig, } +impl OpfsBuilder { + /// Set root for 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 = self.config.root.ok_or( + Error::new(ErrorKind::ConfigInvalid, "root is not specified") + .with_operation("Builder::build"), + )?; + + let info = AccessorInfo::default(); + info.set_scheme(OPFS_SCHEME) + .set_root(&root) + .set_native_capability(Capability { + stat: true, + create_dir: true, + delete: true, + delete_with_recursive: true, + ..Default::default() + }); + + let core = Arc::new(OpfsCore::new(Arc::new(info), root)); + + Ok(OpfsBackend::new(core)) } } /// OPFS Service backend #[derive(Default, Debug, Clone)] -pub struct OpfsBackend {} +pub struct OpfsBackend { + core: Arc, +} + +impl OpfsBackend { + pub(crate) fn new(core: Arc) -> Self { + Self { core } + } +} impl Access for OpfsBackend { type Reader = (); @@ -51,25 +90,25 @@ impl Access for OpfsBackend { type Lister = (); - 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("/"); - info.set_native_capability(Capability { - create_dir: true, - ..Default::default() - }); - Arc::new(info) + self.core.info.clone() + } + + async fn stat(&self, path: &str, _: OpStat) -> Result { + let metadata = self.core.opfs_stat(path).await?; + Ok(RpStat::new(metadata)) } async fn create_dir(&self, path: &str, _: OpCreateDir) -> Result { - let opt = FileSystemGetDirectoryOptions::new(); - opt.set_create(true); - get_directory_handle(path, &opt).await?; + self.core.opfs_create_dir(path).await?; Ok(RpCreateDir::default()) } + + async fn delete(&self) -> Result<(RpDelete, Self::Deleter)> { + let deleter = oio::OneShotDeleter::new(OpfsDeleter::new(self.core.clone())); + Ok((RpDelete::default(), deleter)) + } } diff --git a/core/services/opfs/src/config.rs b/core/services/opfs/src/config.rs index 38bd72c21be7..c5aba6816c19 100644 --- a/core/services/opfs/src/config.rs +++ b/core/services/opfs/src/config.rs @@ -26,7 +26,10 @@ use super::backend::OpfsBuilder; #[derive(Default, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(default)] #[non_exhaustive] -pub struct OpfsConfig {} +pub struct OpfsConfig { + /// root dir for backend + pub root: Option, +} impl Debug for OpfsConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/core/services/opfs/src/core.rs b/core/services/opfs/src/core.rs index 10cd1da91996..ba7115d2a0ac 100644 --- a/core/services/opfs/src/core.rs +++ b/core/services/opfs/src/core.rs @@ -16,58 +16,118 @@ // under the License. use std::fmt::Debug; +use std::sync::Arc; + +use opendal_core::raw::*; +use opendal_core::*; 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 web_sys::{ + File, FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetDirectoryOptions, +}; use super::error::*; use super::utils::*; -#[derive(Default, Debug)] -pub struct OpfsCore {} +#[derive(Debug, Default)] +pub(super) struct OpfsCore { + pub info: Arc, + pub root: String, +} 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)?; + pub(crate) fn new(info: Arc, root: String) -> Self { + Self { info, root } + } + + pub(crate) fn path(&self, path: &str) -> String { + build_abs_path(&self.root, path) + } + + pub(crate) async fn opfs_stat(&self, path: &str) -> Result { + let parent_handle = self.parent_dir_handle(path).await?; + let path = build_abs_path(&self.root, &path); + let last_component = path + .trim_end_matches('/') + .rsplit_once('/') + .map(|s| s.1) + .unwrap_or("/"); + + match JsFuture::from(parent_handle.get_directory_handle(last_component)).await { + // TODO: set content length for directory metadata + Ok(_) => Ok(Metadata::new(EntryMode::DIR)), + Err(err) => { + let err = js_sys::Error::from(err); + match String::from(err.name()).as_str() { + JS_TYPE_MISMATCH_ERROR => { + // the entry is a file and not a directory + let handle: FileSystemFileHandle = + JsFuture::from(parent_handle.get_file_handle(last_component)) + .await + .and_then(JsCast::dyn_into) + .map_err(parse_js_error)?; + + let file: File = JsFuture::from(handle.get_file()) + .await + .and_then(JsCast::dyn_into) + .map_err(parse_js_error)?; + + let last_modified = file.last_modified() as i64; + let metadata = Metadata::new(EntryMode::FILE) + .with_content_length(file.size() as u64) + .with_last_modified(Timestamp::from_millisecond(last_modified)?); + + Ok(metadata) + } + _ => Err(parse_js_error(err.into())), + } + } + } + } + + pub(crate) async fn opfs_create_dir(&self, path: &str) -> Result<()> { + let opt = FileSystemGetDirectoryOptions::new(); + opt.set_create(true); + + self.dir_handle_with_option(path, &opt).await?; Ok(()) } - #[allow(unused)] - async fn read_file(&self, file_name: &str) -> Result, Error> { - let handle = get_handle_by_filename(file_name).await?; + /// Get directory handle with options + pub(crate) async fn dir_handle_with_option( + &self, + path: &str, + opt: &FileSystemGetDirectoryOptions, + ) -> Result { + let path = build_abs_path(&self.root, path); + let dirs: Vec<&str> = path.trim_matches('/').split('/').collect(); + + let mut handle = get_root_directory_handle().await?; + 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)?; + } + Ok(handle) + } + + /// Get parent directory handle + pub(crate) async fn parent_dir_handle(&self, path: &str) -> Result { + let path = build_abs_path(&self.root, path); + + let paths: Vec<&str> = path.trim_matches('/').split('/').collect(); - 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)?; + let mut handle = get_root_directory_handle().await?; + for dir in paths[0..paths.len() - 1].iter() { + handle = JsFuture::from(handle.get_directory_handle(dir)) + .await + .and_then(JsCast::dyn_into) + .map_err(parse_js_error)?; + } - Ok(js_sys::Uint8Array::new(&array_buffer).to_vec()) + Ok(handle) } } diff --git a/core/services/opfs/src/delete.rs b/core/services/opfs/src/delete.rs new file mode 100644 index 000000000000..c2b6e945b1de --- /dev/null +++ b/core/services/opfs/src/delete.rs @@ -0,0 +1,67 @@ +// 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 opendal_core::{ + ErrorKind, Result, + raw::{OpDelete, oio::OneShotDelete}, +}; +use wasm_bindgen_futures::JsFuture; +use web_sys::FileSystemRemoveOptions; + +use super::core::OpfsCore; +use super::error::*; + +pub struct OpfsDeleter { + core: Arc, +} + +impl OpfsDeleter { + pub(crate) fn new(core: Arc) -> Self { + Self { core } + } +} + +impl OneShotDelete for OpfsDeleter { + async fn delete_once(&self, path: String, op: OpDelete) -> Result<()> { + let handle = match self.core.parent_dir_handle(&path).await { + Ok(handle) => handle, + Err(err) if err.kind() == ErrorKind::NotFound => return Ok(()), + Err(err) => return Err(err), + }; + + let path = self.core.path(&path); + let entry_name = path + .trim_end_matches('/') + .rsplit_once('/') + .map(|p| p.1) + .unwrap_or("/"); + + let opt = FileSystemRemoveOptions::new(); + opt.set_recursive(op.recursive()); + + match JsFuture::from(handle.remove_entry_with_options(entry_name, &opt)) + .await + .map_err(parse_js_error) + { + Ok(_) => Ok(()), + Err(err) if err.kind() == ErrorKind::NotFound => Ok(()), + Err(err) => Err(err), + } + } +} diff --git a/core/services/opfs/src/docs.md b/core/services/opfs/src/docs.md index a529c2ff3b5e..f255deddc8ef 100644 --- a/core/services/opfs/src/docs.md +++ b/core/services/opfs/src/docs.md @@ -2,11 +2,11 @@ This service can be used to: -- [ ] create_dir -- [ ] stat +- [x] create_dir +- [x] stat - [ ] read - [ ] write -- [ ] delete +- [x] delete - [ ] list - [ ] copy - [ ] rename diff --git a/core/services/opfs/src/error.rs b/core/services/opfs/src/error.rs index 5f2a1a0fe343..15a8b627fa3a 100644 --- a/core/services/opfs/src/error.rs +++ b/core/services/opfs/src/error.rs @@ -20,9 +20,16 @@ use wasm_bindgen::JsValue; use opendal_core::Error; use opendal_core::ErrorKind; +pub(crate) const JS_NOT_FOUND_ERROR: &str = "NotFoundError"; +pub(crate) const JS_TYPE_MISMATCH_ERROR: &str = "TypeMismatchError"; + pub(crate) fn parse_js_error(msg: JsValue) -> Error { - Error::new( - ErrorKind::Unexpected, - msg.as_string().unwrap_or_else(String::new), - ) + let err = js_sys::Error::from(msg); + + let kind = match String::from(err.name()).as_str() { + JS_NOT_FOUND_ERROR => ErrorKind::NotFound, + _ => ErrorKind::Unexpected, + }; + + Error::new(kind, String::from(&err.message())) } diff --git a/core/services/opfs/src/lib.rs b/core/services/opfs/src/lib.rs index 0c391e162037..258d5d676185 100644 --- a/core/services/opfs/src/lib.rs +++ b/core/services/opfs/src/lib.rs @@ -23,6 +23,7 @@ use opendal_core::DEFAULT_OPERATOR_REGISTRY; mod backend; mod config; mod core; +mod delete; mod error; mod utils; diff --git a/core/services/opfs/src/utils.rs b/core/services/opfs/src/utils.rs index 4c2997a73015..fd93413bbbf4 100644 --- a/core/services/opfs/src/utils.rs +++ b/core/services/opfs/src/utils.rs @@ -18,11 +18,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 web_sys::{FileSystemDirectoryHandle, window}; use super::error::*; @@ -34,37 +30,3 @@ pub(crate) async fn get_root_directory_handle() -> Result Result { - let dirs: Vec<&str> = dir.trim_matches('/').split('/').collect(); - - let mut handle = get_root_directory_handle().await?; - for dir in dirs { - handle = JsFuture::from(handle.get_directory_handle_with_options(dir, dir_opt)) - .await - .and_then(JsCast::dyn_into) - .map_err(parse_js_error)?; - } - - 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)?; - - let opt = FileSystemGetFileOptions::new(); - opt.set_create(true); - - JsFuture::from(root.get_file_handle_with_options(filename, &opt)) - .await - .and_then(JsCast::dyn_into) - .map_err(parse_js_error) -}