browser-fs 0.1.0

A browser-based filesystem implementation for WebAssembly applications
Documentation
use std::borrow::Cow;
use std::ffi::OsString;
use std::io::{Error, ErrorKind, Result};
use std::path::{Component, Path, PathBuf};

use futures_lite::StreamExt;
use js_sys::JsString;
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::stream::JsStream;
use web_sys::{FileSystemDirectoryHandle, FileSystemGetDirectoryOptions, FileSystemRemoveOptions};

use crate::{Entry, FileType};

/// An entry in a directory.
///
/// A stream of entries in a directory is returned by [`read_dir()`].
#[derive(Debug)]
pub struct DirEntry {
    parent: PathBuf,
    entry: Entry,
}

impl DirEntry {
    /// Returns the bare name of this entry without the leading path.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use futures_lite::stream::StreamExt;
    ///
    /// # futures_lite::future::block_on(async {
    /// let mut dir = browser_fs::read_dir(".").await?;
    ///
    /// while let Some(entry) = dir.try_next().await? {
    ///     println!("{}", entry.file_name().to_string_lossy());
    /// }
    /// # std::io::Result::Ok(()) });
    /// ```
    pub fn file_name(&self) -> OsString {
        self.entry.name().into()
    }

    /// Returns the full path to this entry.
    ///
    /// The full path is created by joining the original path passed to
    /// [`read_dir()`] with the name of this entry.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use futures_lite::stream::StreamExt;
    ///
    /// # futures_lite::future::block_on(async {
    /// let mut dir = browser_fs::read_dir(".").await?;
    ///
    /// while let Some(entry) = dir.try_next().await? {
    ///     println!("{:?}", entry.path());
    /// }
    /// # std::io::Result::Ok(()) });
    /// ```
    pub fn path(&self) -> PathBuf {
        self.parent.join(self.entry.name())
    }

    /// Reads the file type for this entry.
    ///
    /// This function will not traverse symbolic links if this entry points at
    /// one.
    ///
    /// # Errors
    ///
    /// An error will be returned in the following situations:
    ///
    /// * This entry does not point to an existing file or directory anymore.
    /// * The current process lacks permissions to read this entry's metadata.
    /// * Some other I/O error occurred.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use futures_lite::stream::StreamExt;
    ///
    /// # futures_lite::future::block_on(async {
    /// let mut dir = browser_fs::read_dir(".").await?;
    ///
    /// while let Some(entry) = dir.try_next().await? {
    ///     println!("{:?}", entry.file_type().await?);
    /// }
    /// # std::io::Result::Ok(()) });
    /// ```
    pub async fn file_type(&self) -> Result<FileType> {
        // just to keep in sync with async-fs
        Ok(self.entry.file_type())
    }
}

pub(crate) async fn get_directory(path: &Path) -> Result<FileSystemDirectoryHandle> {
    let mut parts = split_path(path)?;
    parts.reverse();
    let mut latest = crate::root_directory().await?;
    while let Some(next) = parts.pop() {
        let promise = latest.get_directory_handle(next.as_ref());
        latest = crate::resolve(promise).await?;
    }
    Ok(latest)
}

/// Creates a new, empty directory at the provided path
///
/// # Errors
///
/// This function will return an error in the following situations, but is not
/// limited to just these cases:
///
/// * User lacks permissions to create directory at `path`.
/// * A parent of the given path doesn't exist. (To create a directory and all
///   its missing parents at the same time, use the [`create_dir_all`]
///   function.)
///
/// # Examples
///
/// ```no_run
/// use browser_fs::create_dir;
///
/// # futures_lite::future::block_on(async {
/// create_dir("/foo").await?;
/// # std::io::Result::Ok(()) });
/// ```
pub async fn create_dir<P: AsRef<Path>>(path: P) -> Result<()> {
    let mut parts = split_path(path.as_ref())?;
    parts.reverse();
    let opts = FileSystemGetDirectoryOptions::new();
    let mut latest = crate::root_directory().await?;
    while let Some(next) = parts.pop() {
        opts.set_create(parts.is_empty());
        let promise = latest.get_directory_handle_with_options(next.as_ref(), &opts);
        latest = crate::resolve(promise).await?;
    }
    Ok(())
}

/// Recursively create a directory and all of its parent components if they
/// are missing.
///
/// # Errors
///
/// This function will return an error in the following situations, but is not
/// limited to just these cases:
///
/// * If any directory in the path specified by `path` does not already exist
///   and it could not be created otherwise. The specific error conditions for
///   when a directory is being created (after it is determined to not exist)
///   are outlined by [`fs::create_dir`].
///
/// Notable exception is made for situations where any of the directories
/// specified in the `path` could not be created as it was being created
/// concurrently. Such cases are considered to be successful. That is, calling
/// `create_dir_all` concurrently from multiple threads or processes is
/// guaranteed not to fail due to a race condition with itself.
///
/// [`fs::create_dir`]: create_dir
///
/// # Examples
///
/// ```no_run
/// use browser_fs::create_dir_all;
///
/// # futures_lite::future::block_on(async {
/// create_dir_all("/foo/bar/baz").await?;
/// # std::io::Result::Ok(()) });
/// ```
pub async fn create_dir_all<P: AsRef<Path>>(path: P) -> Result<()> {
    let mut parts = split_path(path.as_ref())?;
    parts.reverse();
    let opts = FileSystemGetDirectoryOptions::new();
    opts.set_create(true);
    let mut latest = crate::root_directory().await?;
    while let Some(next) = parts.pop() {
        let promise = latest.get_directory_handle_with_options(next.as_ref(), &opts);
        latest = crate::resolve(promise).await?;
    }
    Ok(())
}

