Skip to content
Open
1 change: 1 addition & 0 deletions core/services/opfs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ web-sys = { version = "0.3.77", features = [
"FileSystemFileHandle",
"FileSystemGetDirectoryOptions",
"FileSystemGetFileOptions",
"FileSystemRemoveOptions",
"FileSystemWritableFileStream",
"Navigator",
"StorageManager",
Expand Down
75 changes: 57 additions & 18 deletions core/services/opfs/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand All @@ -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<impl Access> {
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<OpfsCore>,
}

impl OpfsBackend {
pub(crate) fn new(core: Arc<OpfsCore>) -> Self {
Self { core }
}
}

impl Access for OpfsBackend {
type Reader = ();
Expand All @@ -51,25 +90,25 @@ impl Access for OpfsBackend {

type Lister = ();

type Deleter = ();
type Deleter = oio::OneShotDeleter<OpfsDeleter>;

fn info(&self) -> Arc<AccessorInfo> {
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<RpStat> {
let metadata = self.core.opfs_stat(path).await?;
Ok(RpStat::new(metadata))
}

async fn create_dir(&self, path: &str, _: OpCreateDir) -> Result<RpCreateDir> {
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))
}
}
5 changes: 4 additions & 1 deletion core/services/opfs/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
}

impl Debug for OpfsConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Expand Down
136 changes: 98 additions & 38 deletions core/services/opfs/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AccessorInfo>,
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<AccessorInfo>, 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<Metadata> {
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<Vec<u8>, 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<FileSystemDirectoryHandle> {
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<FileSystemDirectoryHandle> {
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)
}
}
67 changes: 67 additions & 0 deletions core/services/opfs/src/delete.rs
Original file line number Diff line number Diff line change
@@ -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<OpfsCore>,
}

impl OpfsDeleter {
pub(crate) fn new(core: Arc<OpfsCore>) -> 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),
}
}
}
6 changes: 3 additions & 3 deletions core/services/opfs/src/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 11 additions & 4 deletions core/services/opfs/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
}
Loading
Loading