Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OPFS support for OpenDAL (draft PR) #5269

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions core/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,20 @@ prometheus-client = { version = "0.22.2", optional = true }
tracing = { version = "0.1", optional = true }
# for layers-dtrace
probe = { version = "0.5.1", optional = true }
wasm-bindgen = "0.2.95"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, please mark those dependencies as optional and hide them under the services-opfs feature to ensure other builds aren't affected.

wasm-bindgen-futures = "0.4.45"
web-sys = { version = "0.3.72", features = [
"Window",
"File",
"FileSystemDirectoryHandle",
"FileSystemFileHandle",
"FileSystemGetFileOptions",
"FileSystemWritableFileStream",
"Navigator",
"StorageManager",
"FileSystemGetFileOptions",
] }
js-sys = "0.3.72"

[target.'cfg(target_arch = "wasm32")'.dependencies]
backon = { version = "1.2", features = ["gloo-timers-sleep"] }
Expand Down
3 changes: 3 additions & 0 deletions core/src/services/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ pub use obs::*;
mod onedrive;
pub use onedrive::*;

mod opfs;
pub use opfs::*;

mod oss;
pub use oss::*;

Expand Down
240 changes: 240 additions & 0 deletions core/src/services/opfs/backend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
use serde::Deserialize;
use std::sync::Arc;

use crate::{
raw::{
Access, AccessorInfo, OpCopy, OpCreateDir, OpDelete, OpList, OpRead, OpRename, OpStat,
OpWrite, RpCopy, RpCreateDir, RpDelete, RpList, RpRead, RpRename, RpStat, RpWrite,
},
types, Builder, Capability, Error, Result, Scheme,
};
use std::fmt::Debug;

use super::{core::OpfsCore, lister::OpfsLister, reader::OpfsReader, writer::OpfsWriter};

/// Origin private file system (OPFS) configuration
#[derive(Default, Deserialize)]
#[serde(default)]
#[non_exhaustive]
pub struct OpfsConfig {}

impl Debug for OpfsConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
panic!()
}
}

/// Origin private file system (OPFS) support
#[doc = include_str!("docs.md")]
#[derive(Default)]
pub struct OpfsBuilder {
config: OpfsConfig,
}

impl OpfsBuilder {}

impl Debug for OpfsBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
panic!()
}
}

impl Builder for OpfsBuilder {
const SCHEME: Scheme = Scheme::Opfs;

type Config = ();

fn build(self) -> Result<impl Access> {
Ok(OpfsBackend {})
}
}

/// OPFS Service backend
#[derive(Debug, Clone)]
pub struct OpfsBackend {}

impl Access for OpfsBackend {
type Reader = OpfsReader;

type Writer = OpfsWriter;

type Lister = OpfsLister;

type BlockingLister = OpfsLister;

type BlockingReader = OpfsReader;

type BlockingWriter = OpfsWriter;

fn info(&self) -> Arc<AccessorInfo> {
let mut access_info = AccessorInfo::default();
access_info
.set_scheme(Scheme::Opfs)
.set_native_capability(Capability {
stat: true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please leave them as false until we actually implement them.

read: true,
write: true,
write_can_empty: true,
write_can_append: true,
write_can_multi: true,
create_dir: true,
delete: true,
list: true,
copy: true,
rename: true,
blocking: true,
..Default::default()
});
Arc::new(access_info)
}

async fn create_dir(&self, path: &str, _: OpCreateDir) -> Result<RpCreateDir> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have default implementations for those functions, so we can remove unsupported operations.

Err(Error::new(
types::ErrorKind::Unsupported,
"Operation not supported yet",
))
}

async fn stat(&self, path: &str, _: OpStat) -> Result<RpStat> {
Err(Error::new(
types::ErrorKind::Unsupported,
"Operation not supported yet",
))
}

async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::Reader)> {
let path = path.to_owned();

// OpfsHelper::read_file_with_local_set(path).await;
let out_buf = OpfsCore::read_file(path.as_str()).await?;
Ok::<(RpRead, Self::Reader), Error>((RpRead::default(), Self::Reader {}))
}

async fn write(&self, path: &str, args: OpWrite) -> Result<(RpWrite, Self::Writer)> {
// Access the OPFS
// let path = path.to_owned();

// spawn_local(async move {
// OpfsCore::store_file(path.as_str(), &[1, 2, 3, 4]).await?;
// Ok::<(), Error>(())
// })
// .await
// .unwrap()?;

Ok((RpWrite::default(), Self::Writer {}))
}

async fn delete(&self, path: &str, _: OpDelete) -> Result<RpDelete> {
Err(Error::new(
types::ErrorKind::Unsupported,
"Operation not supported yet",
))
}

async fn list(&self, path: &str, args: OpList) -> Result<(RpList, Self::Lister)> {
Err(Error::new(
types::ErrorKind::Unsupported,
"Operation not supported yet",
))
}

async fn copy(&self, from: &str, to: &str, args: OpCopy) -> Result<RpCopy> {
Err(Error::new(
types::ErrorKind::Unsupported,
"Operation not supported yet",
))
}

