1use crate::libsqlite3::*;
8use crate::utils::LazyCell;
9use crate::vfs::utils::{
10 check_import_db, random_name, register_vfs, ImportDbError, RegisterVfsError, SQLiteIoMethods,
11 SQLiteVfs, VfsAppData, VfsError, VfsFile, VfsResult, VfsStore,
12};
13
14use js_sys::{Array, DataView, IteratorNext, Map, Reflect, Set, Uint8Array};
15use std::collections::HashMap;
16use wasm_bindgen::{JsCast, JsValue};
17use wasm_bindgen_futures::JsFuture;
18use web_sys::{
19 FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetDirectoryOptions,
20 FileSystemGetFileOptions, FileSystemReadWriteOptions, FileSystemSyncAccessHandle,
21 WorkerGlobalScope,
22};
23
24const SECTOR_SIZE: usize = 4096;
25const HEADER_MAX_FILENAME_SIZE: usize = 512;
26const HEADER_FLAGS_SIZE: usize = 4;
27const HEADER_CORPUS_SIZE: usize = HEADER_MAX_FILENAME_SIZE + HEADER_FLAGS_SIZE;
28const HEADER_OFFSET_FLAGS: usize = HEADER_MAX_FILENAME_SIZE;
29const HEADER_OFFSET_DATA: usize = SECTOR_SIZE;
30
31const PERSISTENT_FILE_TYPES: i32 =
41 SQLITE_OPEN_MAIN_DB | SQLITE_OPEN_MAIN_JOURNAL | SQLITE_OPEN_SUPER_JOURNAL | SQLITE_OPEN_WAL;
42
43type Result<T> = std::result::Result<T, OpfsSAHError>;
44
45fn read_write_options(at: f64) -> FileSystemReadWriteOptions {
46 let options = FileSystemReadWriteOptions::new();
47 options.set_at(at);
48 options
49}
50
51struct OpfsSAHPool {
52 dh_opaque: FileSystemDirectoryHandle,
56 header_buffer: Uint8Array,
58 header_buffer_view: DataView,
60 available_sah: Set,
62 map_filename_to_sah: Map,
64 map_sah_to_opaque_name: Map,
66}
67
68impl OpfsSAHPool {
69 async fn new(options: &OpfsSAHPoolCfg) -> Result<OpfsSAHPool> {
70 const OPAQUE_DIR_NAME: &str = ".opaque";
71
72 let vfs_dir = &options.directory;
73 let capacity = options.initial_capacity;
74 let clear_files = options.clear_on_init;
75
76 let create_option = FileSystemGetDirectoryOptions::new();
77 create_option.set_create(true);
78
79 let mut handle: FileSystemDirectoryHandle = JsFuture::from(
80 js_sys::global()
81 .dyn_into::<WorkerGlobalScope>()
82 .map_err(|_| OpfsSAHError::NotSuported)?
83 .navigator()
84 .storage()
85 .get_directory(),
86 )
87 .await
88 .map_err(OpfsSAHError::GetDirHandle)?
89 .into();
90
91 for dir in vfs_dir.split('/').filter(|x| !x.is_empty()) {
92 let next =
93 JsFuture::from(handle.get_directory_handle_with_options(dir, &create_option))
94 .await
95 .map_err(OpfsSAHError::GetDirHandle)?
96 .into();
97 handle = next;
98 }
99
100 let dh_opaque = JsFuture::from(
101 handle.get_directory_handle_with_options(OPAQUE_DIR_NAME, &create_option),
102 )
103 .await
104 .map_err(OpfsSAHError::GetDirHandle)?
105 .into();
106
107 let ap_body = Uint8Array::new_with_length(HEADER_CORPUS_SIZE as _);
108 let dv_body = DataView::new(
109 &ap_body.buffer(),
110 ap_body.byte_offset() as usize,
111 (ap_body.byte_length() - ap_body.byte_offset()) as usize,
112 );
113
114 let pool = Self {
115 dh_opaque,
116 header_buffer: ap_body,
117 header_buffer_view: dv_body,
118 map_filename_to_sah: Map::new(),
119 available_sah: Set::default(),
120 map_sah_to_opaque_name: Map::new(),
121 };
122
123 pool.acquire_access_handles(clear_files).await?;
124 pool.reserve_minimum_capacity(capacity).await?;
125
126 Ok(pool)
127 }
128
129 async fn add_capacity(&self, n: u32) -> Result<u32> {
130 for _ in 0..n {
131 let name = random_name();
132 let handle: FileSystemFileHandle =
133 JsFuture::from(self.dh_opaque.get_file_handle_with_options(&name, &{
134 let options = FileSystemGetFileOptions::new();
135 options.set_create(true);
136 options
137 }))
138 .await
139 .map_err(OpfsSAHError::GetFileHandle)?
140 .into();
141 let sah: FileSystemSyncAccessHandle =
142 JsFuture::from(handle.create_sync_access_handle())
143 .await
144 .map_err(OpfsSAHError::CreateSyncAccessHandle)?
145 .into();
146 self.map_sah_to_opaque_name.set(&sah, &JsValue::from(name));
147 self.set_associated_filename(&sah, None, 0)?;
148 }
149 Ok(self.get_capacity())
150 }
151
152 async fn reserve_minimum_capacity(&self, min: u32) -> Result<()> {
153 self.add_capacity(min.saturating_sub(self.get_capacity()))
154 .await?;
155 Ok(())
156 }
157
158 async fn reduce_capacity(&self, n: u32) -> Result<u32> {
159 let mut result = 0;
160 for sah in Array::from(&self.available_sah) {
161 if result == n || self.get_capacity() == self.get_file_count() {
162 break;
163 }
164 let sah = FileSystemSyncAccessHandle::from(sah);
165 let name = self.map_sah_to_opaque_name.get(&sah).as_string().unwrap();
166
167 sah.close();
168 JsFuture::from(self.dh_opaque.remove_entry(&name))
169 .await
170 .map_err(OpfsSAHError::RemoveEntity)?;
171 self.map_sah_to_opaque_name.delete(&sah);
172 self.available_sah.delete(&sah);
173
174 result += 1;
175 }
176 Ok(result)
177 }
178
179 fn get_capacity(&self) -> u32 {
180 self.map_sah_to_opaque_name.size()
181 }
182
183 fn get_file_count(&self) -> u32 {
184 self.map_filename_to_sah.size()
185 }
186
187 fn get_filenames(&self) -> Vec<String> {
188 self.map_filename_to_sah
189 .keys()
190 .into_iter()
191 .flatten()
192 .map(|x| x.as_string().unwrap())
193 .collect()
194 }
195
196 fn get_associated_filename(&self, sah: &FileSystemSyncAccessHandle) -> Result<Option<String>> {
197 sah.read_with_buffer_source_and_options(&self.header_buffer, &read_write_options(0.0))
198 .map_err(OpfsSAHError::Read)?;
199 let flags = self.header_buffer_view.get_uint32(HEADER_OFFSET_FLAGS);
200 if self.header_buffer.get_index(0) != 0
201 && ((flags & SQLITE_OPEN_DELETEONCLOSE as u32 != 0)
202 || (flags & PERSISTENT_FILE_TYPES as u32) == 0)
203 {
204 self.set_associated_filename(sah, None, 0)?;
205 return Ok(None);
206 }
207
208 let name_length = Array::from(&self.header_buffer)
209 .iter()
210 .position(|x| x.as_f64().unwrap() as u8 == 0)
211 .unwrap_or_default();
212 if name_length == 0 {
213 sah.truncate_with_u32(HEADER_OFFSET_DATA as u32)
214 .map_err(OpfsSAHError::Truncate)?;
215 return Ok(None);
216 }
217 let filename =
219 String::from_utf8(self.header_buffer.subarray(0, name_length as u32).to_vec()).unwrap();
220 Ok(Some(filename))
221 }
222
223 fn set_associated_filename(
224 &self,
225 sah: &FileSystemSyncAccessHandle,
226 filename: Option<&str>,
227 flags: i32,
228 ) -> Result<()> {
229 self.header_buffer_view
230 .set_uint32(HEADER_OFFSET_FLAGS, flags as u32);
231
232 if let Some(filename) = filename {
233 if filename.is_empty() {
234 return Err(OpfsSAHError::Generic("Filename is empty".into()));
235 }
236 if HEADER_MAX_FILENAME_SIZE <= filename.len() + 1 {
237 return Err(OpfsSAHError::Generic(format!(
238 "Filename too long: {filename}"
239 )));
240 }
241 self.header_buffer
242 .subarray(0, filename.len() as u32)
243 .copy_from(filename.as_bytes());
244 self.header_buffer
245 .fill(0, filename.len() as u32, HEADER_MAX_FILENAME_SIZE as u32);
246 self.map_filename_to_sah.set(&JsValue::from(filename), sah);
247 self.available_sah.delete(sah);
248 } else {
249 self.header_buffer
250 .fill(0, 0, HEADER_MAX_FILENAME_SIZE as u32);
251 sah.truncate_with_u32(HEADER_OFFSET_DATA as u32)
252 .map_err(OpfsSAHError::Truncate)?;
253 self.available_sah.add(sah);
254 }
255
256 sah.write_with_js_u8_array_and_options(&self.header_buffer, &read_write_options(0.0))
257 .map_err(OpfsSAHError::Write)?;
258
259 Ok(())
260 }
261
262 async fn acquire_access_handles(&self, clear_files: bool) -> Result<()> {
263 let iter = self.dh_opaque.entries();
264 while let Ok(future) = iter.next() {
265 let next: IteratorNext = JsFuture::from(future)
266 .await
267 .map_err(OpfsSAHError::IterHandle)?
268 .into();
269 if next.done() {
270 break;
271 }
272 let array: Array = next.value().into();
273 let name = array.get(0);
274 let value = array.get(1);
275 let kind = Reflect::get(&value, &JsValue::from("kind"))
276 .map_err(OpfsSAHError::Reflect)?
277 .as_string();
278 if kind.as_deref() == Some("file") {
279 let handle = FileSystemFileHandle::from(value);
280 let sah = JsFuture::from(handle.create_sync_access_handle())
281 .await
282 .map_err(OpfsSAHError::CreateSyncAccessHandle)?;
283 self.map_sah_to_opaque_name.set(&sah, &name);
284 let sah = FileSystemSyncAccessHandle::from(sah);
285 if clear_files {
286 self.set_associated_filename(&sah, None, 0)?;
287 } else if let Some(filename) = self.get_associated_filename(&sah)? {
288 self.map_filename_to_sah.set(&JsValue::from(filename), &sah);
289 } else {
290 self.available_sah.add(&sah);
291 }
292 }
293 }
294
295 Ok(())
296 }
297
298 fn release_access_handles(&self) {
299 for sah in self.map_sah_to_opaque_name.keys().into_iter().flatten() {
300 let sah = FileSystemSyncAccessHandle::from(sah);
301 sah.close();
302 }
303 self.map_sah_to_opaque_name.clear();
304 self.map_filename_to_sah.clear();
305 self.available_sah.clear();
306 }
307
308 fn delete_file(&self, filename: &str) -> Result<bool> {
309 let sah = self.map_filename_to_sah.get(&JsValue::from(filename));
310 let found = !sah.is_undefined();
311 if found {
312 let sah: FileSystemSyncAccessHandle = sah.into();
313 self.map_filename_to_sah.delete(&JsValue::from(filename));
314 self.set_associated_filename(&sah, None, 0)?;
315 }
316 Ok(found)
317 }
318
319 fn has_filename(&self, filename: &str) -> bool {
320 self.map_filename_to_sah.has(&JsValue::from(filename))
321 }
322
323 fn get_sah(&self, filename: &str) -> Option<FileSystemSyncAccessHandle> {
324 self.has_filename(filename).then(|| {
325 self.map_filename_to_sah
326 .get(&JsValue::from(filename))
327 .into()
328 })
329 }
330
331 fn next_available_sah(&self) -> Option<FileSystemSyncAccessHandle> {
332 self.available_sah
333 .keys()
334 .next()
335 .ok()
336 .filter(|x| !x.done())
337 .map(|x| x.value().into())
338 }
339
340 fn export_db(&self, filename: &str) -> Result<Vec<u8>> {
341 let sah = self.map_filename_to_sah.get(&JsValue::from(filename));
342 if sah.is_undefined() {
343 return Err(OpfsSAHError::Generic(format!("File not found: {filename}")));
344 }
345
346 let sah = FileSystemSyncAccessHandle::from(sah);
347 let actual_size = (sah.get_size().map_err(OpfsSAHError::GetSize)?
348 - HEADER_OFFSET_DATA as f64)
349 .max(0.0) as usize;
350
351 let mut data = vec![0; actual_size];
352 if actual_size > 0 {
353 let read = sah
354 .read_with_u8_array_and_options(
355 &mut data,
356 &read_write_options(HEADER_OFFSET_DATA as f64),
357 )
358 .map_err(OpfsSAHError::Read)?;
359 if read != actual_size as f64 {
360 return Err(OpfsSAHError::Generic(format!(
361 "Expected to read {actual_size} bytes but read {read}.",
362 )));
363 }
364 }
365 Ok(data)
366 }
367
368 fn import_db(&self, filename: &str, bytes: &[u8]) -> Result<()> {
369 check_import_db(bytes)?;
370 self.import_db_unchecked(filename, bytes, true)
371 }
372
373 fn import_db_unchecked(&self, filename: &str, bytes: &[u8], clear_wal: bool) -> Result<()> {
374 if self.has_filename(filename) {
375 return Err(OpfsSAHError::Generic(format!(
376 "{filename} file already exists."
377 )));
378 }
379
380 let sah = self
381 .next_available_sah()
382 .ok_or_else(|| OpfsSAHError::Generic("No available handles to import to.".into()))?;
383
384 let length = bytes.len() as f64;
385 let written = sah
386 .write_with_u8_array_and_options(bytes, &read_write_options(HEADER_OFFSET_DATA as f64))
387 .map_err(OpfsSAHError::Write)?;
388 if written != length {
389 self.set_associated_filename(&sah, None, 0)?;
390 return Err(OpfsSAHError::Generic(format!(
391 "Expected to write {length} bytes but wrote {written}.",
392 )));
393 }
394
395 if clear_wal {
396 sah.write_with_u8_array_and_options(
398 &[1, 1],
399 &read_write_options((HEADER_OFFSET_DATA + 18) as f64),
400 )
401 .map_err(OpfsSAHError::Write)?;
402 }
403
404 self.set_associated_filename(&sah, Some(filename), SQLITE_OPEN_MAIN_DB)?;
405
406 Ok(())
407 }
408}
409
410impl VfsFile for FileSystemSyncAccessHandle {
411 fn read(&self, buf: &mut [u8], offset: usize) -> VfsResult<bool> {
412 let n_read = self
413 .read_with_u8_array_and_options(
414 buf,
415 &read_write_options((HEADER_OFFSET_DATA + offset) as f64),
416 )
417 .map_err(OpfsSAHError::Read)
418 .map_err(|err| err.vfs_err(SQLITE_IOERR))?;
419
420 if (n_read as usize) < buf.len() {
421 buf[n_read as usize..].fill(0);
422 return Ok(false);
423 }
424
425 Ok(true)
426 }
427
428 fn write(&mut self, buf: &[u8], offset: usize) -> VfsResult<()> {
429 let n_write = self
430 .write_with_u8_array_and_options(
431 buf,
432 &read_write_options((HEADER_OFFSET_DATA + offset) as f64),
433 )
434 .map_err(OpfsSAHError::Write)
435 .map_err(|err| err.vfs_err(SQLITE_IOERR))?;
436
437 if buf.len() != n_write as usize {
438 return Err(VfsError::new(SQLITE_ERROR, "failed to write file".into()));
439 }
440
441 Ok(())
442 }
443
444 fn truncate(&mut self, size: usize) -> VfsResult<()> {
445 self.truncate_with_f64((HEADER_OFFSET_DATA + size) as f64)
446 .map_err(OpfsSAHError::Truncate)
447 .map_err(|err| err.vfs_err(SQLITE_IOERR))
448 }
449
450 fn flush(&mut self) -> VfsResult<()> {
451 FileSystemSyncAccessHandle::flush(self)
452 .map_err(OpfsSAHError::Flush)
453 .map_err(|err| err.vfs_err(SQLITE_IOERR))
454 }
455
456 fn size(&self) -> VfsResult<usize> {
457 Ok(self
458 .get_size()
459 .map_err(OpfsSAHError::GetSize)
460 .map_err(|err| err.vfs_err(SQLITE_IOERR))? as usize
461 - HEADER_OFFSET_DATA)
462 }
463}
464
465type SyncAccessHandleAppData = OpfsSAHPool;
466
467struct SyncAccessHandleStore;
468
469impl VfsStore<FileSystemSyncAccessHandle, SyncAccessHandleAppData> for SyncAccessHandleStore {
470 fn add_file(vfs: *mut sqlite3_vfs, file: &str, flags: i32) -> VfsResult<()> {
471 let pool = unsafe { Self::app_data(vfs) };
472
473 if let Some(sah) = pool.next_available_sah() {
474 pool.set_associated_filename(&sah, Some(file), flags)
475 .map_err(|err| err.vfs_err(SQLITE_CANTOPEN))?;
476 } else {
477 return Err(VfsError::new(
478 SQLITE_CANTOPEN,
479 "SAH pool is full. Cannot create file".into(),
480 ));
481 };
482
483 Ok(())
484 }
485
486 fn contains_file(vfs: *mut sqlite3_vfs, file: &str) -> VfsResult<bool> {
487 let pool = unsafe { Self::app_data(vfs) };
488 Ok(pool.has_filename(file))
489 }
490
491 fn delete_file(vfs: *mut sqlite3_vfs, file: &str) -> VfsResult<()> {
492 let pool = unsafe { Self::app_data(vfs) };
493 if let Err(err) = pool.delete_file(file) {
494 return Err(err.vfs_err(SQLITE_IOERR_DELETE));
495 }
496 Ok(())
497 }
498
499 fn with_file<F: Fn(&FileSystemSyncAccessHandle) -> VfsResult<i32>>(
500 vfs_file: &super::utils::SQLiteVfsFile,
501 f: F,
502 ) -> VfsResult<i32> {
503 let name = unsafe { vfs_file.name() };
504 let pool = unsafe { Self::app_data(vfs_file.vfs) };
505 match pool.get_sah(name) {
506 Some(file) => f(&file),
507 None => Err(VfsError::new(SQLITE_IOERR, format!("{name} not found"))),
508 }
509 }
510
511 fn with_file_mut<F: Fn(&mut FileSystemSyncAccessHandle) -> VfsResult<i32>>(
512 vfs_file: &super::utils::SQLiteVfsFile,
513 f: F,
514 ) -> VfsResult<i32> {
515 let name = unsafe { vfs_file.name() };
516 let pool = unsafe { Self::app_data(vfs_file.vfs) };
517 match pool.get_sah(name) {
518 Some(mut file) => f(&mut file),
519 None => Err(VfsError::new(SQLITE_IOERR, format!("{name} not found"))),
520 }
521 }
522}
523
524struct SyncAccessHandleIoMethods;
525
526impl SQLiteIoMethods for SyncAccessHandleIoMethods {
527 type File = FileSystemSyncAccessHandle;
528 type AppData = SyncAccessHandleAppData;
529 type Store = SyncAccessHandleStore;
530
531 const VERSION: ::std::os::raw::c_int = 1;
532
533 unsafe extern "C" fn xSectorSize(_pFile: *mut sqlite3_file) -> ::std::os::raw::c_int {
534 SECTOR_SIZE as i32
535 }
536
537 unsafe extern "C" fn xCheckReservedLock(
538 _pFile: *mut sqlite3_file,
539 pResOut: *mut ::std::os::raw::c_int,
540 ) -> ::std::os::raw::c_int {
541 *pResOut = 1;
542 SQLITE_OK
543 }
544
545 unsafe extern "C" fn xDeviceCharacteristics(
546 _pFile: *mut sqlite3_file,
547 ) -> ::std::os::raw::c_int {
548 SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
549 }
550}
551
552struct SyncAccessHandleVfs;
553
554impl SQLiteVfs<SyncAccessHandleIoMethods> for SyncAccessHandleVfs {
555 const VERSION: ::std::os::raw::c_int = 2;
556 const MAX_PATH_SIZE: ::std::os::raw::c_int = HEADER_MAX_FILENAME_SIZE as _;
557}
558
559pub struct OpfsSAHPoolCfgBuilder(OpfsSAHPoolCfg);
561
562impl OpfsSAHPoolCfgBuilder {
563 pub fn new() -> Self {
564 Self(OpfsSAHPoolCfg::default())
565 }
566
567 pub fn vfs_name(mut self, name: &str) -> Self {
569 self.0.vfs_name = name.into();
570 self
571 }
572
573 pub fn directory(mut self, directory: &str) -> Self {
575 self.0.directory = directory.into();
576 self
577 }
578
579 pub fn clear_on_init(mut self, set: bool) -> Self {
584 self.0.clear_on_init = set;
585 self
586 }
587
588 pub fn initial_capacity(mut self, cap: u32) -> Self {
591 self.0.initial_capacity = cap;
592 self
593 }
594
595 pub fn build(self) -> OpfsSAHPoolCfg {
597 self.0
598 }
599}
600
601impl Default for OpfsSAHPoolCfgBuilder {
602 fn default() -> Self {
603 Self::new()
604 }
605}
606
607pub struct OpfsSAHPoolCfg {
609 pub vfs_name: String,
611 pub directory: String,
613 pub clear_on_init: bool,
618 pub initial_capacity: u32,
621}
622
623impl Default for OpfsSAHPoolCfg {
624 fn default() -> Self {
625 Self {
626 vfs_name: "opfs-sahpool".into(),
627 directory: ".opfs-sahpool".into(),
628 clear_on_init: false,
629 initial_capacity: 6,
630 }
631 }
632}
633
634#[derive(thiserror::Error, Debug)]
635pub enum OpfsSAHError {
636 #[error(transparent)]
637 Vfs(#[from] RegisterVfsError),
638 #[error(transparent)]
639 ImportDb(#[from] ImportDbError),
640 #[error("This vfs is only available in dedicated worker")]
641 NotSuported,
642 #[error("An error occurred while getting the directory handle")]
643 GetDirHandle(JsValue),
644 #[error("An error occurred while getting the file handle")]
645 GetFileHandle(JsValue),
646 #[error("An error occurred while creating sync access handle")]
647 CreateSyncAccessHandle(JsValue),
648 #[error("An error occurred while iterating")]
649 IterHandle(JsValue),
650 #[error("An error occurred while getting filename")]
651 GetPath(JsValue),
652 #[error("An error occurred while removing entity")]
653 RemoveEntity(JsValue),
654 #[error("An error occurred while getting size")]
655 GetSize(JsValue),
656 #[error("An error occurred while reading data")]
657 Read(JsValue),
658 #[error("An error occurred while writing data")]
659 Write(JsValue),
660 #[error("An error occurred while flushing data")]
661 Flush(JsValue),
662 #[error("An error occurred while truncating data")]
663 Truncate(JsValue),
664 #[error("An error occurred while getting data using reflect")]
665 Reflect(JsValue),
666 #[error("Generic error: {0}")]
667 Generic(String),
668}
669
670impl OpfsSAHError {
671 fn vfs_err(&self, code: i32) -> VfsError {
672 VfsError::new(code, format!("{self}"))
673 }
674}
675
676pub struct OpfsSAHPoolUtil {
681 pool: &'static VfsAppData<SyncAccessHandleAppData>,
682}
683
684unsafe impl Send for OpfsSAHPoolUtil {}
685
686unsafe impl Sync for OpfsSAHPoolUtil {}
687
688impl OpfsSAHPoolUtil {
689 pub fn get_capacity(&self) -> u32 {
691 self.pool.get_capacity()
692 }
693
694 pub async fn add_capacity(&self, n: u32) -> Result<u32> {
696 self.pool.add_capacity(n).await
697 }
698
699 pub async fn reduce_capacity(&self, n: u32) -> Result<u32> {
702 self.pool.reduce_capacity(n).await
703 }
704
705 pub async fn reserve_minimum_capacity(&self, min: u32) -> Result<()> {
708 self.pool.reserve_minimum_capacity(min).await
709 }
710}
711
712impl OpfsSAHPoolUtil {
713 pub fn import_db(&self, filename: &str, bytes: &[u8]) -> Result<()> {
722 self.pool.import_db(filename, bytes)
723 }
724
725 pub fn import_db_unchecked(&self, filename: &str, bytes: &[u8]) -> Result<()> {
727 self.pool.import_db_unchecked(filename, bytes, false)
728 }
729
730 pub fn export_db(&self, filename: &str) -> Result<Vec<u8>> {
732 self.pool.export_db(filename)
733 }
734
735 pub fn delete_db(&self, filename: &str) -> Result<bool> {
737 self.pool.delete_file(filename)
738 }
739
740 pub async fn clear_all(&self) -> Result<()> {
742 self.pool.release_access_handles();
743 self.pool.acquire_access_handles(true).await?;
744 Ok(())
745 }
746
747 pub fn exists(&self, filename: &str) -> Result<bool> {
749 Ok(self.pool.has_filename(filename))
750 }
751
752 pub fn list(&self) -> Vec<String> {
754 self.pool.get_filenames()
755 }
756
757 pub fn count(&self) -> u32 {
759 self.pool.get_file_count()
760 }
761}
762
763pub async fn install(options: &OpfsSAHPoolCfg, default_vfs: bool) -> Result<OpfsSAHPoolUtil> {
766 #[cfg_attr(target_feature = "atomics", thread_local)]
767 static NAME2VFS: LazyCell<
768 tokio::sync::Mutex<HashMap<String, &'static VfsAppData<SyncAccessHandleAppData>>>,
769 > = LazyCell::new(|| tokio::sync::Mutex::new(HashMap::new()));
770
771 let mut name2vfs = NAME2VFS.lock().await;
772
773 let vfs_name = &options.vfs_name;
774 let pool = if let Some(pool) = name2vfs.get(vfs_name) {
775 pool
776 } else {
777 let pool = OpfsSAHPool::new(options).await?;
778 let vfs = register_vfs::<SyncAccessHandleIoMethods, SyncAccessHandleVfs>(
779 vfs_name,
780 pool,
781 default_vfs,
782 )?;
783 let pool = unsafe { SyncAccessHandleStore::app_data(vfs) };
784 name2vfs.insert(vfs_name.clone(), pool);
785 pool
786 };
787
788 let util = OpfsSAHPoolUtil { pool };
789
790 Ok(util)
791}
792
793#[cfg(test)]
794mod tests {
795 use crate::{
796 sahpool_vfs::{
797 OpfsSAHPool, OpfsSAHPoolCfgBuilder, SyncAccessHandleAppData, SyncAccessHandleStore,
798 },
799 utils::{test_suite::test_vfs_store, VfsAppData},
800 };
801 use wasm_bindgen_test::wasm_bindgen_test;
802 use web_sys::FileSystemSyncAccessHandle;
803
804 #[wasm_bindgen_test]
805 async fn test_opfs_vfs_store() {
806 let data = OpfsSAHPool::new(
807 &OpfsSAHPoolCfgBuilder::new()
808 .directory("test_opfs_suite")
809 .build(),
810 )
811 .await
812 .unwrap();
813
814 test_vfs_store::<SyncAccessHandleAppData, FileSystemSyncAccessHandle, SyncAccessHandleStore>(VfsAppData::new(data))
815 .unwrap();
816 }
817}