browser-fs 0.1.0

A browser-based filesystem implementation for WebAssembly applications
Documentation
use std::io::{Error, ErrorKind};
use std::path::Path;

use wasm_bindgen::prelude::*;
use web_sys::{FileSystemFileHandle, FileSystemGetFileOptions, FileSystemSyncAccessHandle};

#[wasm_bindgen]
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct FileOptions {
    pub append: bool,
    pub create: bool,
    pub create_new: bool,
    pub read: bool,
    pub truncate: bool,
    pub write: bool,
}

/// A builder for opening files with configurable options.
///
/// Files can be opened in [`read`][`OpenOptions::read()`] and/or
/// [`write`][`OpenOptions::write()`] mode.
///
/// The [`append`][`OpenOptions::append()`] option opens files in a special
/// writing mode that moves the file cursor to the end of file before every
/// write operation.
///
/// It is also possible to [`truncate`][`OpenOptions::truncate()`] the file
/// right after opening, to [`create`][`OpenOptions::create()`] a file if it
/// doesn't exist yet, or to always create a new file with
/// [`create_new`][`OpenOptions::create_new()`].
///
/// # Examples
///
/// Open a file for reading:
///
/// ```no_run
/// use browser_fs::OpenOptions;
///
/// # futures_lite::future::block_on(async {
/// let file = OpenOptions::new().read(true).open("a.txt").await?;
/// # std::io::Result::Ok(()) });
/// ```
///
/// Open a file for both reading and writing, and create it if it doesn't exist
/// yet:
///
/// ```no_run
/// use browser_fs::OpenOptions;
///
/// # futures_lite::future::block_on(async {
/// let file = OpenOptions::new()
///     .read(true)
///     .write(true)
///     .create(true)
///     .open("a.txt")
///     .await?;
/// # std::io::Result::Ok(()) });
/// ```
#[derive(Debug, Clone, Copy)]
pub struct OpenOptions {
    inner: FileOptions,
}

impl Default for OpenOptions {
    fn default() -> Self {
        Self::new()
    }
}

impl OpenOptions {
    /// Creates a blank set of options.
    ///
    /// All options are initially set to `false`.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use browser_fs::OpenOptions;
    ///
    /// # futures_lite::future::block_on(async {
    /// let file = OpenOptions::new().read(true).open("a.txt").await?;
    /// # std::io::Result::Ok(()) });
    /// ```
    pub fn new() -> Self {
        Self {
            inner: FileOptions::default(),
        }
    }

    /// Configures the option for append mode.
    ///
    /// When set to `true`, this option means the file will be writable after
    /// opening and the file cursor will be moved to the end of file before
    /// every write operaiton.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use browser_fs::OpenOptions;
    ///
    /// # futures_lite::future::block_on(async {
    /// let file = OpenOptions::new().append(true).open("a.txt").await?;
    /// # std::io::Result::Ok(()) });
    /// ```
    pub fn append(mut self, append: bool) -> OpenOptions {
        self.inner.append = append;
        self
    }

    /// Configures the option for creating a new file if it doesn't exist.
    ///
    /// When set to `true`, this option means a new file will be created if it
    /// doesn't exist.
    ///
    /// The file must be opened in [`write`][`OpenOptions::write()`] or
    /// [`append`][`OpenOptions::append()`] mode for file creation to work.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use browser_fs::OpenOptions;
    ///
    /// # futures_lite::future::block_on(async {
    /// let file = OpenOptions::new()
    ///     .write(true)
    ///     .create(true)
    ///     .open("a.txt")
    ///     .await?;
    /// # std::io::Result::Ok(()) });
    /// ```
    pub fn create(mut self, create: bool) -> OpenOptions {
        self.inner.create = create;
        self
    }

    /// Configures the option for creating a new file or failing if it already
    /// exists.
    ///
    /// When set to `true`, this option means a new file will be created, or the
    /// open operation will fail if the file already exists.
    ///
    /// The file must be opened in [`write`][`OpenOptions::write()`] or
    /// [`append`][`OpenOptions::append()`] mode for file creation to work.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use browser_fs::OpenOptions;
    ///
    /// # futures_lite::future::block_on(async {
    /// let file = OpenOptions::new()
    ///     .write(true)
    ///     .create_new(true)
    ///     .open("a.txt")
    ///     .await?;
    /// # std::io::Result::Ok(()) });
    /// ```
    pub fn create_new(mut self, create_new: bool) -> OpenOptions {
        self.inner.create_new = create_new;
        self
    }

