browser_fs/
lib.rs

1//! Browser filesystem primitives.
2//!
3//! This crate is an async version of [`std::fs`] that is designed to work in a
4//! browser, only in a web worker.
5//!
6//! It is supposed to provide an API similar to [`async-fs`](https://docs.rs/async-fs/).
7//!
8//! This crate is inspired by [`web-fs`](https://docs.rs/web-fs/) which provides a mechanism of using the browser filesystem
9//! from the main thread, which is not the case for the current implementation,
10//! making it simpler and easier to integrate if your app already works in a web
11//! worker.
12//!
13//! # Known constraints
14//!
15//! ## File size
16//!
17//! `wasm32` only support at most 4GB memory, therefore it's not possible to
18//! handle files bigger than that.
19//!
20//! ## Concurrent access
21//!
22//! The current implementation, based on `web-sys` cannot open multiple file
23//! handler at the same time on the same file. The underneath implementation is
24//! blocking this by returning an error.
25//!
26//! # Examples
27//!
28//! Create a new file and write some bytes to it:
29//!
30//! ```no_run
31//! use browser_fs::File;
32//! use futures_lite::io::AsyncWriteExt;
33//!
34//! # futures_lite::future::block_on(async {
35//! let mut file = File::create("a.txt").await?;
36//! file.write_all(b"Hello, world!").await?;
37//! file.flush().await?;
38//! # std::io::Result::Ok(()) });
39//! ```
40
41use std::io::{Error, ErrorKind, Result};
42
43use js_sys::Promise;
44use wasm_bindgen::{JsCast, JsValue};
45use wasm_bindgen_futures::JsFuture;
46use web_sys::{
47    DomException, Exception, FileSystemDirectoryHandle, FileSystemFileHandle, StorageManager,
48    WorkerGlobalScope,
49};
50
51mod directory;
52mod external;
53mod file;
54mod metadata;
55mod open_options;
56mod read;
57mod seek;
58mod write;
59
60pub use directory::*;
61pub use file::*;
62pub use metadata::*;
63pub use open_options::*;
64
65fn from_js_error(value: JsValue) -> Error {
66    if value.is_instance_of::<DomException>() {
67        let handle = value.unchecked_into::<DomException>();
68        return match handle.name().as_str() {
69            "NotAllowedError" => Error::new(ErrorKind::PermissionDenied, handle.message()),
70            "NotFoundError" => Error::new(ErrorKind::NotFound, handle.message()),
71            "TypeMismatchError" => Error::new(ErrorKind::Other, handle.message()),
72            "InvalidModificationError" => {
73                Error::new(ErrorKind::DirectoryNotEmpty, handle.message())
74            }
75            _ => Error::other(handle.name()),
76        };
77    }
78    if value.is_instance_of::<Exception>() {
79        let handle = value.unchecked_into::<Exception>();
80        return Error::other(handle.name());
81    }
82    if let Some(err) = value.dyn_ref::<web_sys::js_sys::TypeError>() {
83        let message: String = err.message().into();
84        return Error::new(ErrorKind::InvalidInput, message);
85    }
86    Error::other("unknown error")
87}
88
89fn from_js<V: JsCast>(value: JsValue) -> Result<V> {
90    value.dyn_into::<V>().map_err(from_js_error)
91}
92
93async fn resolve<V: JsCast>(promise: Promise) -> Result<V> {
94    from_js(JsFuture::from(promise).await.map_err(from_js_error)?)
95}
96
97async fn resolve_undefined(promise: Promise) -> Result<()> {
98    JsFuture::from(promise).await.map_err(from_js_error)?;
99    Ok(())
100}
101
102async fn storage() -> Result<StorageManager> {
103    if js_sys::global().is_instance_of::<WorkerGlobalScope>() {
104        let global = js_sys::global().unchecked_into::<WorkerGlobalScope>();
105        return Ok(global.navigator().storage());
106    }
107    Err(Error::new(
108        ErrorKind::Unsupported,
109        "storage manage not accessible",
110    ))
111}
112
113async fn root_directory() -> Result<FileSystemDirectoryHandle> {
114    let storage = storage().await?;
115    let res = JsFuture::from(storage.get_directory())
116        .await
117        .map_err(|_| Error::new(ErrorKind::Unsupported, "unable to access root directory"))?;
118    res.dyn_into::<FileSystemDirectoryHandle>()
119        .map_err(|_| Error::new(ErrorKind::Unsupported, "unable to cast root directory"))
120}
121
122#[derive(Clone, Copy, Debug)]
123pub enum FileType {
124    File,
125    Directory,
126}
127
128impl FileType {
129    pub const fn is_dir(&self) -> bool {
130        matches!(self, Self::Directory)
131    }
132
133    pub const fn is_file(&self) -> bool {
134        matches!(self, Self::File)
135    }
136
137    pub const fn is_symlink(&self) -> bool {
138        false
139    }
140}
141
142#[derive(Debug)]
143enum Entry {
144    Directory(FileSystemDirectoryHandle),
145    File(FileSystemFileHandle),
146}
147
148impl Entry {
149    fn name(&self) -> String {
150        match self {
151            Self::Directory(inner) => inner.name(),
152            Self::File(inner) => inner.name(),
153        }
154    }
155
156    fn try_from_js_value(value: JsValue) -> Result<Self> {
157        if value.is_instance_of::<FileSystemFileHandle>() {
158            Ok(Self::File(value.unchecked_into()))
159        } else if value.is_instance_of::<FileSystemDirectoryHandle>() {
160            Ok(Self::Directory(value.unchecked_into()))
161        } else {
162            Err(Error::new(ErrorKind::InvalidData, "unable to caste handle"))
163        }
164    }
165
166    fn file_type(&self) -> FileType {
167        match self {
168            Self::Directory(_) => FileType::Directory,
169            Self::File(_) => FileType::File,
170        }
171    }
172}
173
174impl Entry {
175    async fn from_directory(parent: &FileSystemDirectoryHandle, name: &str) -> Result<Self> {
176        let dir_promise = parent.get_directory_handle(name);
177        let dir_res = crate::resolve::<FileSystemDirectoryHandle>(dir_promise).await;
178
179        let file_promise = parent.get_file_handle(name);
180        let file_res = crate::resolve::<FileSystemFileHandle>(file_promise).await;
181
182        dir_res.map(Self::Directory).or(file_res.map(Self::File))
183    }
184}