async fn rename(&self, from: &str, to: &str, _args: OpRename) -> Result<RpRename> {
Err(Error::new(
types::ErrorKind::Unsupported,
"Operation not supported yet",
))
}

fn blocking_create_dir(&self, path: &str, args: OpCreateDir) -> Result<RpCreateDir> {
Err(Error::new(
types::ErrorKind::Unsupported,
"Operation not supported yet",
))
}

fn blocking_stat(&self, path: &str, args: OpStat) -> Result<RpStat> {
Err(Error::new(
types::ErrorKind::Unsupported,
"Operation not supported yet",
))
}

fn blocking_read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::BlockingReader)> {
Err(Error::new(
types::ErrorKind::Unsupported,
"Operation not supported yet",
))
}

fn blocking_write(&self, path: &str, args: OpWrite) -> Result<(RpWrite, Self::BlockingWriter)> {
Err(Error::new(
types::ErrorKind::Unsupported,
"Operation not supported yet",
))
}

fn blocking_delete(&self, path: &str, args: OpDelete) -> Result<RpDelete> {
Err(Error::new(
types::ErrorKind::Unsupported,
"Operation not supported yet",
))
}

fn blocking_list(&self, path: &str, args: OpList) -> Result<(RpList, Self::BlockingLister)> {
Err(Error::new(
types::ErrorKind::Unsupported,
"Operation not supported yet",
))
}

fn blocking_copy(&self, from: &str, to: &str, args: OpCopy) -> Result<RpCopy> {
Err(Error::new(
types::ErrorKind::Unsupported,
"Operation not supported yet",
))
}

fn blocking_rename(&self, from: &str, to: &str, args: OpRename) -> Result<RpRename> {
Err(Error::new(
types::ErrorKind::Unsupported,
"Operation not supported yet",
))
}
}

#[cfg(test)]
#[cfg(target_arch = "wasm32")]
mod opfs_tests {
use wasm_bindgen::prelude::*;
use wasm_bindgen_test::*;

use std::collections::HashMap;

use crate::Operator;

use super::*;

wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);

#[wasm_bindgen]
pub async fn test_opfs() -> String {
let map = HashMap::new();
let op = Operator::via_map(Scheme::Opfs, map).unwrap();
let bs = op.read("path/to/file").await.unwrap();
"ok".to_string()
}

#[wasm_bindgen_test]
async fn basic_test() -> Result<()> {
let s = test_opfs().await;
assert_eq!(s, "ok".to_string());
Ok(())
}
}
82 changes: 82 additions & 0 deletions core/src/services/opfs/core.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use std::fmt::Debug;

use crate::Result;

use web_sys::{
window, File, FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetFileOptions,
FileSystemWritableFileStream,
};

use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;

pub struct OpfsCore {}

impl Debug for OpfsCore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
panic!()
}
}

impl OpfsCore {
pub async fn store_file(file_name: &str, content: &[u8]) -> Result<(), JsValue> {
// Access the OPFS
let navigator = window().unwrap().navigator();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please try your best to avoid using unwrap(), and return an error instead.

let storage_manager = navigator.storage();
let root: FileSystemDirectoryHandle = JsFuture::from(storage_manager.get_directory())
.await?
.dyn_into()?;

let opt = FileSystemGetFileOptions::new();
opt.set_create(true);

// Create or get the file in the OPFS
let file_handle: FileSystemFileHandle =
JsFuture::from(root.get_file_handle_with_options(file_name, &opt))
.await?
.dyn_into()?;

// Create a writable stream
let writable: FileSystemWritableFileStream = JsFuture::from(file_handle.create_writable())
.await?
.dyn_into()?;

// Write the content to the file
JsFuture::from(
writable
.write_with_u8_array(content)
.expect("failed to write file"),
)
.await?;

// Close the writable stream
JsFuture::from(writable.close()).await?;

Ok(())
}

pub async fn read_file(file_name: &str) -> Result<Vec<u8>, JsValue> {
// Access the OPFS
let navigator = window().unwrap().navigator();
let storage_manager = navigator.storage();
let root: FileSystemDirectoryHandle = JsFuture::from(storage_manager.get_directory())
.await?
.dyn_into()?;

// Get the file handle
let file_handle: FileSystemFileHandle = JsFuture::from(root.get_file_handle(file_name))
.await?
.dyn_into()?;

// Get the file from the handle
let file: File = JsFuture::from(file_handle.get_file()).await?.dyn_into()?;
let array_buffer = JsFuture::from(file.array_buffer()).await?;

// Convert the ArrayBuffer to a Vec<u8>
let u8_array = js_sys::Uint8Array::new(&array_buffer);
let mut vec = vec![0; u8_array.length() as usize];
u8_array.copy_to(&mut vec[..]);

Ok(vec)
}
}
Empty file.
10 changes: 10 additions & 0 deletions core/src/services/opfs/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use wasm_bindgen::JsValue;

use crate::Error;
use crate::ErrorKind;

impl From<JsValue> for Error {
fn from(value: JsValue) -> Self {
Error::new(ErrorKind::Unexpected, "Error")
}
}
1 change: 1 addition & 0 deletions core/src/services/opfs/helper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Loading
Loading