    /// Configures the option for read mode.
    ///
    /// When set to `true`, this option means the file will be readable after
    /// opening.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use browser_fs::OpenOptions;
    ///
    /// # futures_lite::future::block_on(async {
    /// let file = OpenOptions::new().read(true).open("a.txt").await?;
    /// # std::io::Result::Ok(()) });
    /// ```
    pub fn read(mut self, read: bool) -> OpenOptions {
        self.inner.read = read;
        self
    }

    /// Configures the option for truncating the previous file.
    ///
    /// When set to `true`, the file will be truncated to the length of 0 bytes.
    ///
    /// The file must be opened in [`write`][`OpenOptions::write()`] or
    /// [`append`][`OpenOptions::append()`] mode for truncation to work.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use browser_fs::OpenOptions;
    ///
    /// # futures_lite::future::block_on(async {
    /// let file = OpenOptions::new()
    ///     .write(true)
    ///     .truncate(true)
    ///     .open("a.txt")
    ///     .await?;
    /// # std::io::Result::Ok(()) });
    /// ```
    pub fn truncate(mut self, truncate: bool) -> OpenOptions {
        self.inner.truncate = truncate;
        self
    }

    /// Configures the option for write mode.
    ///
    /// When set to `true`, this option means the file will be writable after
    /// opening.
    ///
    /// If the file already exists, write calls on it will overwrite the
    /// previous contents without truncating it.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use browser_fs::OpenOptions;
    ///
    /// # futures_lite::future::block_on(async {
    /// let file = OpenOptions::new().write(true).open("a.txt").await?;
    /// # std::io::Result::Ok(()) });
    /// ```
    pub fn write(mut self, write: bool) -> OpenOptions {
        self.inner.write = write;
        self
    }

    /// Opens a file with the configured options.
    ///
    /// # Errors
    ///
    /// An error will be returned in the following situations:
    ///
    /// * The file does not exist and neither [`create`] nor [`create_new`] were
    ///   set.
    /// * The file's parent directory does not exist.
    /// * The current process lacks permissions to open the file in the
    ///   configured mode.
    /// * The file already exists and [`create_new`] was set.
    /// * Invalid combination of options was used, like [`truncate`] was set but
    ///   [`write`] wasn't, or none of [`read`], [`write`], and [`append`] modes
    ///   was set.
    /// * An OS-level occurred, like too many files are open or the file name is
    ///   too long.
    /// * Some other I/O error occurred.
    ///
    /// [`read`]: `OpenOptions::read()`
    /// [`write`]: `OpenOptions::write()`
    /// [`append`]: `OpenOptions::append()`
    /// [`truncate`]: `OpenOptions::truncate()`
    /// [`create`]: `OpenOptions::create()`
    /// [`create_new`]: `OpenOptions::create_new()`
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use browser_fs::OpenOptions;
    ///
    /// # futures_lite::future::block_on(async {
    /// let file = OpenOptions::new().read(true).open("a.txt").await?;
    /// # std::io::Result::Ok(()) });
    /// ```
    pub async fn open<P: AsRef<Path>>(&self, path: P) -> std::io::Result<crate::file::File> {
        let fpath = path.as_ref();
        let dir = if let Some(parent) = fpath.parent() {
            crate::get_directory(parent).await?
        } else {
            crate::root_directory().await?
        };
        let Some(fname) = fpath.file_name() else {
            return Err(Error::new(ErrorKind::InvalidInput, "filename not found"));
        };
        let opts = FileSystemGetFileOptions::new();
        opts.set_create(self.inner.create || self.inner.create_new);
        let promise = dir.get_file_handle_with_options(fname.to_string_lossy().as_ref(), &opts);
        let file = crate::resolve::<FileSystemFileHandle>(promise).await?;
        let promise = file.create_sync_access_handle();
        let access = crate::resolve::<FileSystemSyncAccessHandle>(promise).await?;
        let mut offset = 0;

        if self.inner.write {
            if self.inner.truncate || self.inner.create_new {
                access.truncate_with_u32(0).map_err(crate::from_js_error)?;
            } else {
                offset = access.get_size().map_err(crate::from_js_error)? as u32;
            }
        }

        Ok(crate::file::File::new(file, access, offset))
    }
}