/// Splits a path into components and remove potential parent dirs
fn split_path(path: &Path) -> Result<Vec<Cow<'_, str>>> {
    let mut res = Vec::new();
    for item in path.components() {
        match item {
            Component::RootDir => {
                res.clear();
            }
            Component::Normal(name) => {
                res.push(name.to_string_lossy());
            }
            Component::ParentDir => {
                if res.pop().is_none() {
                    return Err(Error::new(
                        ErrorKind::InvalidInput,
                        "unable to reach provided path",
                    ));
                }
            }
            Component::Prefix(_) => {
                return Err(Error::new(
                    ErrorKind::InvalidInput,
                    "invalid path with prefix",
                ))
            }
            Component::CurDir => {}
        }
    }
    Ok(res)
}

/// Removes an empty directory.
///
/// Note that this function can only delete an empty directory. If you want to
/// delete a directory and all of its contents, use [`remove_dir_all()`]
/// instead.
///
/// # Errors
///
/// An error will be returned in the following situations:
///
/// * `path` is not an existing and empty directory.
/// * The current process lacks permissions to remove the directory.
/// * Some other I/O error occurred.
///
/// # Examples
///
/// ```no_run
/// # futures_lite::future::block_on(async {
/// browser_fs::remove_dir("./some/directory").await?;
/// # std::io::Result::Ok(()) });
/// ```
pub async fn remove_dir<P: AsRef<Path>>(path: P) -> Result<()> {
    let Some(parent) = path.as_ref().parent() else {
        return Err(Error::new(
            ErrorKind::InvalidInput,
            "unable to find parent directory",
        ));
    };
    let Some(name) = path.as_ref().file_name() else {
        return Err(Error::new(
            ErrorKind::InvalidInput,
            "unable to find directory name",
        ));
    };
    let parent = get_directory(parent).await?;
    let promise = parent.remove_entry(name.to_string_lossy().as_ref());
    crate::resolve_undefined(promise).await
}

/// Removes a directory and all of its contents.
///
/// # Errors
///
/// An error will be returned in the following situations:
///
/// * `path` is not an existing directory.
/// * The current process lacks permissions to remove the directory.
/// * Some other I/O error occurred.
///
/// # Examples
///
/// ```no_run
/// # futures_lite::future::block_on(async {
/// browser_fs::remove_dir_all("./some/directory").await?;
/// # std::io::Result::Ok(()) });
/// ```
pub async fn remove_dir_all<P: AsRef<Path>>(path: P) -> Result<()> {
    let Some(parent) = path.as_ref().parent() else {
        return Err(Error::new(
            ErrorKind::InvalidInput,
            "unable to find parent directory",
        ));
    };
    let Some(name) = path.as_ref().file_name() else {
        return Err(Error::new(
            ErrorKind::InvalidInput,
            "unable to find directory name",
        ));
    };
    let parent = get_directory(parent).await?;
    let opts = FileSystemRemoveOptions::new();
    opts.set_recursive(true);
    let promise = parent.remove_entry_with_options(name.to_string_lossy().as_ref(), &opts);
    crate::resolve_undefined(promise).await
}

fn read_dir_entry(parent: &Path, value: JsValue) -> Result<DirEntry> {
    let array: web_sys::js_sys::Array = value.dyn_into().map_err(crate::from_js_error)?;
    let _name: JsString = array.get(0).dyn_into().map_err(crate::from_js_error)?;
    let entry = crate::Entry::try_from_js_value(array.get(1))?;

    Ok(DirEntry {
        parent: parent.to_path_buf(),
        entry,
    })
}

/// Returns a stream of entries in a directory.
///
/// The stream yields items of type [`std::io::Result`]`<`[`DirEntry`]`>`. Note
/// that I/O errors can occur while reading from the stream.
///
/// # Errors
///
/// An error will be returned in the following situations:
///
/// * `path` does not point to an existing directory.
/// * The current process lacks permissions to read the contents of the
///   directory.
/// * Some other I/O error occurred.
///
/// # Examples
///
/// ```no_run
/// # futures_lite::future::block_on(async {
/// use futures_lite::stream::StreamExt;
///
/// let mut entries = browser_fs::read_dir(".").await?;
///
/// while let Some(entry) = entries.try_next().await? {
///     println!("{}", entry.file_name().to_string_lossy());
/// }
/// # std::io::Result::Ok(()) });
/// ```
pub async fn read_dir<P: AsRef<Path>>(
    path: P,
) -> Result<impl futures_lite::Stream<Item = Result<DirEntry>>> {
    let directory = get_directory(path.as_ref()).await?;
    let stream = JsStream::from(directory.entries());

    Ok(stream.map(move |entry| {
        let entry = entry.map_err(crate::from_js_error)?;
        read_dir_entry(path.as_ref(), entry)
    }))
}

#[cfg(test)]
mod tests {
    #[test]
    fn split_path() {
        let path = std::path::PathBuf::from("/foo/bar/baz");
        assert_eq!(super::split_path(&path).unwrap(), vec!["foo", "bar", "baz"]);
        let path = std::path::PathBuf::from("/foo/bar/../baz");
        assert_eq!(super::split_path(&path).unwrap(), vec!["foo", "baz"]);
    }
}