Skip to main content

hydroperx_wwwfs/
lib.rs

1use std::fmt::Debug;
2use futures::Stream;
3use futures::StreamExt;
4use js_sys::{ArrayBuffer, AsyncIterator, Uint8Array};
5use wasm_bindgen::{JsCast, JsValue};
6use wasm_bindgen_futures::{JsFuture, stream::JsStream};
7
8pub struct GetFileHandleOptions {
9    pub create: bool,
10}
11
12impl Default for GetFileHandleOptions {
13    fn default() -> Self {
14        Self {
15            create: false,
16        }
17    }
18}
19
20pub struct GetDirectoryHandleOptions {
21    pub create: bool,
22}
23
24impl Default for GetDirectoryHandleOptions {
25    fn default() -> Self {
26        Self {
27            create: false,
28        }
29    }
30}
31
32pub struct CreateWritableOptions {
33    pub keep_existing_data: bool,
34}
35
36impl Default for CreateWritableOptions {
37    fn default() -> Self {
38        Self {
39            keep_existing_data: false,
40        }
41    }
42}
43
44pub struct FileSystemRemoveOptions {
45    pub recursive: bool,
46}
47
48impl Default for FileSystemRemoveOptions {
49    fn default() -> Self {
50        Self {
51            recursive: false,
52        }
53    }
54}
55
56#[derive(Debug, Clone)]
57pub enum DirectoryEntry {
58    File(FileHandle),
59    Directory(DirectoryHandle),
60}
61
62/// Returns the Origin Private File System's root directory.
63pub async fn storage_directory() -> std::io::Result<DirectoryHandle> {
64    use wasm_bindgen_futures::JsFuture;
65    use web_sys::FileSystemDirectoryHandle;
66
67    let window = web_sys::window().ok_or(std::io::Error::new(std::io::ErrorKind::Other, "No window object"))?;
68    let navigator = window.navigator();
69
70    let root_directory_handle =
71        FileSystemDirectoryHandle::from(map_io_result(JsFuture::from(navigator.storage().get_directory()).await)?);
72
73    Ok(DirectoryHandle::from(root_directory_handle))
74}
75
76#[derive(Debug, Clone)]
77pub struct DirectoryHandle(web_sys::FileSystemDirectoryHandle);
78
79#[derive(Debug, Clone)]
80pub struct FileHandle(web_sys::FileSystemFileHandle);
81
82#[derive(Debug, Clone)]
83pub struct WritableFileStream(web_sys::FileSystemWritableFileStream);
84
85#[derive(Debug, Clone)]
86pub struct Blob(web_sys::Blob);
87
88#[derive(Debug, Clone)]
89pub struct File(web_sys::File);
90
91#[derive(Debug, Clone)]
92pub struct FileList(web_sys::FileList);
93
94pub struct FileListIter {
95    list: FileList,
96    index: usize,
97}
98
99impl From<web_sys::FileSystemDirectoryHandle> for DirectoryHandle {
100    fn from(handle: web_sys::FileSystemDirectoryHandle) -> Self {
101        Self(handle)
102    }
103}
104
105impl From<web_sys::FileSystemFileHandle> for FileHandle {
106    fn from(handle: web_sys::FileSystemFileHandle) -> Self {
107        Self(handle)
108    }
109}
110
111impl From<web_sys::FileSystemWritableFileStream> for WritableFileStream {
112    fn from(handle: web_sys::FileSystemWritableFileStream) -> Self {
113        Self(handle)
114    }
115}
116
117impl From<web_sys::Blob> for Blob {
118    fn from(handle: web_sys::Blob) -> Self {
119        Self(handle)
120    }
121}
122
123impl From<web_sys::File> for File {
124    fn from(handle: web_sys::File) -> Self {
125        Self(handle)
126    }
127}
128
129impl From<web_sys::FileList> for FileList {
130    fn from(handle: web_sys::FileList) -> Self {
131        Self(handle)
132    }
133}
134
135impl DirectoryHandle {
136    /// Entry name (last portion of the path).
137    pub fn name(&self) -> String {
138        self.0.name()
139    }
140
141    /// Determines if two directory entries are equal.
142    pub async fn eq(&self, other: &Self) -> bool {
143        map_io_result(JsFuture::from(self.0.is_same_entry(&other.0.clone().dyn_into().unwrap())).await).unwrap().is_truthy()
144    }
145
146    pub async fn get_file_handle(&self, name: &str) -> std::io::Result<FileHandle> {
147        self.get_file_handle_with_options(name, &Default::default()).await
148    }
149
150    pub async fn get_file_handle_with_options(
151        &self,
152        name: &str,
153        options: &crate::GetFileHandleOptions,
154    ) -> std::io::Result<FileHandle> {
155        let fs_options = web_sys::FileSystemGetFileOptions::new();
156        fs_options.set_create(options.create);
157        let file_system_file_handle = web_sys::FileSystemFileHandle::from(
158            map_io_result(JsFuture::from(self.0.get_file_handle_with_options(name, &fs_options)).await)?,
159        );
160        Ok(FileHandle(file_system_file_handle))
161    }
162
163    pub async fn get_directory_handle(&self, name: &str) -> std::io::Result<Self> {
164        self.get_directory_handle_with_options(name, &Default::default()).await
165    }
166
167    pub async fn get_directory_handle_with_options(
168        &self,
169        name: &str,
170        options: &crate::GetDirectoryHandleOptions,
171    ) -> std::io::Result<Self> {
172        let fs_options = web_sys::FileSystemGetDirectoryOptions::new();
173        fs_options.set_create(options.create);
174        let file_system_directory_handle = web_sys::FileSystemDirectoryHandle::from(
175            map_io_result(JsFuture::from(self.0.get_directory_handle_with_options(name, &fs_options)).await)?,
176        );
177        Ok(DirectoryHandle(file_system_directory_handle))
178    }
179
180    pub async fn remove_entry(&mut self, name: &str) -> std::io::Result<()> {
181        map_io_result(JsFuture::from(self.0.remove_entry(name)).await)?;
182        Ok(())
183    }
184
185    pub async fn remove_entry_with_options(
186        &mut self,
187        name: &str,
188        options: &FileSystemRemoveOptions,
189    ) -> std::io::Result<()> {
190        let fs_options = web_sys::FileSystemRemoveOptions::new();
191        fs_options.set_recursive(options.recursive);
192        map_io_result(JsFuture::from(self.0.remove_entry_with_options(name, &fs_options)).await)?;
193        Ok(())
194    }
195
196    pub async fn entries(
197        &self,
198    ) -> std::io::Result<impl Stream<Item = std::io::Result<(String, DirectoryEntry)>>>
199    {
200        let entries_iterator = self.0.entries();
201        let async_iterator = AsyncIterator::from(entries_iterator);
202        let js_stream: JsStream = JsStream::from(async_iterator);
203
204        let stream = js_stream.map(|item| {
205            map_io_result(match item {
206                Ok(js_array) => {
207                    // entries() returns [key, value] pairs
208                    let array = js_sys::Array::from(&js_array);
209                    let filename = array
210                        .get(0)
211                        .as_string()
212                        .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "Invalid filename"))?;
213                    let handle = array.get(1);
214
215                    // Determine if it's a file or directory handle
216                    let entry = if handle.has_type::<web_sys::FileSystemFileHandle>() {
217                        DirectoryEntry::File(FileHandle(web_sys::FileSystemFileHandle::from(handle)))
218                    } else if handle.has_type::<web_sys::FileSystemDirectoryHandle>() {
219                        DirectoryEntry::Directory(DirectoryHandle(web_sys::FileSystemDirectoryHandle::from(
220                            handle,
221                        )))
222                    } else {
223                        return Err(std::io::Error::new(std::io::ErrorKind::Other, "Unknown handle type"));
224                    };
225
226                    Ok((filename, entry))
227                }
228                Err(e) => Err(e),
229            })
230        });
231
232        Ok(stream)
233    }
234}
235
236impl FileHandle {
237    /// Filename.
238    pub fn name(&self) -> String {
239        self.0.name()
240    }
241
242    /// Determines if two file entries are equal.
243    pub async fn eq(&self, other: &Self) -> bool {
244        map_io_result(JsFuture::from(self.0.is_same_entry(&other.0.clone().dyn_into().unwrap())).await).unwrap().is_truthy()
245    }
246
247    pub async fn create_writable(&mut self) -> std::io::Result<WritableFileStream> {
248        self.create_writable_with_options(&Default::default()).await
249    }
250
251    pub async fn create_writable_with_options(
252        &mut self,
253        options: &crate::CreateWritableOptions,
254    ) -> std::io::Result<WritableFileStream> {
255        let fs_options = web_sys::FileSystemCreateWritableOptions::new();
256        fs_options.set_keep_existing_data(options.keep_existing_data);
257        let file_system_writable_file_stream = web_sys::FileSystemWritableFileStream::unchecked_from_js(
258            map_io_result(JsFuture::from(self.0.create_writable_with_options(&fs_options)).await)?,
259        );
260        Ok(WritableFileStream(file_system_writable_file_stream))
261    }
262
263    pub async fn read(&self) -> std::io::Result<Vec<u8>> {
264        self.get_blob().await?.binary().await
265    }
266
267    pub async fn size(&self) -> std::io::Result<usize> {
268        let size = self.get_blob().await?.size();
269        Ok(size)
270    }
271}
272
273impl FileHandle {
274    pub async fn get_blob(&self) -> std::io::Result<Blob> {
275        let file: web_sys::Blob = map_io_result(JsFuture::from(self.0.get_file()).await)?.into();
276        Ok(Blob(file))
277    }
278
279    pub async fn get_file(&self) -> std::io::Result<File> {
280        let file: web_sys::File = map_io_result(JsFuture::from(self.0.get_file()).await)?.into();
281        Ok(File(file))
282    }
283}
284
285impl WritableFileStream {
286    pub async fn write(&mut self, data: Vec<u8>) -> std::io::Result<()> {
287        // You'd think we could just do
288        // ```
289        // JsFuture::from(self.0.write_with_u8_array(data.as_mut_slice())?).await?;
290        // ```
291        // But a safari bug makes this write basically the entire wasm heap to the file.
292        // So we have to write as a blob first.
293
294        let uint8_array = js_sys::Uint8Array::from(data.as_slice());
295        let array = js_sys::Array::new();
296        array.push(&uint8_array);
297        let blob = map_io_result(web_sys::Blob::new_with_u8_array_sequence(&array))?;
298
299        map_io_result(JsFuture::from(map_io_result(self.0.write_with_blob(&blob))?).await)?;
300        Ok(())
301    }
302
303    pub async fn close(&mut self) -> std::io::Result<()> {
304        map_io_result(JsFuture::from(self.0.close()).await)?;
305        Ok(())
306    }
307
308    pub async fn seek(&mut self, offset: usize) -> std::io::Result<()> {
309        map_io_result(JsFuture::from(map_io_result(self.0.seek_with_u32(offset as u32))?).await)?;
310        Ok(())
311    }
312}
313
314impl Blob {
315    pub fn size(&self) -> usize {
316        self.0.size() as usize
317    }
318
319    pub async fn binary(&self) -> std::io::Result<Vec<u8>> {
320        let buffer = ArrayBuffer::unchecked_from_js(map_io_result(JsFuture::from(self.0.array_buffer()).await)?);
321        let uint8_array = Uint8Array::new(&buffer);
322        let mut vec = vec![0; self.size()];
323        uint8_array.copy_to(&mut vec);
324        Ok(vec)
325    }
326
327    #[allow(dead_code)]
328    pub async fn text(&self) -> std::io::Result<String> {
329        map_io_result(JsFuture::from(self.0.text()).await)?
330            .as_string()
331            .ok_or(std::io::Error::new(std::io::ErrorKind::Other, "Unknown error"))
332    }
333}
334
335impl File {
336    /// Last modified timestamp since the UNIX epoch.
337    pub fn last_modified(&self) -> std::time::SystemTime {
338        std::time::SystemTime::UNIX_EPOCH.checked_add(
339            std::time::Duration::from_millis(unsafe { self.0.last_modified().to_int_unchecked() })
340        ).unwrap()
341    }
342
343    /// Filename.
344    pub fn name(&self) -> String {
345        self.0.name()
346    }
347
348    /// Returns the inherited `Blob` interface.
349    pub fn as_blob(&self) -> Blob {
350        Blob::from(self.0.clone().dyn_into::<web_sys::Blob>().unwrap())
351    }
352}
353
354impl FileList {
355    pub fn len(&self) -> usize {
356        self.0.length() as usize
357    }
358
359    pub fn iter(&self) -> FileListIter {
360        self.clone().into_iter()
361    }
362
363    pub fn get(&self, index: usize) -> Option<File> {
364        self.0.item(index as u32).map(|f| File::from(f))
365    }
366}
367
368impl std::iter::IntoIterator for FileList {
369    type Item = File;
370    type IntoIter = FileListIter;
371
372    fn into_iter(self) -> Self::IntoIter {
373        FileListIter {
374            list: self,
375            index: 0,
376        }
377    }
378}
379
380impl std::iter::Iterator for FileListIter {
381    type Item = File;
382
383    fn next(&mut self) -> Option<Self::Item> {
384        let Some(item) = self.list.0.item(self.index as u32) else {
385            return None;
386        };
387        self.index += 1;
388        Some(File::from(item))
389    }
390}
391
392fn map_io_result<T>(result: Result<T, JsValue>) -> std::io::Result<T> {
393    result.map_err(|e| {
394        use wasm_bindgen::JsCast;
395        let Ok(e) = e.dyn_into::<js_sys::Error>() else {
396            return std::io::Error::new(std::io::ErrorKind::Other, "Unknown error");
397        };
398        std::io::Error::new(std::io::ErrorKind::Other, e.to_string().as_string().unwrap())
399    })
400}