#![allow(clippy::arc_with_non_send_sync)]
use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use js_sys::Array;
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use web_sys::{
FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetDirectoryOptions,
FileSystemGetFileOptions, FileSystemHandle, FileSystemHandleKind, FileSystemReadWriteOptions,
FileSystemSyncAccessHandle,
};
use zarrs::array::Array as ZarrArray;
use zarrs::storage::byte_range::ByteRangeIterator;
use zarrs::storage::{
Bytes, ListableStorageTraits, MaybeBytesIterator, OffsetBytesIterator, ReadableStorageTraits,
StorageError, StoreKey, StoreKeys, StoreKeysPrefixes, StorePrefix, StorePrefixes,
WritableStorageTraits,
};
use crate::error::{Error, Result};
use super::zarr_write::{write_all_to_store, OutputSchema};
fn js_err(context: &str, e: &JsValue) -> StorageError {
StorageError::Other(format!("{context}: {}", format_js(e)))
}
fn format_js(e: &JsValue) -> String {
e.as_string()
.or_else(|| {
js_sys::Reflect::get(e, &JsValue::from_str("message"))
.ok()?
.as_string()
})
.unwrap_or_else(|| format!("{e:?}"))
}
fn opfs_err(context: &str, e: &JsValue) -> Error {
Error::Format {
format: "zarr".to_string(),
detail: format!("{context}: {}", format_js(e)),
}
}
struct OpenFile {
handle: FileSystemSyncAccessHandle,
}
impl OpenFile {
fn size(&self) -> std::result::Result<u64, StorageError> {
let n = self
.handle
.get_size()
.map_err(|e| js_err("FileSystemSyncAccessHandle.getSize", &e))?;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
Ok(n as u64)
}
fn read_at(&self, offset: u64, len: usize) -> std::result::Result<Bytes, StorageError> {
let mut buf = vec![0u8; len];
let opts = FileSystemReadWriteOptions::new();
#[allow(clippy::cast_precision_loss)]
opts.set_at(offset as f64);
let got = self
.handle
.read_with_u8_array_and_options(&mut buf, &opts)
.map_err(|e| js_err("FileSystemSyncAccessHandle.read", &e))?;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let got = got as usize;
if got != len {
return Err(StorageError::Other(format!(
"short OPFS read: wanted {len} bytes at {offset}, got {got}"
)));
}
Ok(Bytes::from(buf))
}
}
pub struct OpfsStore {
files: RefCell<BTreeMap<StoreKey, OpenFile>>,
staged: RefCell<BTreeMap<StoreKey, Bytes>>,
erased: RefCell<BTreeSet<StoreKey>>,
}
impl OpfsStore {
#[must_use]
pub fn staging() -> Self {
Self {
files: RefCell::new(BTreeMap::new()),
staged: RefCell::new(BTreeMap::new()),
erased: RefCell::new(BTreeSet::new()),
}
}
pub async fn open(dir: &FileSystemDirectoryHandle) -> std::result::Result<Self, JsValue> {
let store = Self::staging();
let mut found = Vec::new();
walk(dir, String::new(), &mut found).await?;
for (path, file) in found {
let handle: FileSystemSyncAccessHandle =
JsFuture::from(file.create_sync_access_handle())
.await?
.unchecked_into();
let key = StoreKey::new(&path).map_err(|e| JsValue::from_str(&e.to_string()))?;
store.files.borrow_mut().insert(key, OpenFile { handle });
}
Ok(store)
}
pub fn close(&self) {
for file in self.files.borrow().values() {
file.handle.close();
}
self.files.borrow_mut().clear();
}
#[must_use]
pub fn open_file_count(&self) -> usize {
self.files.borrow().len()
}
pub fn stored_bytes(&self) -> std::result::Result<u64, StorageError> {
self.files
.borrow()
.values()
.try_fold(0u64, |acc, f| Ok(acc + f.size()?))
}
pub async fn commit(
&self,
dir: &FileSystemDirectoryHandle,
) -> std::result::Result<(), JsValue> {
let erased: Vec<StoreKey> = self.erased.borrow_mut().iter().cloned().collect();
for key in erased {
let (parents, name) = split_key(key.as_str());
if let Some(parent) = resolve_dir(dir, &parents, false).await? {
let _ = JsFuture::from(parent.remove_entry(&name)).await;
}
}
self.erased.borrow_mut().clear();
let staged: Vec<(StoreKey, Bytes)> = self
.staged
.borrow_mut()
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
for (key, value) in staged {
let (parents, name) = split_key(key.as_str());
let parent = resolve_dir(dir, &parents, true)
.await?
.ok_or_else(|| JsValue::from_str("directory creation returned nothing"))?;
let opts = FileSystemGetFileOptions::new();
opts.set_create(true);
let file: FileSystemFileHandle =
JsFuture::from(parent.get_file_handle_with_options(&name, &opts))
.await?
.unchecked_into();
let handle: FileSystemSyncAccessHandle =
JsFuture::from(file.create_sync_access_handle())
.await?
.unchecked_into();
handle.truncate_with_f64(0.0)?;
handle.write_with_u8_array(&value)?;
handle.flush()?;
handle.close();
}
self.staged.borrow_mut().clear();
Ok(())
}
fn whole(&self, key: &StoreKey) -> std::result::Result<Option<Bytes>, StorageError> {
if let Some(b) = self.staged.borrow().get(key) {
return Ok(Some(b.clone()));
}
let files = self.files.borrow();
let Some(file) = files.get(key) else {
return Ok(None);
};
let size = file.size()?;
#[allow(clippy::cast_possible_truncation)]
Ok(Some(file.read_at(0, size as usize)?))
}
pub fn get_whole(&self, key: &str) -> std::result::Result<Option<Bytes>, StorageError> {
let key = StoreKey::new(key).map_err(|e| StorageError::Other(e.to_string()))?;
self.whole(&key)
}
fn known_keys(&self) -> StoreKeys {
let mut keys: BTreeSet<StoreKey> = self.files.borrow().keys().cloned().collect();
keys.extend(self.staged.borrow().keys().cloned());
for k in self.erased.borrow().iter() {
keys.remove(k);
}
keys.into_iter().collect()
}
}
fn split_key(key: &str) -> (Vec<String>, String) {
let mut parts: Vec<String> = key.split('/').map(ToString::to_string).collect();
let name = parts.pop().unwrap_or_default();
(parts, name)
}
async fn resolve_dir(
dir: &FileSystemDirectoryHandle,
parents: &[String],
create: bool,
) -> std::result::Result<Option<FileSystemDirectoryHandle>, JsValue> {
let mut cur = dir.clone();
for part in parents {
let opts = FileSystemGetDirectoryOptions::new();
opts.set_create(create);
match JsFuture::from(cur.get_directory_handle_with_options(part, &opts)).await {
Ok(next) => cur = next.unchecked_into(),
Err(e) if !create => {
let _ = e;
return Ok(None);
}
Err(e) => return Err(e),
}
}
Ok(Some(cur))
}
fn walk<'a>(
dir: &'a FileSystemDirectoryHandle,
prefix: String,
out: &'a mut Vec<(String, FileSystemFileHandle)>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = std::result::Result<(), JsValue>> + 'a>> {
Box::pin(async move {
let entries = dir.entries();
loop {
let next = JsFuture::from(entries.next()?).await?;
if js_sys::Reflect::get(&next, &JsValue::from_str("done"))?
.as_bool()
.unwrap_or(false)
{
break;
}
let pair: Array = js_sys::Reflect::get(&next, &JsValue::from_str("value"))?.into();
let name = pair.get(0).as_string().unwrap_or_default();
let handle: FileSystemHandle = pair.get(1).unchecked_into();
let path = if prefix.is_empty() {
name.clone()
} else {
format!("{prefix}/{name}")
};
match handle.kind() {
FileSystemHandleKind::File => {
out.push((path, handle.unchecked_into()));
}
_ => {
let sub: FileSystemDirectoryHandle = handle.unchecked_into();
walk(&sub, path, out).await?;
}
}
}
Ok(())
})
}
impl ReadableStorageTraits for OpfsStore {
fn get_partial_many<'a>(
&'a self,
key: &StoreKey,
byte_ranges: ByteRangeIterator<'a>,
) -> std::result::Result<MaybeBytesIterator<'a>, StorageError> {
if let Some(whole) = self.staged.borrow().get(key).cloned() {
let size = whole.len() as u64;
let mut out: Vec<std::result::Result<Bytes, StorageError>> = Vec::new();
for br in byte_ranges {
#[allow(clippy::cast_possible_truncation)]
let start = br.start(size) as usize;
#[allow(clippy::cast_possible_truncation)]
let end = start + br.length(size) as usize;
out.push(if end > whole.len() {
Err(StorageError::Other(format!(
"byte range {start}..{end} out of bounds for {} bytes",
whole.len()
)))
} else {
Ok(whole.slice(start..end))
});
}
return Ok(Some(Box::new(out.into_iter())));
}
let files = self.files.borrow();
let Some(file) = files.get(key) else {
return Ok(None);
};
let size = file.size()?;
let mut out: Vec<std::result::Result<Bytes, StorageError>> = Vec::new();
for br in byte_ranges {
let start = br.start(size);
let len = br.length(size);
#[allow(clippy::cast_possible_truncation)]
out.push(file.read_at(start, len as usize));
}
Ok(Some(Box::new(out.into_iter())))
}
fn size_key(&self, key: &StoreKey) -> std::result::Result<Option<u64>, StorageError> {
if let Some(b) = self.staged.borrow().get(key) {
return Ok(Some(b.len() as u64));
}
self.files.borrow().get(key).map(OpenFile::size).transpose()
}
fn supports_get_partial(&self) -> bool {
true
}
}
impl WritableStorageTraits for OpfsStore {
fn set(&self, key: &StoreKey, value: Bytes) -> std::result::Result<(), StorageError> {
self.erased.borrow_mut().remove(key);
self.staged.borrow_mut().insert(key.clone(), value);
Ok(())
}
fn set_partial_many(
&self,
key: &StoreKey,
offset_values: OffsetBytesIterator,
) -> std::result::Result<(), StorageError> {
zarrs::storage::store_set_partial_many(self, key, offset_values)
}
fn erase(&self, key: &StoreKey) -> std::result::Result<(), StorageError> {
self.staged.borrow_mut().remove(key);
if self.files.borrow_mut().remove(key).is_some() {
self.erased.borrow_mut().insert(key.clone());
}
Ok(())
}
fn erase_prefix(&self, prefix: &StorePrefix) -> std::result::Result<(), StorageError> {
for key in self.known_keys() {
if key.has_prefix(prefix) {
self.erase(&key)?;
}
}
Ok(())
}
fn supports_set_partial(&self) -> bool {
false
}
}
impl ListableStorageTraits for OpfsStore {
fn list(&self) -> std::result::Result<StoreKeys, StorageError> {
Ok(self.known_keys())
}
fn list_prefix(&self, prefix: &StorePrefix) -> std::result::Result<StoreKeys, StorageError> {
Ok(self
.known_keys()
.into_iter()
.filter(|k| k.has_prefix(prefix))
.collect())
}
fn list_dir(
&self,
prefix: &StorePrefix,
) -> std::result::Result<StoreKeysPrefixes, StorageError> {
let mut keys: StoreKeys = Vec::new();
let mut prefixes: BTreeSet<String> = BTreeSet::new();
for key in self.known_keys() {
if !key.has_prefix(prefix) {
continue;
}
let rest = &key.as_str()[prefix.as_str().len()..];
match rest.split_once('/') {
None => keys.push(key),
Some((head, _)) => {
prefixes.insert(format!("{}{head}/", prefix.as_str()));
}
}
}
let prefixes: StorePrefixes = prefixes
.into_iter()
.map(StorePrefix::new)
.collect::<std::result::Result<_, _>>()
.map_err(|e| StorageError::Other(e.to_string()))?;
Ok(StoreKeysPrefixes::new(keys, prefixes))
}
fn size_prefix(&self, prefix: &StorePrefix) -> std::result::Result<u64, StorageError> {
let mut total = 0;
for key in self.list_prefix(prefix)? {
total += self.size_key(&key)?.unwrap_or(0);
}
Ok(total)
}
}
pub async fn write_zarr_opfs(
dir: &FileSystemDirectoryHandle,
base_url: &str,
schema: &OutputSchema,
) -> Result<u64> {
let store = Arc::new(OpfsStore::staging());
write_all_to_store(store.clone(), base_url, schema)?;
store
.commit(dir)
.await
.map_err(|e| opfs_err("commit the Zarr store to OPFS", &e))?;
let written = OpfsStore::open(dir)
.await
.map_err(|e| opfs_err("reopen the committed store", &e))?;
let bytes = written.stored_bytes().map_err(|e| Error::Format {
format: "zarr".to_string(),
detail: format!("size the committed store: {e}"),
})?;
written.close();
Ok(bytes)
}
pub async fn read_zarr_opfs_array(dir: &FileSystemDirectoryHandle, name: &str) -> Result<Vec<f64>> {
let store = Arc::new(
OpfsStore::open(dir)
.await
.map_err(|e| opfs_err("open the OPFS store", &e))?,
);
let result = (|| {
let array =
ZarrArray::open(store.clone(), &format!("/{name}")).map_err(|e| Error::Format {
format: "zarr".to_string(),
detail: format!("open array '{name}': {e}"),
})?;
array
.retrieve_array_subset::<Vec<f64>>(&array.subset_all())
.map_err(|e| Error::Format {
format: "zarr".to_string(),
detail: format!("decode array '{name}': {e}"),
})
})();
store.close();
result
}