browser-fs 0.1.0

A browser-based filesystem implementation for WebAssembly applications
Documentation
//! Browser filesystem primitives.
//!
//! This crate is an async version of [`std::fs`] that is designed to work in a
//! browser, only in a web worker.
//!
//! It is supposed to provide an API similar to [`async-fs`](https://docs.rs/async-fs/).
//!
//! This crate is inspired by [`web-fs`](https://docs.rs/web-fs/) which provides a mechanism of using the browser filesystem
//! from the main thread, which is not the case for the current implementation,
//! making it simpler and easier to integrate if your app already works in a web
//! worker.
//!
//! # Known constraints
//!
//! ## File size
//!
//! `wasm32` only support at most 4GB memory, therefore it's not possible to
//! handle files bigger than that.
//!
//! ## Concurrent access
//!
//! The current implementation, based on `web-sys` cannot open multiple file
//! handler at the same time on the same file. The underneath implementation is
//! blocking this by returning an error.
//!
//! # Examples
//!
//! Create a new file and write some bytes to it:
//!
//! ```no_run
//! use browser_fs::File;
//! use futures_lite::io::AsyncWriteExt;
//!
//! # futures_lite::future::block_on(async {
//! let mut file = File::create("a.txt").await?;
//! file.write_all(b"Hello, world!").await?;
//! file.flush().await?;
//! # std::io::Result::Ok(()) });
//! ```

use std::io::{Error, ErrorKind, Result};

use js_sys::Promise;
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use web_sys::{
    DomException, Exception, FileSystemDirectoryHandle, FileSystemFileHandle, StorageManager,
    WorkerGlobalScope,
};

mod directory;
mod external;
mod file;
mod metadata;
mod open_options;
mod read;
mod seek;
mod write;

pub use directory::*;
pub use file::*;
pub use metadata::*;
pub use open_options::*;

fn from_js_error(value: JsValue) -> Error {
    if value.is_instance_of::<DomException>() {
        let handle = value.unchecked_into::<DomException>();
        return match handle.name().as_str() {
            "NotAllowedError" => Error::new(ErrorKind::PermissionDenied, handle.message()),
            "NotFoundError" => Error::new(ErrorKind::NotFound, handle.message()),
            "TypeMismatchError" => Error::new(ErrorKind::Other, handle.message()),
            "InvalidModificationError" => {
                Error::new(ErrorKind::DirectoryNotEmpty, handle.message())
            }
            _ => Error::other(handle.name()),
        };
    }
    if value.is_instance_of::<Exception>() {
        let handle = value.unchecked_into::<Exception>();
        return Error::other(handle.name());
    }
    if let Some(err) = value.dyn_ref::<web_sys::js_sys::TypeError>() {
        let message: String = err.message().into();
        return Error::new(ErrorKind::InvalidInput, message);
    }
    Error::other("unknown error")
}

fn from_js<V: JsCast>(value: JsValue) -> Result<V> {
    value.dyn_into::<V>().map_err(from_js_error)
}

async fn resolve<V: JsCast>(promise: Promise) -> Result<V> {
    from_js(JsFuture::from(promise).await.map_err(from_js_error)?)
}

async fn resolve_undefined(promise: Promise) -> Result<()> {
    JsFuture::from(promise).await.map_err(from_js_error)?;
    Ok(())
}

async fn storage() -> Result<StorageManager> {
    if js_sys::global().is_instance_of::<WorkerGlobalScope>() {
        let global = js_sys::global().unchecked_into::<WorkerGlobalScope>();
        return Ok(global.navigator().storage());
    }
    Err(Error::new(
        ErrorKind::Unsupported,
        "storage manage not accessible",
    ))
}

async fn root_directory() -> Result<FileSystemDirectoryHandle> {
    let storage = storage().await?;
    let res = JsFuture::from(storage.get_directory())
        .await
        .map_err(|_| Error::new(ErrorKind::Unsupported, "unable to access root directory"))?;
    res.dyn_into::<FileSystemDirectoryHandle>()
        .map_err(|_| Error::new(ErrorKind::Unsupported, "unable to cast root directory"))
}

#[derive(Clone, Copy, Debug)]
pub enum FileType {
    File,
    Directory,
}

impl FileType {
    pub const fn is_dir(&self) -> bool {
        matches!(self, Self::Directory)
    }

    pub const fn is_file(&self) -> bool {
        matches!(self, Self::File)
    }

    pub const fn is_symlink(&self) -> bool {
        false
    }
}

#[derive(Debug)]
enum Entry {
    Directory(FileSystemDirectoryHandle),
    File(FileSystemFileHandle),
}

impl Entry {
    fn name(&self) -> String {
        match self {
            Self::Directory(inner) => inner.name(),
            Self::File(inner) => inner.name(),
        }
    }

    fn try_from_js_value(value: JsValue) -> Result<Self> {
        if value.is_instance_of::<FileSystemFileHandle>() {
            Ok(Self::File(value.unchecked_into()))
        } else if value.is_instance_of::<FileSystemDirectoryHandle>() {
            Ok(Self::Directory(value.unchecked_into()))
        } else {
            Err(Error::new(ErrorKind::InvalidData, "unable to caste handle"))
        }
    }

    fn file_type(&self) -> FileType {
        match self {
            Self::Directory(_) => FileType::Directory,
            Self::File(_) => FileType::File,
        }
    }
}

impl Entry {
    async fn from_directory(parent: &FileSystemDirectoryHandle, name: &str) -> Result<Self> {
        let dir_promise = parent.get_directory_handle(name);
        let dir_res = crate::resolve::<FileSystemDirectoryHandle>(dir_promise).await;

        let file_promise = parent.get_file_handle(name);
        let file_res = crate::resolve::<FileSystemFileHandle>(file_promise).await;

        dir_res.map(Self::Directory).or(file_res.map(Self::File))
    }
}