use wasm_bindgen::throw_str;
pub mod callbacks {
use crate::{
blob::Blob,
file_reader::{FileReadError, ReadyState},
};
use gloo_events::EventListener;
use std::{cell::RefCell, rc::Rc};
use wasm_bindgen::{prelude::*, throw_str, JsCast, UnwrapThrowExt};
#[derive(Debug)]
pub struct FileReader {
reader: web_sys::FileReader,
_load_listener: EventListener,
_error_listener: EventListener,
}
impl std::ops::Drop for FileReader {
fn drop(&mut self) {
if !ReadyState::from(self.reader.ready_state()).is_done() {
self.reader.abort();
}
}
}
pub fn read_as_text<F>(blob: &Blob, callback: F) -> FileReader
where
F: FnOnce(Result<String, FileReadError>) + 'static,
{
read(
blob,
callback,
|value| value.as_string().unwrap_throw(),
|reader, blob| reader.read_as_text(blob).unwrap_throw(),
)
}
pub fn read_as_data_url<F>(blob: &Blob, callback: F) -> FileReader
where
F: FnOnce(Result<String, FileReadError>) + 'static,
{
read(
blob,
callback,
|value| value.as_string().unwrap_throw(),
|reader, blob| reader.read_as_data_url(blob).unwrap_throw(),
)
}
pub fn read_as_array_buffer<F>(blob: &Blob, callback: F) -> FileReader
where
F: FnOnce(Result<js_sys::ArrayBuffer, FileReadError>) + 'static,
{
read(
blob,
callback,
|value| value.dyn_into::<js_sys::ArrayBuffer>().unwrap_throw(),
|reader, blob| reader.read_as_array_buffer(blob).unwrap_throw(),
)
}
pub fn read_as_bytes<F>(blob: &Blob, callback: F) -> FileReader
where
F: FnOnce(Result<Vec<u8>, FileReadError>) + 'static,
{
read_as_array_buffer(blob, move |result| {
callback(result.map(|buffer| js_sys::Uint8Array::new(&buffer).to_vec()));
})
}
fn read<T, CF, EF, RF>(blob: &Blob, callback: CF, extract_fn: EF, read_fn: RF) -> FileReader
where
CF: FnOnce(Result<T, FileReadError>) + 'static,
EF: Fn(JsValue) -> T + 'static,
RF: Fn(&web_sys::FileReader, &web_sys::Blob) + 'static,
{
let load_callback: Rc<RefCell<Option<CF>>> = Rc::new(RefCell::new(Some(callback)));
let error_callback = load_callback.clone();
let reader = web_sys::FileReader::new().unwrap_throw();
let load_reader = reader.clone();
let error_reader = reader.clone();
let load_listener = EventListener::new(&reader, "load", move |_event| {
let result = extract_fn(load_reader.result().unwrap_throw());
let callback = load_callback.borrow_mut().take().unwrap_throw();
callback(Ok(result));
});
let error_listener = EventListener::new(&reader, "error", move |_event| {
let exception = error_reader.error().unwrap_throw();
let error = match exception.name().as_str() {
"NotFoundError" => FileReadError::NotFound(exception.message()),
"NotReadableError" => FileReadError::NotReadable(exception.message()),
"SecurityError" => FileReadError::Security(exception.message()),
_ => throw_str("unrecognised error type"),
};
let callback = error_callback.borrow_mut().take().unwrap_throw();
callback(Err(error));
});
read_fn(&reader, blob.as_ref());
FileReader {
reader,
_load_listener: load_listener,
_error_listener: error_listener,
}
}
}
#[cfg(feature = "futures")]
pub mod futures {
use crate::{Blob, FileReadError};
use std::future::Future;
use wasm_bindgen::UnwrapThrowExt;
pub fn read_as_text(blob: &Blob) -> impl Future<Output = Result<String, FileReadError>> {
let (sender, receiver) = futures_channel::oneshot::channel();
let reader = super::callbacks::read_as_text(blob, |result| {
sender.send(result).unwrap_throw();
});
async move {
let output = receiver.await.unwrap_throw();
drop(reader);
output
}
}
pub fn read_as_data_url(blob: &Blob) -> impl Future<Output = Result<String, FileReadError>> {
let (sender, receiver) = futures_channel::oneshot::channel();
let reader = super::callbacks::read_as_data_url(blob, |result| {
sender.send(result).unwrap_throw();
});
async move {
let output = receiver.await.unwrap_throw();
drop(reader);
output
}
}
pub fn read_as_array_buffer(
blob: &Blob,
) -> impl Future<Output = Result<js_sys::ArrayBuffer, FileReadError>> {
let (sender, receiver) = futures_channel::oneshot::channel();
let reader = super::callbacks::read_as_array_buffer(blob, |result| {
sender.send(result).unwrap_throw();
});
async move {
let output = receiver.await.unwrap_throw();
drop(reader);
output
}
}
pub fn read_as_bytes(blob: &Blob) -> impl Future<Output = Result<Vec<u8>, FileReadError>> {
let (sender, receiver) = futures_channel::oneshot::channel();
let reader = super::callbacks::read_as_bytes(blob, |result| {
sender.send(result).unwrap_throw();
});
async move {
let output = receiver.await.unwrap_throw();
drop(reader);
output
}
}
}
enum ReadyState {
Empty,
Loading,
Done,
}
impl ReadyState {
fn is_done(&self) -> bool {
matches!(self, ReadyState::Done)
}
}
impl From<u16> for ReadyState {
fn from(val: u16) -> Self {
match val {
0 => ReadyState::Empty,
1 => ReadyState::Loading,
2 => ReadyState::Done,
_ => throw_str("got invalid value for FileReader.readyState"),
}
}
}
#[derive(Debug)]
pub enum FileReadError {
AbortedEarly,
NotFound(String),
NotReadable(String),
Security(String),
}
impl std::fmt::Display for FileReadError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
FileReadError::AbortedEarly => write!(f, "FileReader aborted early"),
FileReadError::NotFound(msg) => write!(f, "FileReader cannot find blob: {msg}"),
FileReadError::NotReadable(msg) => {
write!(f, "FileReader cannot read contents of blob: {msg}")
}
FileReadError::Security(msg) => {
write!(f, "FileReader encountered a security exception: {msg}")
}
}
}
}
impl std::error::Error for FileReadError {}