dioxus_web/
files.rs

1use dioxus_core::AnyhowContext;
2use dioxus_html::{bytes::Bytes, FileData, NativeFileData};
3use futures_channel::oneshot;
4use js_sys::Uint8Array;
5use send_wrapper::SendWrapper;
6use std::{pin::Pin, prelude::rust_2024::Future};
7use wasm_bindgen::{prelude::Closure, JsCast};
8use web_sys::{File, FileList, FileReader};
9
10/// A file representation for the web platform
11#[derive(Clone)]
12pub struct WebFileData {
13    file: File,
14    reader: FileReader,
15}
16
17unsafe impl Send for WebFileData {}
18unsafe impl Sync for WebFileData {}
19
20impl WebFileData {
21    /// Create a new WebFileData from a web_sys::File
22    pub fn new(file: File, reader: FileReader) -> Self {
23        Self { file, reader }
24    }
25}
26
27impl NativeFileData for WebFileData {
28    fn name(&self) -> String {
29        self.file.name()
30    }
31
32    fn size(&self) -> u64 {
33        self.file.size() as u64
34    }
35
36    fn last_modified(&self) -> u64 {
37        self.file.last_modified() as u64
38    }
39
40    fn read_bytes(
41        &self,
42    ) -> Pin<Box<dyn Future<Output = Result<Bytes, dioxus_core::CapturedError>> + 'static>> {
43        let file_reader = self.reader.clone();
44        let file_reader_ = self.reader.clone();
45        let file = self.file.clone();
46        Box::pin(async move {
47            let (rx, tx) = oneshot::channel();
48            let on_load: Closure<dyn FnMut()> = Closure::new({
49                let mut rx = Some(rx);
50                move || {
51                    let result = file_reader.result();
52                    let _ = rx
53                        .take()
54                        .expect("multiple files read without refreshing the channel")
55                        .send(result);
56                }
57            });
58
59            file_reader_.set_onload(Some(on_load.as_ref().unchecked_ref()));
60            on_load.forget();
61            file_reader_
62                .read_as_array_buffer(&file)
63                .ok()
64                .context("Failed to read file")?;
65
66            let js_val = tx.await?.ok().context("Failed to read file")?;
67            let as_u8_arr = Uint8Array::new(&js_val);
68            let as_u8_vec = as_u8_arr.to_vec().into();
69            Ok(as_u8_vec)
70        })
71    }
72
73    fn read_string(
74        &self,
75    ) -> Pin<Box<dyn Future<Output = Result<String, dioxus_core::CapturedError>> + 'static>> {
76        let file_reader = self.reader.clone();
77        let file_reader_ = self.reader.clone();
78        let file = self.file.clone();
79        Box::pin(async move {
80            let (rx, tx) = oneshot::channel();
81            let on_load: Closure<dyn FnMut()> = Closure::new({
82                let mut rx = Some(rx);
83                move || {
84                    let result = file_reader.result();
85                    let _ = rx
86                        .take()
87                        .expect("multiple files read without refreshing the channel")
88                        .send(result);
89                }
90            });
91
92            file_reader_.set_onload(Some(on_load.as_ref().unchecked_ref()));
93            on_load.forget();
94            file_reader_
95                .read_as_text(&file)
96                .ok()
97                .context("Failed to read file")?;
98
99            let js_val = tx.await?.ok().context("Failed to read file")?;
100            let as_string = js_val.as_string().context("Failed to read file")?;
101            Ok(as_string)
102        })
103    }
104
105    /// we'd like to use `blob` to readable stream here, but we cannot.
106    ///
107    /// We just read the entire file into memory and return it as a single chunk.
108    /// This is not super great, especially given the wasm <-> js boundary duplication cost.
109    ///
110    /// For more efficient streaming of byte data, consider using the dedicated FileStream type which
111    /// goes directly from `File` to fetch request body without going through Rust.
112    ///
113    /// We should maybe update these APIs to use our own custom `ByteBuffer` type to avoid going through `Vec<u8>`?
114    fn byte_stream(
115        &self,
116    ) -> Pin<
117        Box<
118            dyn futures_util::Stream<Item = Result<Bytes, dioxus_core::CapturedError>>
119                + 'static
120                + Send,
121        >,
122    > {
123        let file = self.file.dyn_ref::<web_sys::Blob>().unwrap().clone();
124        Box::pin(SendWrapper::new(futures_util::stream::once(async move {
125            let array_buff = wasm_bindgen_futures::JsFuture::from(file.array_buffer())
126                .await
127                .unwrap();
128            let as_uint_array = array_buff.dyn_into::<Uint8Array>().unwrap();
129            Ok(as_uint_array.to_vec().into())
130        })))
131    }
132
133    fn inner(&self) -> &dyn std::any::Any {
134        &self.file
135    }
136
137    fn path(&self) -> std::path::PathBuf {
138        std::path::PathBuf::from(self.file.name())
139    }
140
141    fn content_type(&self) -> Option<String> {
142        let type_ = self.file.type_();
143        if type_.is_empty() {
144            None
145        } else {
146            Some(type_)
147        }
148    }
149}
150
151/// A file engine for the web platform
152#[derive(Clone)]
153pub(crate) struct WebFileEngine {
154    file_list: FileList,
155}
156
157impl WebFileEngine {
158    /// Create a new file engine from a file list
159    pub fn new(file_list: FileList) -> Self {
160        Self { file_list }
161    }
162
163    fn len(&self) -> usize {
164        self.file_list.length() as usize
165    }
166
167    fn get(&self, index: usize) -> Option<File> {
168        self.file_list.item(index as u32)
169    }
170
171    pub fn to_files(&self) -> Vec<FileData> {
172        (0..self.len())
173            .filter_map(|i| self.get(i))
174            .map(|file| {
175                FileData::new(WebFileData {
176                    file,
177                    reader: FileReader::new().unwrap(),
178                })
179            })
180            .collect()
181    }
182}
183
184/// Helper trait for extracting the underlying `web_sys::File` from a `FileData`
185pub trait WebFileExt {
186    /// returns web_sys::File
187    fn get_web_file(&self) -> Option<web_sys::File>;
188}
189
190impl WebFileExt for FileData {
191    fn get_web_file(&self) -> Option<web_sys::File> {
192        self.inner().downcast_ref::<web_sys::File>().cloned()
193    }
194}