sqlite_wasm_rs/vfs/
sahpool.rs

1//! opfs-sahpool vfs implementation, ported from sqlite-wasm.
2//!
3//! See [`opfs-sahpool`](https://sqlite.org/wasm/doc/trunk/persistence.md#vfs-opfs-sahpool) for details.
4//!
5//! ```rust
6//! use sqlite_wasm_rs::{
7//!     self as ffi,
8//!     sahpool_vfs::{install as install_opfs_sahpool, OpfsSAHPoolCfg},
9//! };
10//!
11//! async fn open_db() {
12//!     // install opfs-sahpool persistent vfs and set as default vfs
13//!     install_opfs_sahpool(&OpfsSAHPoolCfg::default(), true)
14//!         .await
15//!         .unwrap();
16//!
17//!     // open with opfs-sahpool vfs
18//!     let mut db = std::ptr::null_mut();
19//!     let ret = unsafe {
20//!         ffi::sqlite3_open_v2(
21//!             c"opfs-sahpool.db".as_ptr().cast(),
22//!             &mut db as *mut _,
23//!             ffi::SQLITE_OPEN_READWRITE | ffi::SQLITE_OPEN_CREATE,
24//!             std::ptr::null()
25//!         )
26//!     };
27//!     assert_eq!(ffi::SQLITE_OK, ret);
28//! }
29//! ```
30//!
31//! The VFS is based on
32//! [`FileSystemSyncAccessHandle`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle)
33//! read and write, and you can install the
34//! [`opfs-explorer`](https://chromewebstore.google.com/detail/opfs-explorer/acndjpgkpaclldomagafnognkcgjignd)
35//! plugin to browse files.
36
37use crate::libsqlite3::*;
38use crate::vfs::utils::{
39    check_import_db, random_name, register_vfs, registered_vfs, ImportDbError, RegisterVfsError,
40    SQLiteIoMethods, SQLiteVfs, VfsAppData, VfsError, VfsFile, VfsResult, VfsStore,
41};
42
43use js_sys::{Array, DataView, IteratorNext, Map, Reflect, Set, Uint8Array};
44use wasm_bindgen::{JsCast, JsValue};
45use wasm_bindgen_futures::JsFuture;
46use web_sys::{
47    FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetDirectoryOptions,
48    FileSystemGetFileOptions, FileSystemReadWriteOptions, FileSystemSyncAccessHandle,
49    WorkerGlobalScope,
50};
51
52const SECTOR_SIZE: usize = 4096;
53const HEADER_MAX_FILENAME_SIZE: usize = 512;
54const HEADER_FLAGS_SIZE: usize = 4;
55const HEADER_CORPUS_SIZE: usize = HEADER_MAX_FILENAME_SIZE + HEADER_FLAGS_SIZE;
56const HEADER_OFFSET_FLAGS: usize = HEADER_MAX_FILENAME_SIZE;
57const HEADER_OFFSET_DATA: usize = SECTOR_SIZE;
58
59// Will be enabled after sqlite fix is released
60//
61// <https://sqlite.org/src/forumpost/042d53c928382021>
62//
63// <https://github.com/sqlite/sqlite-wasm/issues/97>
64//
65// const HEADER_DIGEST_SIZE: usize = 8;
66// const HEADER_OFFSET_DIGEST: usize = HEADER_CORPUS_SIZE;
67
68const PERSISTENT_FILE_TYPES: i32 =
69    SQLITE_OPEN_MAIN_DB | SQLITE_OPEN_MAIN_JOURNAL | SQLITE_OPEN_SUPER_JOURNAL | SQLITE_OPEN_WAL;
70
71type Result<T> = std::result::Result<T, OpfsSAHError>;
72
73fn read_write_options(at: f64) -> FileSystemReadWriteOptions {
74    let options = FileSystemReadWriteOptions::new();
75    options.set_at(at);
76    options
77}
78
79struct OpfsSAHPool {
80    /// Directory handle to the subdir of vfs root which holds
81    /// the randomly-named "opaque" files. This subdir exists in the
82    /// hope that we can eventually support client-created files in
83    dh_opaque: FileSystemDirectoryHandle,
84    /// Buffer used by [sg]et_associated_filename()
85    header_buffer: Uint8Array,
86    /// DataView for self.header_buffer
87    header_buffer_view: DataView,
88    /// Set of currently-unused SAHs
89    available_sah: Set,
90    /// Maps client-side file names to SAHs
91    map_filename_to_sah: Map,
92    /// Maps SAHs to their opaque file names
93    map_sah_to_opaque_name: Map,
94}
95
96impl OpfsSAHPool {
97    async fn new(options: &OpfsSAHPoolCfg) -> Result<OpfsSAHPool> {
98        const OPAQUE_DIR_NAME: &str = ".opaque";
99
100        let vfs_dir = &options.directory;
101        let capacity = options.initial_capacity;
102        let clear_files = options.clear_on_init;
103
104        let create_option = FileSystemGetDirectoryOptions::new();
105        create_option.set_create(true);
106
107        let mut handle: FileSystemDirectoryHandle = JsFuture::from(
108            js_sys::global()
109                .dyn_into::<WorkerGlobalScope>()
110                .map_err(|_| OpfsSAHError::NotSuported)?
111                .navigator()
112                .storage()
113                .get_directory(),
114        )
115        .await
116        .map_err(OpfsSAHError::GetDirHandle)?
117        .into();
118
119        for dir in vfs_dir.split('/').filter(|x| !x.is_empty()) {
120            let next =
121                JsFuture::from(handle.get_directory_handle_with_options(dir, &create_option))
122                    .await
123                    .map_err(OpfsSAHError::GetDirHandle)?
124                    .into();
125            handle = next;
126        }
127
128        let dh_opaque = JsFuture::from(
129            handle.get_directory_handle_with_options(OPAQUE_DIR_NAME, &create_option),
130        )
131        .await
132        .map_err(OpfsSAHError::GetDirHandle)?
133        .into();
134
135        let ap_body = Uint8Array::new_with_length(HEADER_CORPUS_SIZE as _);
136        let dv_body = DataView::new(
137            &ap_body.buffer(),
138            ap_body.byte_offset() as usize,
139            (ap_body.byte_length() - ap_body.byte_offset()) as usize,
140        );
141
142        let pool = Self {
143            dh_opaque,
144            header_buffer: ap_body,
145            header_buffer_view: dv_body,
146            map_filename_to_sah: Map::new(),
147            available_sah: Set::default(),
148            map_sah_to_opaque_name: Map::new(),
149        };
150
151        pool.acquire_access_handles(clear_files).await?;
152        pool.reserve_minimum_capacity(capacity).await?;
153
154        Ok(pool)
155    }
156
157    async fn add_capacity(&self, n: u32) -> Result<u32> {
158        for _ in 0..n {
159            let name = random_name();
160            let handle: FileSystemFileHandle =
161                JsFuture::from(self.dh_opaque.get_file_handle_with_options(&name, &{
162                    let options = FileSystemGetFileOptions::new();
163                    options.set_create(true);
164                    options
165                }))
166                .await
167                .map_err(OpfsSAHError::GetFileHandle)?
168                .into();
169            let sah: FileSystemSyncAccessHandle =
170                JsFuture::from(handle.create_sync_access_handle())
171                    .await
172                    .map_err(OpfsSAHError::CreateSyncAccessHandle)?
173                    .into();
174            self.map_sah_to_opaque_name.set(&sah, &JsValue::from(name));
175            self.set_associated_filename(&sah, None, 0)?;
176        }
177        Ok(self.get_capacity())
178    }
179
180    async fn reserve_minimum_capacity(&self, min: u32) -> Result<()> {
181        self.add_capacity(min.saturating_sub(self.get_capacity()))
182            .await?;
183        Ok(())
184    }
185
186    async fn reduce_capacity(&self, n: u32) -> Result<u32> {
187        let mut result = 0;
188        for sah in Array::from(&self.available_sah) {
189            if result == n || self.get_capacity() == self.get_file_count() {
190                break;
191            }
192            let sah = FileSystemSyncAccessHandle::from(sah);
193            let name = self.map_sah_to_opaque_name.get(&sah).as_string().unwrap();
194
195            sah.close();
196            JsFuture::from(self.dh_opaque.remove_entry(&name))
197                .await
198                .map_err(OpfsSAHError::RemoveEntity)?;
199            self.map_sah_to_opaque_name.delete(&sah);
200            self.available_sah.delete(&sah);
201
202            result += 1;
203        }
204        Ok(result)
205    }
206
207    fn get_capacity(&self) -> u32 {
208        self.map_sah_to_opaque_name.size()
209    }
210
211    fn get_file_count(&self) -> u32 {
212        self.map_filename_to_sah.size()
213    }
214
215    fn get_filenames(&self) -> Vec<String> {
216        self.map_filename_to_sah
217            .keys()
218            .into_iter()
219            .flatten()
220            .map(|x| x.as_string().unwrap())
221            .collect()
222    }
223
224    fn get_associated_filename(&self, sah: &FileSystemSyncAccessHandle) -> Result<Option<String>> {
225        sah.read_with_buffer_source_and_options(&self.header_buffer, &read_write_options(0.0))
226            .map_err(OpfsSAHError::Read)?;
227        let flags = self.header_buffer_view.get_uint32(HEADER_OFFSET_FLAGS);
228        if self.header_buffer.get_index(0) != 0
229            && ((flags & SQLITE_OPEN_DELETEONCLOSE as u32 != 0)
230                || (flags & PERSISTENT_FILE_TYPES as u32) == 0)
231        {
232            self.set_associated_filename(sah, None, 0)?;
233            return Ok(None);
234        }
235
236        let name_length = Array::from(&self.header_buffer)
237            .iter()
238            .position(|x| x.as_f64().unwrap() as u8 == 0)
239            .unwrap_or_default();
240        if name_length == 0 {
241            sah.truncate_with_u32(HEADER_OFFSET_DATA as u32)
242                .map_err(OpfsSAHError::Truncate)?;
243            return Ok(None);
244        }
245        // set_associated_filename ensures that it is utf8
246        let filename =
247            String::from_utf8(self.header_buffer.subarray(0, name_length as u32).to_vec()).unwrap();
248        Ok(Some(filename))
249    }
250
251    fn set_associated_filename(
252        &self,
253        sah: &FileSystemSyncAccessHandle,
254        filename: Option<&str>,
255        flags: i32,
256    ) -> Result<()> {
257        self.header_buffer_view
258            .set_uint32(HEADER_OFFSET_FLAGS, flags as u32);
259
260        if let Some(filename) = filename {
261            if filename.is_empty() {
262                return Err(OpfsSAHError::Generic("Filename is empty".into()));
263            }
264            if HEADER_MAX_FILENAME_SIZE <= filename.len() + 1 {
265                return Err(OpfsSAHError::Generic(format!(
266                    "Filename too long: {filename}"
267                )));
268            }
269            self.header_buffer
270                .subarray(0, filename.len() as u32)
271                .copy_from(filename.as_bytes());
272            self.header_buffer
273                .fill(0, filename.len() as u32, HEADER_MAX_FILENAME_SIZE as u32);
274            self.map_filename_to_sah.set(&JsValue::from(filename), sah);
275            self.available_sah.delete(sah);
276        } else {
277            self.header_buffer
278                .fill(0, 0, HEADER_MAX_FILENAME_SIZE as u32);
279            sah.truncate_with_u32(HEADER_OFFSET_DATA as u32)
280                .map_err(OpfsSAHError::Truncate)?;
281            self.available_sah.add(sah);
282        }
283
284        sah.write_with_js_u8_array_and_options(&self.header_buffer, &read_write_options(0.0))
285            .map_err(OpfsSAHError::Write)?;
286
287        Ok(())
288    }
289
290    async fn acquire_access_handles(&self, clear_files: bool) -> Result<()> {
291        let iter = self.dh_opaque.entries();
292        while let Ok(future) = iter.next() {
293            let next: IteratorNext = JsFuture::from(future)
294                .await
295                .map_err(OpfsSAHError::IterHandle)?
296                .into();
297            if next.done() {
298                break;
299            }
300            let array: Array = next.value().into();
301            let name = array.get(0);
302            let value = array.get(1);
303            let kind = Reflect::get(&value, &JsValue::from("kind"))
304                .map_err(OpfsSAHError::Reflect)?
305                .as_string();
306            if kind.as_deref() == Some("file") {
307                let handle = FileSystemFileHandle::from(value);
308                let sah = JsFuture::from(handle.create_sync_access_handle())
309                    .await
310                    .map_err(OpfsSAHError::CreateSyncAccessHandle)?;
311                self.map_sah_to_opaque_name.set(&sah, &name);
312                let sah = FileSystemSyncAccessHandle::from(sah);
313                if clear_files {
314                    self.set_associated_filename(&sah, None, 0)?;
315                } else if let Some(filename) = self.get_associated_filename(&sah)? {
316                    self.map_filename_to_sah.set(&JsValue::from(filename), &sah);
317                } else {
318                    self.available_sah.add(&sah);
319                }
320            }
321        }
322
323        Ok(())
324    }
325
326    fn release_access_handles(&self) {
327        for sah in self.map_sah_to_opaque_name.keys().into_iter().flatten() {
328            let sah = FileSystemSyncAccessHandle::from(sah);
329            sah.close();
330        }
331        self.map_sah_to_opaque_name.clear();
332        self.map_filename_to_sah.clear();
333        self.available_sah.clear();
334    }
335
336    fn delete_file(&self, filename: &str) -> Result<bool> {
337        let sah = self.map_filename_to_sah.get(&JsValue::from(filename));
338        let found = !sah.is_undefined();
339        if found {
340            let sah: FileSystemSyncAccessHandle = sah.into();
341            self.map_filename_to_sah.delete(&JsValue::from(filename));
342            self.set_associated_filename(&sah, None, 0)?;
343        }
344        Ok(found)
345    }
346
347    fn has_filename(&self, filename: &str) -> bool {
348        self.map_filename_to_sah.has(&JsValue::from(filename))
349    }
350
351    fn get_sah(&self, filename: &str) -> Option<FileSystemSyncAccessHandle> {
352        self.has_filename(filename).then(|| {
353            self.map_filename_to_sah
354                .get(&JsValue::from(filename))
355                .into()
356        })
357    }
358
359    fn next_available_sah(&self) -> Option<FileSystemSyncAccessHandle> {
360        self.available_sah
361            .keys()
362            .next()
363            .ok()
364            .filter(|x| !x.done())
365            .map(|x| x.value().into())
366    }
367
368    fn export_db(&self, filename: &str) -> Result<Vec<u8>> {
369        let sah = self.map_filename_to_sah.get(&JsValue::from(filename));
370        if sah.is_undefined() {
371            return Err(OpfsSAHError::Generic(format!("File not found: {filename}")));
372        }
373
374        let sah = FileSystemSyncAccessHandle::from(sah);
375        let actual_size = (sah.get_size().map_err(OpfsSAHError::GetSize)?
376            - HEADER_OFFSET_DATA as f64)
377            .max(0.0) as usize;
378
379        let mut data = vec![0; actual_size];
380        if actual_size > 0 {
381            let read = sah
382                .read_with_u8_array_and_options(
383                    &mut data,
384                    &read_write_options(HEADER_OFFSET_DATA as f64),
385                )
386                .map_err(OpfsSAHError::Read)?;
387            if read != actual_size as f64 {
388                return Err(OpfsSAHError::Generic(format!(
389                    "Expected to read {actual_size} bytes but read {read}.",
390                )));
391            }
392        }
393        Ok(data)
394    }
395
396    fn import_db(&self, filename: &str, bytes: &[u8]) -> Result<()> {
397        check_import_db(bytes)?;
398        self.import_db_unchecked(filename, bytes, true)
399    }
400
401    fn import_db_unchecked(&self, filename: &str, bytes: &[u8], clear_wal: bool) -> Result<()> {
402        if self.has_filename(filename) {
403            return Err(OpfsSAHError::Generic(format!(
404                "{filename} file already exists."
405            )));
406        }
407
408        let sah = self
409            .next_available_sah()
410            .ok_or_else(|| OpfsSAHError::Generic("No available handles to import to.".into()))?;
411
412        let length = bytes.len() as f64;
413        let written = sah
414            .write_with_u8_array_and_options(bytes, &read_write_options(HEADER_OFFSET_DATA as f64))
415            .map_err(OpfsSAHError::Write)?;
416        if written != length {
417            self.set_associated_filename(&sah, None, 0)?;
418            return Err(OpfsSAHError::Generic(format!(
419                "Expected to write {length} bytes but wrote {written}.",
420            )));
421        }
422
423        if clear_wal {
424            // forced to write back to legacy mode
425            sah.write_with_u8_array_and_options(
426                &[1, 1],
427                &read_write_options((HEADER_OFFSET_DATA + 18) as f64),
428            )
429            .map_err(OpfsSAHError::Write)?;
430        }
431
432        self.set_associated_filename(&sah, Some(filename), SQLITE_OPEN_MAIN_DB)?;
433
434        Ok(())
435    }
436}
437
438impl VfsFile for FileSystemSyncAccessHandle {
439    fn read(&self, buf: &mut [u8], offset: usize) -> VfsResult<bool> {
440        let n_read = self
441            .read_with_u8_array_and_options(
442                buf,
443                &read_write_options((HEADER_OFFSET_DATA + offset) as f64),
444            )
445            .map_err(OpfsSAHError::Read)
446            .map_err(|err| err.vfs_err(SQLITE_IOERR))?;
447
448        if (n_read as usize) < buf.len() {
449            buf[n_read as usize..].fill(0);
450            return Ok(false);
451        }
452
453        Ok(true)
454    }
455
456    fn write(&mut self, buf: &[u8], offset: usize) -> VfsResult<()> {
457        let n_write = self
458            .write_with_u8_array_and_options(
459                buf,
460                &read_write_options((HEADER_OFFSET_DATA + offset) as f64),
461            )
462            .map_err(OpfsSAHError::Write)
463            .map_err(|err| err.vfs_err(SQLITE_IOERR))?;
464
465        if buf.len() != n_write as usize {
466            return Err(VfsError::new(SQLITE_ERROR, "failed to write file".into()));
467        }
468
469        Ok(())
470    }
471
472    fn truncate(&mut self, size: usize) -> VfsResult<()> {
473        self.truncate_with_f64((HEADER_OFFSET_DATA + size) as f64)
474            .map_err(OpfsSAHError::Truncate)
475            .map_err(|err| err.vfs_err(SQLITE_IOERR))
476    }
477
478    fn flush(&mut self) -> VfsResult<()> {
479        FileSystemSyncAccessHandle::flush(self)
480            .map_err(OpfsSAHError::Flush)
481            .map_err(|err| err.vfs_err(SQLITE_IOERR))
482    }
483
484    fn size(&self) -> VfsResult<usize> {
485        Ok(self
486            .get_size()
487            .map_err(OpfsSAHError::GetSize)
488            .map_err(|err| err.vfs_err(SQLITE_IOERR))? as usize
489            - HEADER_OFFSET_DATA)
490    }
491}
492
493type SyncAccessHandleAppData = OpfsSAHPool;
494
495struct SyncAccessHandleStore;
496
497impl VfsStore<FileSystemSyncAccessHandle, SyncAccessHandleAppData> for SyncAccessHandleStore {
498    fn add_file(vfs: *mut sqlite3_vfs, file: &str, flags: i32) -> VfsResult<()> {
499        let pool = unsafe { Self::app_data(vfs) };
500
501        if let Some(sah) = pool.next_available_sah() {
502            pool.set_associated_filename(&sah, Some(file), flags)
503                .map_err(|err| err.vfs_err(SQLITE_CANTOPEN))?;
504        } else {
505            return Err(VfsError::new(
506                SQLITE_CANTOPEN,
507                "SAH pool is full. Cannot create file".into(),
508            ));
509        };
510
511        Ok(())
512    }
513
514    fn contains_file(vfs: *mut sqlite3_vfs, file: &str) -> VfsResult<bool> {
515        let pool = unsafe { Self::app_data(vfs) };
516        Ok(pool.has_filename(file))
517    }
518
519    fn delete_file(vfs: *mut sqlite3_vfs, file: &str) -> VfsResult<()> {
520        let pool = unsafe { Self::app_data(vfs) };
521        if let Err(err) = pool.delete_file(file) {
522            return Err(err.vfs_err(SQLITE_IOERR_DELETE));
523        }
524        Ok(())
525    }
526
527    fn with_file<F: Fn(&FileSystemSyncAccessHandle) -> VfsResult<i32>>(
528        vfs_file: &super::utils::SQLiteVfsFile,
529        f: F,
530    ) -> VfsResult<i32> {
531        let name = unsafe { vfs_file.name() };
532        let pool = unsafe { Self::app_data(vfs_file.vfs) };
533        match pool.get_sah(name) {
534            Some(file) => f(&file),
535            None => Err(VfsError::new(SQLITE_IOERR, format!("{name} not found"))),
536        }
537    }
538
539    fn with_file_mut<F: Fn(&mut FileSystemSyncAccessHandle) -> VfsResult<i32>>(
540        vfs_file: &super::utils::SQLiteVfsFile,
541        f: F,
542    ) -> VfsResult<i32> {
543        let name = unsafe { vfs_file.name() };
544        let pool = unsafe { Self::app_data(vfs_file.vfs) };
545        match pool.get_sah(name) {
546            Some(mut file) => f(&mut file),
547            None => Err(VfsError::new(SQLITE_IOERR, format!("{name} not found"))),
548        }
549    }
550}
551
552struct SyncAccessHandleIoMethods;
553
554impl SQLiteIoMethods for SyncAccessHandleIoMethods {
555    type File = FileSystemSyncAccessHandle;
556    type AppData = SyncAccessHandleAppData;
557    type Store = SyncAccessHandleStore;
558
559    const VERSION: ::std::os::raw::c_int = 1;
560
561    unsafe extern "C" fn xSectorSize(_pFile: *mut sqlite3_file) -> ::std::os::raw::c_int {
562        SECTOR_SIZE as i32
563    }
564
565    unsafe extern "C" fn xCheckReservedLock(
566        _pFile: *mut sqlite3_file,
567        pResOut: *mut ::std::os::raw::c_int,
568    ) -> ::std::os::raw::c_int {
569        *pResOut = 1;
570        SQLITE_OK
571    }
572
573    unsafe extern "C" fn xDeviceCharacteristics(
574        _pFile: *mut sqlite3_file,
575    ) -> ::std::os::raw::c_int {
576        SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
577    }
578}
579
580struct SyncAccessHandleVfs;
581
582impl SQLiteVfs<SyncAccessHandleIoMethods> for SyncAccessHandleVfs {
583    const VERSION: ::std::os::raw::c_int = 2;
584    const MAX_PATH_SIZE: ::std::os::raw::c_int = HEADER_MAX_FILENAME_SIZE as _;
585}
586
587/// Build `OpfsSAHPoolCfg`
588pub struct OpfsSAHPoolCfgBuilder(OpfsSAHPoolCfg);
589
590impl OpfsSAHPoolCfgBuilder {
591    pub fn new() -> Self {
592        Self(OpfsSAHPoolCfg::default())
593    }
594
595    /// The SQLite VFS name under which this pool's VFS is registered.
596    pub fn vfs_name(mut self, name: &str) -> Self {
597        self.0.vfs_name = name.into();
598        self
599    }
600
601    /// Specifies the OPFS directory name in which to store metadata for the `vfs_name`
602    pub fn directory(mut self, directory: &str) -> Self {
603        self.0.directory = directory.into();
604        self
605    }
606
607    /// If truthy, contents and filename mapping are removed from each SAH
608    /// as it is acquired during initalization of the VFS, leaving the VFS's
609    /// storage in a pristine state. Use this only for databases which need not
610    /// survive a page reload.
611    pub fn clear_on_init(mut self, set: bool) -> Self {
612        self.0.clear_on_init = set;
613        self
614    }
615
616    /// Specifies the default capacity of the VFS, i.e. the number of files
617    /// it may contain.
618    pub fn initial_capacity(mut self, cap: u32) -> Self {
619        self.0.initial_capacity = cap;
620        self
621    }
622
623    /// Build `OpfsSAHPoolCfg`.
624    pub fn build(self) -> OpfsSAHPoolCfg {
625        self.0
626    }
627}
628
629impl Default for OpfsSAHPoolCfgBuilder {
630    fn default() -> Self {
631        Self::new()
632    }
633}
634
635/// `OpfsSAHPool` options
636pub struct OpfsSAHPoolCfg {
637    /// The SQLite VFS name under which this pool's VFS is registered.
638    pub vfs_name: String,
639    /// Specifies the OPFS directory name in which to store metadata for the `vfs_name`.
640    pub directory: String,
641    /// If truthy, contents and filename mapping are removed from each SAH
642    /// as it is acquired during initalization of the VFS, leaving the VFS's
643    /// storage in a pristine state. Use this only for databases which need not
644    /// survive a page reload.
645    pub clear_on_init: bool,
646    /// Specifies the default capacity of the VFS, i.e. the number of files
647    /// it may contain.
648    pub initial_capacity: u32,
649}
650
651impl Default for OpfsSAHPoolCfg {
652    fn default() -> Self {
653        Self {
654            vfs_name: "opfs-sahpool".into(),
655            directory: ".opfs-sahpool".into(),
656            clear_on_init: false,
657            initial_capacity: 6,
658        }
659    }
660}
661
662#[derive(thiserror::Error, Debug)]
663pub enum OpfsSAHError {
664    #[error(transparent)]
665    Vfs(#[from] RegisterVfsError),
666    #[error(transparent)]
667    ImportDb(#[from] ImportDbError),
668    #[error("This vfs is only available in dedicated worker")]
669    NotSuported,
670    #[error("An error occurred while getting the directory handle")]
671    GetDirHandle(JsValue),
672    #[error("An error occurred while getting the file handle")]
673    GetFileHandle(JsValue),
674    #[error("An error occurred while creating sync access handle")]
675    CreateSyncAccessHandle(JsValue),
676    #[error("An error occurred while iterating")]
677    IterHandle(JsValue),
678    #[error("An error occurred while getting filename")]
679    GetPath(JsValue),
680    #[error("An error occurred while removing entity")]
681    RemoveEntity(JsValue),
682    #[error("An error occurred while getting size")]
683    GetSize(JsValue),
684    #[error("An error occurred while reading data")]
685    Read(JsValue),
686    #[error("An error occurred while writing data")]
687    Write(JsValue),
688    #[error("An error occurred while flushing data")]
689    Flush(JsValue),
690    #[error("An error occurred while truncating data")]
691    Truncate(JsValue),
692    #[error("An error occurred while getting data using reflect")]
693    Reflect(JsValue),
694    #[error("Generic error: {0}")]
695    Generic(String),
696}
697
698impl OpfsSAHError {
699    fn vfs_err(&self, code: i32) -> VfsError {
700        VfsError::new(code, format!("{self}"))
701    }
702}
703
704/// SAHPoolVfs management tool.
705pub struct OpfsSAHPoolUtil {
706    pool: &'static VfsAppData<SyncAccessHandleAppData>,
707}
708
709unsafe impl Send for OpfsSAHPoolUtil {}
710
711unsafe impl Sync for OpfsSAHPoolUtil {}
712
713impl OpfsSAHPoolUtil {
714    /// Returns the number of files currently contained in the SAH pool.
715    pub fn get_capacity(&self) -> u32 {
716        self.pool.get_capacity()
717    }
718
719    /// Adds n entries to the current pool.
720    pub async fn add_capacity(&self, n: u32) -> Result<u32> {
721        self.pool.add_capacity(n).await
722    }
723
724    /// Removes up to n entries from the pool, with the caveat that
725    /// it can only remove currently-unused entries.
726    pub async fn reduce_capacity(&self, n: u32) -> Result<u32> {
727        self.pool.reduce_capacity(n).await
728    }
729
730    /// Removes up to n entries from the pool, with the caveat that it can only
731    /// remove currently-unused entries.
732    pub async fn reserve_minimum_capacity(&self, min: u32) -> Result<()> {
733        self.pool.reserve_minimum_capacity(min).await
734    }
735}
736
737impl OpfsSAHPoolUtil {
738    /// Imports the contents of an SQLite database, provided as a byte array
739    /// under the given name, overwriting any existing content.
740    ///
741    /// If the database is imported with WAL mode enabled,
742    /// it will be forced to write back to legacy mode, see
743    /// <https://sqlite.org/forum/forumpost/67882c5b04>.
744    ///
745    /// If the imported database is encrypted, use `import_db_unchecked` instead.
746    pub fn import_db(&self, filename: &str, bytes: &[u8]) -> Result<()> {
747        self.pool.import_db(filename, bytes)
748    }
749
750    /// `import_db` without checking, can be used to import encrypted database.
751    pub fn import_db_unchecked(&self, filename: &str, bytes: &[u8]) -> Result<()> {
752        self.pool.import_db_unchecked(filename, bytes, false)
753    }
754
755    /// Export the database.
756    pub fn export_db(&self, filename: &str) -> Result<Vec<u8>> {
757        self.pool.export_db(filename)
758    }
759
760    /// Delete the specified database, make sure that the database is closed.
761    pub fn delete_db(&self, filename: &str) -> Result<bool> {
762        self.pool.delete_file(filename)
763    }
764
765    /// Delete all database, make sure that all database is closed.
766    pub async fn clear_all(&self) -> Result<()> {
767        self.pool.release_access_handles();
768        self.pool.acquire_access_handles(true).await?;
769        Ok(())
770    }
771
772    /// Does the database exists.
773    pub fn exists(&self, filename: &str) -> Result<bool> {
774        Ok(self.pool.has_filename(filename))
775    }
776
777    /// List all files.
778    pub fn list(&self) -> Vec<String> {
779        self.pool.get_filenames()
780    }
781
782    /// Number of files.
783    pub fn count(&self) -> u32 {
784        self.pool.get_file_count()
785    }
786}
787
788/// Register `opfs-sahpool` vfs and return a management tool which can be used
789/// to perform basic administration of the file pool.
790///
791/// If the vfs corresponding to `options.vfs_name` has been registered,
792/// only return a management tool without register.
793pub async fn install(options: &OpfsSAHPoolCfg, default_vfs: bool) -> Result<OpfsSAHPoolUtil> {
794    static REGISTER_GUARD: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
795    let _guard = REGISTER_GUARD.lock().await;
796
797    let vfs = if let Some(vfs) = registered_vfs(&options.vfs_name)? {
798        vfs
799    } else {
800        register_vfs::<SyncAccessHandleIoMethods, SyncAccessHandleVfs>(
801            &options.vfs_name,
802            OpfsSAHPool::new(options).await?,
803            default_vfs,
804        )?
805    };
806
807    let pool = unsafe { SyncAccessHandleStore::app_data(vfs) };
808
809    Ok(OpfsSAHPoolUtil { pool })
810}
811
812#[cfg(test)]
813mod tests {
814    use crate::{
815        sahpool_vfs::{
816            OpfsSAHPool, OpfsSAHPoolCfgBuilder, SyncAccessHandleAppData, SyncAccessHandleStore,
817        },
818        utils::{test_suite::test_vfs_store, VfsAppData},
819    };
820    use wasm_bindgen_test::wasm_bindgen_test;
821    use web_sys::FileSystemSyncAccessHandle;
822
823    #[wasm_bindgen_test]
824    async fn test_opfs_vfs_store() {
825        let data = OpfsSAHPool::new(
826            &OpfsSAHPoolCfgBuilder::new()
827                .directory("test_opfs_suite")
828                .build(),
829        )
830        .await
831        .unwrap();
832
833        test_vfs_store::<SyncAccessHandleAppData, FileSystemSyncAccessHandle, SyncAccessHandleStore>(VfsAppData::new(data))
834            .unwrap();
835    }
836}