use js_sys::{Array, Reflect};
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use web_sys::{
FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetDirectoryOptions,
FileSystemGetFileOptions, FileSystemReadWriteOptions, FileSystemSyncAccessHandle,
};
use super::BrowserSourceError;
use super::js::haem_list_files;
pub(super) async fn get_directory(
parent: &FileSystemDirectoryHandle,
name: &str,
create: bool,
) -> Result<Option<FileSystemDirectoryHandle>, BrowserSourceError> {
let options = FileSystemGetDirectoryOptions::new();
options.set_create(create);
match JsFuture::from(parent.get_directory_handle_with_options(name, &options)).await {
Ok(value) => value
.dyn_into()
.map(Some)
.map_err(|_| BrowserSourceError::backend("directory cast", "invalid handle")),
Err(error) if !create && js_name(&error) == "NotFoundError" => Ok(None),
Err(error) => Err(js_source("get directory handle", &error)),
}
}
pub(super) async fn get_file(
parent: &FileSystemDirectoryHandle,
name: &str,
create: bool,
) -> Result<FileSystemFileHandle, BrowserSourceError> {
let options = FileSystemGetFileOptions::new();
options.set_create(create);
JsFuture::from(parent.get_file_handle_with_options(name, &options))
.await
.map_err(|error| js_source("get file handle", &error))?
.dyn_into()
.map_err(|_| BrowserSourceError::backend("file cast", "invalid file handle"))
}
pub(super) async fn sync_handle(
file: &FileSystemFileHandle,
) -> Result<FileSystemSyncAccessHandle, BrowserSourceError> {
JsFuture::from(file.create_sync_access_handle())
.await
.map_err(|error| js_source("create sync access handle", &error))?
.dyn_into()
.map_err(|_| BrowserSourceError::backend("access cast", "invalid sync access handle"))
}
pub(super) async fn write_flushed(
directory: &FileSystemDirectoryHandle,
name: &str,
bytes: &[u8],
) -> Result<(), BrowserSourceError> {
let file = get_file(directory, name, true).await?;
let access = sync_handle(&file).await?;
write_access(&access, bytes)?;
access
.flush()
.map_err(|error| js_source("flush sync access handle", &error))?;
access.close();
Ok(())
}
pub(super) fn write_access(
access: &FileSystemSyncAccessHandle,
bytes: &[u8],
) -> Result<(), BrowserSourceError> {
access
.truncate_with_u32(0)
.map_err(|error| js_source("truncate before write", &error))?;
let options = FileSystemReadWriteOptions::new();
options.set_at(0.0);
let written = access
.write_with_u8_array_and_options(bytes, &options)
.map_err(|error| js_source("write sync access handle", &error))?;
let written = f64_to_usize(written, "write byte count")?;
if written != bytes.len() {
return Err(BrowserSourceError::backend(
"write sync access handle",
format!("short write: {written}/{}", bytes.len()),
));
}
let length = u32::try_from(bytes.len())
.map_err(|_| BrowserSourceError::backend("truncate after write", "file too large"))?;
access
.truncate_with_u32(length)
.map_err(|error| js_source("truncate after write", &error))
}
pub(super) async fn read_optional(
directory: &FileSystemDirectoryHandle,
name: &str,
) -> Result<Option<Vec<u8>>, BrowserSourceError> {
let file = match get_file(directory, name, false).await {
Ok(file) => file,
Err(BrowserSourceError::JavaScript { name, .. }) if name == "NotFoundError" => {
return Ok(None);
}
Err(error) => return Err(error),
};
let access = sync_handle(&file).await?;
let bytes = read_access(&access)?;
access.close();
Ok(Some(bytes))
}
pub(super) async fn read_required(
directory: &FileSystemDirectoryHandle,
name: &str,
) -> Result<Vec<u8>, BrowserSourceError> {
read_optional(directory, name).await?.ok_or_else(|| {
BrowserSourceError::backend("read required file", format!("NotFoundError: {name}"))
})
}
pub(super) fn read_access(
access: &FileSystemSyncAccessHandle,
) -> Result<Vec<u8>, BrowserSourceError> {
let size = access
.get_size()
.map_err(|error| js_source("get file size", &error))?;
if !(size.is_finite() && size >= 0.0 && size.fract() == 0.0 && size <= u32::MAX.into()) {
return Err(BrowserSourceError::backend("get file size", "invalid size"));
}
let size = f64_to_usize(size, "read byte count")?;
let mut bytes = vec![0; size];
let options = FileSystemReadWriteOptions::new();
options.set_at(0.0);
let read = access
.read_with_u8_array_and_options(&mut bytes, &options)
.map_err(|error| js_source("read sync access handle", &error))?;
let read = f64_to_usize(read, "read byte count")?;
if read != size {
return Err(BrowserSourceError::backend(
"read sync access handle",
format!("short read: {read}/{size}"),
));
}
Ok(bytes)
}
fn f64_to_usize(value: f64, operation: &'static str) -> Result<usize, BrowserSourceError> {
if !(value.is_finite() && value >= 0.0 && value.fract() == 0.0) {
return Err(BrowserSourceError::backend(
operation,
format!("invalid byte count: {value}"),
));
}
format!("{value:.0}").parse().map_err(|_| {
BrowserSourceError::backend(operation, format!("byte count exceeds usize: {value}"))
})
}
pub(super) async fn list_files(
directory: &FileSystemDirectoryHandle,
) -> Result<Vec<String>, BrowserSourceError> {
let value = JsFuture::from(haem_list_files(directory))
.await
.map_err(|error| js_source("list directory files", &error))?;
Array::from(&value)
.iter()
.map(|value| {
value.as_string().ok_or_else(|| {
BrowserSourceError::backend("list directory files", "non-string entry")
})
})
.collect()
}
pub(super) fn js_source(operation: &'static str, value: &JsValue) -> BrowserSourceError {
BrowserSourceError::JavaScript {
operation,
name: js_name(value),
message: Reflect::get(value, &JsValue::from_str("message"))
.ok()
.and_then(|value| value.as_string())
.or_else(|| value.as_string())
.unwrap_or_else(|| format!("{value:?}")),
}
}
fn js_name(value: &JsValue) -> String {
Reflect::get(value, &JsValue::from_str("name"))
.ok()
.and_then(|value| value.as_string())
.unwrap_or_default()
}