| // 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 super::OPFS_SCHEME; |
| use opendal_core::raw::*; |
| use opendal_core::*; |
| |
| #[derive(Debug, Clone)] |
| pub struct OpfsCore { |
| pub root: String, |
| pub info: ServiceInfo, |
| pub capability: Capability, |
| } |
| |
| impl OpfsCore { |
| pub fn new(root: String) -> Self { |
| let info = ServiceInfo::new(OPFS_SCHEME, &root, "opfs"); |
| let 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, |
| capability, |
| } |
| } |
| } |
| |
| mod error { |
| 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(value: JsValue) -> Error { |
| if let Some(exc) = value.dyn_ref::<DomException>() { |
| let kind = match exc.name().as_str() { |
| "NotFoundError" => ErrorKind::NotFound, |
| "TypeMismatchError" => ErrorKind::NotFound, |
| "NotAllowedError" => ErrorKind::PermissionDenied, |
| "QuotaExceededError" => ErrorKind::Unexpected, |
| e => { |
| console_debug!("Got unhandled DOM exception {e:?}"); |
| 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::<js_sys::Error>() { |
| return Error::new(ErrorKind::Unexpected, String::from(err.message())); |
| } |
| |
| Error::new( |
| ErrorKind::Unexpected, |
| value |
| .as_string() |
| .unwrap_or_else(|| "unknown JS error".to_string()), |
| ) |
| } |
| } |
| |
| pub(super) use error::*; |
| |
| mod utils { |
| 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 crate::core::*; |
| |
| /// Get the OPFS root directory handle. |
| pub(crate) async fn get_root_directory_handle() -> Result<FileSystemDirectoryHandle> { |
| 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) |
| .map_err(parse_js_error) |
| } |
| |
| /// Navigate to a directory handle by path. |
| /// |
| /// When `create` is true, intermediate directories are created as needed. |
| pub(crate) async fn get_directory_handle( |
| path: &str, |
| create: bool, |
| ) -> Result<FileSystemDirectoryHandle> { |
| let opt = FileSystemGetDirectoryOptions::new(); |
| opt.set_create(create); |
| |
| let trimmed = path.trim_matches('/'); |
| let mut handle = get_root_directory_handle().await?; |
| 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) |
| .map_err(parse_js_error)?; |
| } |
| |
| 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<FileSystemFileHandle> { |
| 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) |
| } |
| } |
| |
| pub(super) use utils::*; |