Skip to main content

git_async/web/
mod.rs

1//! An implementation of filesystem operations for the web
2//!
3//! This module implements the `git-async` filesystem operations using either
4//! the [Web File System
5//! API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API) or
6//! the [File and Directory Entries
7//! API](https://developer.mozilla.org/en-US/docs/Web/API/File_and_Directory_Entries_API).
8//!
9//! # Examples
10//!
11//! Using the Web File System API:
12//! ```
13//! # use wasm_bindgen::JsError;
14//! # use git_async::{Repo, RepoConfig, web::{WebDirectory, WebFileSystem}};
15//! async fn open_repo(handle: &web_sys::FileSystemDirectoryHandle) -> Result<Repo<WebFileSystem>, JsError> {
16//!     let repo = Repo::open(WebDirectory::new(handle)?)
17//!         .await
18//!         .map_err(|e| JsError::new(&format!("{:?}", e)))?;
19//!     Ok(repo)
20//! }
21//! ```
22//!
23//! Using the Web File and Directory Entries API:
24//! ```
25//! # use wasm_bindgen::JsError;
26//! # use git_async::{Repo, RepoConfig, web::{WebDirectory, WebFileSystem}};
27//! async fn open_repo(file_list: &web_sys::FileList) -> Result<Repo<WebFileSystem>, JsError> {
28//!     let repo = Repo::open(WebDirectory::new(file_list)?)
29//!         .await
30//!         .map_err(|e| JsError::new(&format!("{:?}", e)))?;
31//!     Ok(repo)
32//! }
33//! ```
34
35use crate::file_system::{DirEntry, Directory, File, FileSystem, FileSystemError, Offset};
36use alloc::{boxed::Box, string::String, vec, vec::Vec};
37use js_sys::{Array, JsString, Promise, Reflect, TypeError, Uint8Array};
38use wasm_bindgen::prelude::*;
39use web_sys::{DomException, FileList, FileSystemDirectoryHandle};
40
41fn to_filesystem_error(value: JsValue) -> FileSystemError {
42    if value.has_type::<DomException>()
43        && Reflect::get(&value, &JsValue::from("name")).unwrap() == "NotFoundError"
44    {
45        FileSystemError::NotFound(Box::new(value))
46    } else if let Ok(s) = value.clone().dyn_into::<JsString>()
47        && s == "file not found"
48    {
49        FileSystemError::NotFound(Box::new(value))
50    } else {
51        FileSystemError::Other(Box::new(value))
52    }
53}
54
55#[wasm_bindgen(module = "/src/web/file-system.js")]
56extern "C" {
57    type DirectoryWrapper;
58    #[wasm_bindgen(constructor)]
59    fn new(inner: &JsValue) -> DirectoryWrapper;
60
61    #[wasm_bindgen(method)]
62    fn openSubdir(this: &DirectoryWrapper, name: &str) -> Promise;
63    #[wasm_bindgen(method)]
64    fn listDir(this: &DirectoryWrapper) -> Promise;
65    #[wasm_bindgen(method)]
66    fn openFile(this: &DirectoryWrapper, name: &str) -> Promise;
67
68    type FileWrapper;
69    #[wasm_bindgen(method)]
70    fn readAll(this: &FileWrapper) -> Promise;
71    #[wasm_bindgen(method)]
72    fn readSegment(this: &FileWrapper, offset: f64, length: f64) -> Promise;
73
74    type FSDirectory;
75    #[wasm_bindgen(constructor)]
76    fn new(handle: &FileSystemDirectoryHandle) -> FSDirectory;
77
78    type EntriesDirectory;
79    fn entriesDirectoryFromFileList(fileList: &FileList) -> EntriesDirectory;
80}
81
82/// File system operations using web APIs
83pub struct WebFileSystem;
84impl FileSystem for WebFileSystem {
85    type File = WebFile;
86    type Directory = WebDirectory;
87}
88
89/// A provider for the [`Directory`] trait which delegates to web APIs
90pub struct WebDirectory {
91    directory: DirectoryWrapper,
92}
93
94impl Clone for WebDirectory {
95    fn clone(&self) -> Self {
96        Self {
97            directory: self.directory.clone().dyn_into().unwrap(),
98        }
99    }
100}
101
102impl WebDirectory {
103    /// Construct a new [`WebDirectory`] from either a
104    /// [`web_sys::FileSystemDirectoryHandle`] or a [`web_sys::FileList`].
105    ///
106    /// If the argument is not one of these, this function returns an `Err`.
107    pub fn new(inner: &JsValue) -> Result<Self, JsError> {
108        if let Ok(handle) = inner.clone().dyn_into::<FileSystemDirectoryHandle>() {
109            let directory = FSDirectory::new(&handle);
110            Ok(Self {
111                directory: DirectoryWrapper::new(&directory),
112            })
113        } else if let Ok(file_list) = inner.clone().dyn_into::<web_sys::FileList>() {
114            let directory = entriesDirectoryFromFileList(&file_list);
115            Ok(Self {
116                directory: DirectoryWrapper::new(&directory),
117            })
118        } else {
119            Err(JsError::new(
120                "must provide either a FileSystemDirectory Handle or a FileList object",
121            ))
122        }
123    }
124}
125
126impl Directory<WebFile> for WebDirectory {
127    async fn open_subdir(&self, name: &[u8]) -> Result<Self, FileSystemError> {
128        let f = async || -> Result<Self, JsValue> {
129            let subdir: DirectoryWrapper = self
130                .directory
131                .openSubdir(str::from_utf8(name).map_err(|_| TypeError::new("name was not UTF-8"))?)
132                .await?
133                .dyn_into()?;
134            Ok(Self { directory: subdir })
135        };
136        f().await.map_err(to_filesystem_error)
137    }
138
139    async fn list_dir(&self) -> Result<Vec<DirEntry>, FileSystemError> {
140        let f = async || -> Result<Vec<DirEntry>, JsValue> {
141            let entries: Array = self.directory.listDir().await?.dyn_into()?;
142            let directories: Array = entries.at(0).dyn_into()?;
143            let files: Array = entries.at(1).dyn_into()?;
144            let mut out: Vec<DirEntry> = Vec::new();
145            for name in directories {
146                let name: JsString = name.dyn_into()?;
147                let name: String = name.into();
148                let name: Vec<u8> = name.into_bytes();
149                out.push(DirEntry::Directory(name));
150            }
151            for name in files {
152                let name: JsString = name.dyn_into()?;
153                let name: String = name.into();
154                let name: Vec<u8> = name.into_bytes();
155                out.push(DirEntry::File(name));
156            }
157            Ok(out)
158        };
159        f().await.map_err(to_filesystem_error)
160    }
161
162    async fn open_file(&self, name: &[u8]) -> Result<WebFile, FileSystemError> {
163        let f = async || -> Result<WebFile, JsValue> {
164            let js_file: FileWrapper = self
165                .directory
166                .openFile(str::from_utf8(name).map_err(|_| TypeError::new("name was not UTF-8"))?)
167                .await?
168                .dyn_into()?;
169            Ok(WebFile { file: js_file })
170        };
171        f().await.map_err(to_filesystem_error)
172    }
173}
174
175/// A provider for the [`File`] trait which delegates to web APIs
176pub struct WebFile {
177    file: FileWrapper,
178}
179
180impl File for WebFile {
181    async fn read_all(&mut self) -> Result<Vec<u8>, FileSystemError> {
182        let f = async || -> Result<Vec<u8>, JsValue> {
183            let data: Uint8Array = self.file.readAll().await?.dyn_into()?;
184            let mut out = vec![0u8; data.length() as usize];
185            data.copy_to(&mut out);
186            Ok(out)
187        };
188        f().await.map_err(to_filesystem_error)
189    }
190
191    async fn read_segment(
192        &mut self,
193        offset: Offset,
194        dest: &mut [u8],
195    ) -> Result<usize, FileSystemError> {
196        let mut f = async || -> Result<usize, JsValue> {
197            assert!(offset.0 <= 2u64.pow(53), "offset not representable as f64");
198            #[allow(clippy::cast_precision_loss)]
199            let offset = offset.0 as f64;
200            assert!(
201                dest.len() as u64 <= 2u64.pow(53),
202                "length not representable as f64"
203            );
204            #[allow(clippy::cast_precision_loss)]
205            let length = dest.len() as f64;
206            let data: Uint8Array = self.file.readSegment(offset, length).await?.dyn_into()?;
207            let bytes_read = data.length() as usize;
208            data.copy_to(&mut dest[0..bytes_read]);
209            Ok(bytes_read)
210        };
211        f().await.map_err(to_filesystem_error)
212    }
213}