gloo_file/
file_reader.rs

1use wasm_bindgen::throw_str;
2
3pub mod callbacks {
4    use crate::{
5        blob::Blob,
6        file_reader::{FileReadError, ReadyState},
7    };
8    use gloo_events::EventListener;
9    use std::{cell::RefCell, rc::Rc};
10    use wasm_bindgen::{prelude::*, throw_str, JsCast, UnwrapThrowExt};
11
12    /// A guard object that aborts the file read when dropped (if the read isn't already
13    /// finished).
14    #[derive(Debug)]
15    pub struct FileReader {
16        reader: web_sys::FileReader,
17        _load_listener: EventListener,
18        _error_listener: EventListener,
19    }
20
21    impl std::ops::Drop for FileReader {
22        fn drop(&mut self) {
23            if !ReadyState::from(self.reader.ready_state()).is_done() {
24                self.reader.abort();
25            }
26        }
27    }
28
29    /// Asynchronously converts `blob` into a text string and then passes it to the `callback`.
30    ///
31    /// If the returned `FileReader` is dropped before the callback is called, the read will be
32    /// cancelled.
33    pub fn read_as_text<F>(blob: &Blob, callback: F) -> FileReader
34    where
35        F: FnOnce(Result<String, FileReadError>) + 'static,
36    {
37        read(
38            blob,
39            callback,
40            |value| value.as_string().unwrap_throw(),
41            |reader, blob| reader.read_as_text(blob).unwrap_throw(),
42        )
43    }
44
45    /// Asynchronously converts the `blob` into a base64 encoded `data:` URL and then passes it to
46    /// the `callback`.
47    ///
48    /// If the returned `FileReader` is dropped before the callback is called, the read will be
49    /// cancelled.
50    pub fn read_as_data_url<F>(blob: &Blob, callback: F) -> FileReader
51    where
52        F: FnOnce(Result<String, FileReadError>) + 'static,
53    {
54        read(
55            blob,
56            callback,
57            |value| value.as_string().unwrap_throw(),
58            |reader, blob| reader.read_as_data_url(blob).unwrap_throw(),
59        )
60    }
61
62    /// Asynchronously converts the `blob` into an array buffer and then passes it to the `callback`.
63    ///
64    /// If the returned `FileReader` is dropped before the callback is called, the read will be
65    /// cancelled.
66    pub fn read_as_array_buffer<F>(blob: &Blob, callback: F) -> FileReader
67    where
68        F: FnOnce(Result<js_sys::ArrayBuffer, FileReadError>) + 'static,
69    {
70        read(
71            blob,
72            callback,
73            |value| value.dyn_into::<js_sys::ArrayBuffer>().unwrap_throw(),
74            |reader, blob| reader.read_as_array_buffer(blob).unwrap_throw(),
75        )
76    }
77
78    /// Asynchronously converts the `blob` into a `Vec<u8>` and then passes it to the `callback`.
79    ///
80    /// If the returned `FileReader` is dropped before the callback is called, the read will be
81    /// cancelled.
82    pub fn read_as_bytes<F>(blob: &Blob, callback: F) -> FileReader
83    where
84        F: FnOnce(Result<Vec<u8>, FileReadError>) + 'static,
85    {
86        read_as_array_buffer(blob, move |result| {
87            callback(result.map(|buffer| js_sys::Uint8Array::new(&buffer).to_vec()));
88        })
89    }
90
91    /// Generic function to start the async read of the `FileReader`.
92    ///
93    /// `callback` is the user-supplied callback, `extract_fn` function converts between JsValue
94    /// returned by the read and the type the callback expects, and `read_fn` is the method that
95    /// runs the async read on the `FileReader`.
96    fn read<T, CF, EF, RF>(blob: &Blob, callback: CF, extract_fn: EF, read_fn: RF) -> FileReader
97    where
98        CF: FnOnce(Result<T, FileReadError>) + 'static,
99        EF: Fn(JsValue) -> T + 'static,
100        RF: Fn(&web_sys::FileReader, &web_sys::Blob) + 'static,
101    {
102        // we need to be able to run the FnOnce, while proving to the compiler that it can only run
103        // once. The easiest way is to `take` it out of an Option. The `Rc` and `RefCell` are
104        // because we need shared ownership and mutability (for the `take`).
105        let load_callback: Rc<RefCell<Option<CF>>> = Rc::new(RefCell::new(Some(callback)));
106        let error_callback = load_callback.clone();
107
108        let reader = web_sys::FileReader::new().unwrap_throw();
109
110        let load_reader = reader.clone();
111        let error_reader = reader.clone();
112        // Either the load listener or the error listener will be called, never both (so FnOnce is
113        // ok).
114        let load_listener = EventListener::new(&reader, "load", move |_event| {
115            let result = extract_fn(load_reader.result().unwrap_throw());
116            let callback = load_callback.borrow_mut().take().unwrap_throw();
117            callback(Ok(result));
118        });
119
120        let error_listener = EventListener::new(&reader, "error", move |_event| {
121            let exception = error_reader.error().unwrap_throw();
122            let error = match exception.name().as_str() {
123                "NotFoundError" => FileReadError::NotFound(exception.message()),
124                "NotReadableError" => FileReadError::NotReadable(exception.message()),
125                "SecurityError" => FileReadError::Security(exception.message()),
126                // This branch should never be hit, so returning a less helpful error message is
127                // less of an issue than pulling in `format!` code.
128                _ => throw_str("unrecognised error type"),
129            };
130            let callback = error_callback.borrow_mut().take().unwrap_throw();
131            callback(Err(error));
132        });
133
134        read_fn(&reader, blob.as_ref());
135
136        FileReader {
137            reader,
138            _load_listener: load_listener,
139            _error_listener: error_listener,
140        }
141    }
142}
143
144#[cfg(feature = "futures")]
145pub mod futures {
146    use crate::{Blob, FileReadError};
147    use std::future::Future;
148    use wasm_bindgen::UnwrapThrowExt;
149
150    /// Returns the contents of `blob` as a text string.
151    ///
152    /// Equivalent to `async fn read_as_text(blob: &Blob) -> Result<String, FileReadError>` but
153    /// without borrowing the `Blob` fore the lifetime of the future.
154    pub fn read_as_text(blob: &Blob) -> impl Future<Output = Result<String, FileReadError>> {
155        let (sender, receiver) = futures_channel::oneshot::channel();
156        let reader = super::callbacks::read_as_text(blob, |result| {
157            sender.send(result).unwrap_throw();
158        });
159
160        async move {
161            let output = receiver.await.unwrap_throw();
162            drop(reader);
163            output
164        }
165    }
166
167    /// Returns the contents of `blob` as a base64 encoded `data:` URL.
168    ///
169    /// Equivalent to `async fn read_as_data_url(blob: &Blob) -> Result<String, FileReadError>` but
170    /// without borrowing the `Blob` fore the lifetime of the future.
171    pub fn read_as_data_url(blob: &Blob) -> impl Future<Output = Result<String, FileReadError>> {
172        let (sender, receiver) = futures_channel::oneshot::channel();
173        let reader = super::callbacks::read_as_data_url(blob, |result| {
174            sender.send(result).unwrap_throw();
175        });
176
177        async move {
178            let output = receiver.await.unwrap_throw();
179            drop(reader);
180            output
181        }
182    }
183
184    /// Returns the contents of `blob` as an array buffer.
185    ///
186    /// Equivalent to
187    /// `async fn read_as_array_buffer(blob: &Blob) -> Result<js_sys::ArrayBuffer, FileReadError>`
188    /// but without borrowing the `Blob` fore the lifetime of the future.
189    pub fn read_as_array_buffer(
190        blob: &Blob,
191    ) -> impl Future<Output = Result<js_sys::ArrayBuffer, FileReadError>> {
192        let (sender, receiver) = futures_channel::oneshot::channel();
193        let reader = super::callbacks::read_as_array_buffer(blob, |result| {
194            sender.send(result).unwrap_throw();
195        });
196
197        async move {
198            let output = receiver.await.unwrap_throw();
199            drop(reader);
200            output
201        }
202    }
203
204    /// Returns the contents of `blob` as a `Vec<u8>`.
205    ///
206    /// Equivalent to
207    /// `async fn read_as_bytes(blob: &Blob) -> Result<Vec<u8>, FileReadError>`
208    /// but without borrowing the `Blob` fore the lifetime of the future.
209    pub fn read_as_bytes(blob: &Blob) -> impl Future<Output = Result<Vec<u8>, FileReadError>> {
210        let (sender, receiver) = futures_channel::oneshot::channel();
211        let reader = super::callbacks::read_as_bytes(blob, |result| {
212            sender.send(result).unwrap_throw();
213        });
214
215        async move {
216            let output = receiver.await.unwrap_throw();
217            drop(reader);
218            output
219        }
220    }
221}
222
223enum ReadyState {
224    Empty,
225    Loading,
226    Done,
227}
228
229impl ReadyState {
230    fn is_done(&self) -> bool {
231        matches!(self, ReadyState::Done)
232    }
233}
234
235impl From<u16> for ReadyState {
236    fn from(val: u16) -> Self {
237        match val {
238            0 => ReadyState::Empty,
239            1 => ReadyState::Loading,
240            2 => ReadyState::Done,
241            _ => throw_str("got invalid value for FileReader.readyState"),
242        }
243    }
244}
245
246#[derive(Debug)]
247pub enum FileReadError {
248    AbortedEarly,
249    NotFound(String),
250    NotReadable(String),
251    Security(String),
252}
253
254impl std::fmt::Display for FileReadError {
255    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
256        match self {
257            FileReadError::AbortedEarly => write!(f, "FileReader aborted early"),
258            FileReadError::NotFound(msg) => write!(f, "FileReader cannot find blob: {msg}"),
259            FileReadError::NotReadable(msg) => {
260                write!(f, "FileReader cannot read contents of blob: {msg}")
261            }
262            FileReadError::Security(msg) => {
263                write!(f, "FileReader encountered a security exception: {msg}")
264            }
265        }
266    }
267}
268
269impl std::error::Error for FileReadError {}