browser-fs 0.1.0

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

use web_sys::{FileSystemFileHandle, FileSystemSyncAccessHandle};

use crate::{Metadata, OpenOptions};

// NOTE: `FileSystemFileHandle` can return a `File` that is also a `Blob`. From
// that we could read the content of the file easily. This could be a solution
// to the limitation of having a single access handle per file.

/// An open file on the filesystem.
///
/// Depending on what options the file was opened with, this type can be used
/// for reading and/or writing.
///
/// Files are automatically closed when they get dropped and any errors detected
/// on closing are ignored. Use the [`sync_all()`][`File::sync_all()`] method
/// before dropping a file if such errors need to be handled.
///
/// **NOTE:** If writing to a file, make sure to call
/// [`flush()`][`futures_lite::io::AsyncWriteExt::flush()`],
/// [`sync_data()`][`File::sync_data()`], or [`sync_all()`][`File::sync_all()`]
/// before dropping the file or else some written data might get lost!
///
/// # 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(()) });
/// ```
///
/// Read the contents of a file into a vector of bytes:
///
/// ```no_run
/// use browser_fs::File;
/// use futures_lite::io::AsyncReadExt;
///
/// # futures_lite::future::block_on(async {
/// let mut file = File::open("a.txt").await?;
///
/// let mut contents = Vec::new();
/// file.read_to_end(&mut contents).await?;
/// # std::io::Result::Ok(()) });
/// ```
#[derive(Debug)]
pub struct File {
    pub(crate) file: FileSystemFileHandle,
    pub(crate) access: FileSystemSyncAccessHandle,
    pub(crate) offset: u32,
}

impl File {
    pub(crate) fn new(
        file: FileSystemFileHandle,
        access: FileSystemSyncAccessHandle,
        offset: u32,
    ) -> Self {
        Self {
            file,
            access,
            offset,
        }
    }

    /// Opens a file in read-only mode.
    ///
    /// See the [`OpenOptions::open()`] function for more options.
    ///
    /// # Errors
    ///
    /// An error will be returned in the following situations:
    ///
    /// * `path` does not point to an existing file.
    /// * The current process lacks permissions to read the file.
    /// * Some other I/O error occurred.
    /// * The file is already open.
    ///
    /// For more details, see the list of errors documented by
    /// [`OpenOptions::open()`].
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use browser_fs::File;
    ///
    /// # futures_lite::future::block_on(async {
    /// let file = File::open("a.txt").await?;
    /// # std::io::Result::Ok(()) });
    /// ```
    pub async fn open<P: AsRef<Path>>(path: P) -> Result<File> {
        OpenOptions::new().read(true).open(path).await
    }

    /// Opens a file in write-only mode.
    ///
    /// This method will create a file if it does not exist, and will append to
    /// it if it does.
    ///
    /// See the [`OpenOptions::open`] function for more options.
    ///
    /// # Errors
    ///
    /// An error will be returned in the following situations:
    ///
    /// * The file's parent directory does not exist.
    /// * The current process lacks permissions to write to the file.
    /// * Some other I/O error occurred.
    /// * The file is already open.
    ///
    /// For more details, see the list of errors documented by
    /// [`OpenOptions::open()`].
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use browser_fs::File;
    ///
    /// # futures_lite::future::block_on(async {
    /// let file = File::create("a.txt").await?;
    /// # std::io::Result::Ok(()) });
    /// ```
    pub async fn create<P: AsRef<Path>>(path: P) -> Result<File> {
        OpenOptions::new().create(true).write(true).open(path).await
    }

    /// Opens a file in write-only mode.
    ///
    /// This method will create a file if it does not exist, and will truncate
    /// it if it does.
    ///
    /// See the [`OpenOptions::open`] function for more options.
    ///
    /// # Errors
    ///
    /// An error will be returned in the following situations:
    ///
    /// * The file's parent directory does not exist.
    /// * The current process lacks permissions to write to the file.
    /// * Some other I/O error occurred.
    /// * The file is already open.
    ///
    /// For more details, see the list of errors documented by
    /// [`OpenOptions::open()`].
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use browser_fs::File;
    ///
    /// # futures_lite::future::block_on(async {
    /// let file = File::create_new("a.txt").await?;
    /// # std::io::Result::Ok(()) });
    /// ```
    pub async fn create_new<P: AsRef<Path>>(path: P) -> Result<File> {
        OpenOptions::new()
            .create_new(true)
            .write(true)
            .open(path)
            .await
    }

    /// Truncates the file.
    ///
    /// If `size` is less than the current file size, then the file will be
    /// truncated. If it is greater than the current file size, then nothing
    /// will happend.
    ///
    /// The file's cursor stays at the same position, even if the cursor ends up
    /// being past the end of the file after this operation.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use browser_fs::File;
    ///
    /// # futures_lite::future::block_on(async {
    /// let mut file = File::create("a.txt").await?;
    /// file.set_len(10).await?;
    /// # std::io::Result::Ok(()) });
    /// ```
    pub async fn set_len(&self, size: u64) -> Result<()> {
        self.access
            .truncate_with_u32(size as u32)
            .map_err(crate::from_js_error)?;
        Ok(())
    }

    /// Synchronizes buffered contents and metadata to disk.
    ///
    /// This function will ensure that all in-memory data reaches the
    /// filesystem.
    ///
    /// This can be used to handle errors that would otherwise only be caught by
    /// closing the file. When a file is dropped, errors in synchronizing
    /// this in-memory data are ignored.
    ///
    /// # Examples
    ///
    /// ```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.sync_all().await?;
    /// # std::io::Result::Ok(()) });
    /// ```
    pub async fn sync_all(&self) -> Result<()> {
        self.sync_data().await
    }

    /// Synchronizes buffered contents to disk.
    ///
    /// This is similar to [`sync_all()`][`File::sync_data()`], except that file
    /// metadata may not be synchronized.
    ///
    /// This is intended for use cases that must synchronize the contents of the
    /// file, but don't need the file metadata synchronized to disk.
    ///
    /// Note that some platforms may simply implement this in terms of
    /// [`sync_all()`][`File::sync_data()`].
    ///
    /// # Examples
    ///
    /// ```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.sync_data().await?;
    /// # std::io::Result::Ok(()) });
    /// ```
    pub async fn sync_data(&self) -> Result<()> {
        self.access.flush().map_err(crate::from_js_error)?;
        Ok(())
    }

    /// Queries metadata about the underlying file.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # futures_lite::future::block_on(async {
    /// let mut file = browser_fs::File::open("a.txt").await?;
    /// let metadata = file.metadata().await?;
    /// # std::io::Result::Ok(()) });
    /// ```
    pub async fn metadata(&self) -> Result<Metadata> {
        Metadata::from_file_handle(&self.file).await
    }
}

impl Drop for File {
    fn drop(&mut self) {
        self.access.close();
    }
}

/// Removes a file.
///
/// # Errors
///
/// An error will be returned in the following situations:
///
/// * `path` does not point to an existing file.
/// * The current process lacks permissions to remove the file.
/// * Some other I/O error occurred.
///
/// # Examples
///
/// ```no_run
/// # futures_lite::future::block_on(async {
/// browser_fs::remove_file("a.txt").await?;
/// # std::io::Result::Ok(()) });
/// ```
pub async fn remove_file<P: AsRef<Path>>(path: P) -> Result<()> {
    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 promise = dir.remove_entry(fname.to_string_lossy().as_ref());
    crate::resolve_undefined(promise).await
}

/// Reads the entire contents of a file as raw bytes.
///
/// This is a convenience function for reading entire files. It pre-allocates a
/// buffer based on the file size when available, so it is typically faster than
/// manually opening a file and reading from it.
///
/// If you want to read the contents as a string, use [`read_to_string()`]
/// instead.
///
/// # Errors
///
/// An error will be returned in the following situations:
///
/// * `path` does not point to an existing file.
/// * The current process lacks permissions to read the file.
/// * Some other I/O error occurred.
///
/// # Examples
///
/// ```no_run
/// # futures_lite::future::block_on(async {
/// let contents = browser_fs::read("a.txt").await?;
/// # std::io::Result::Ok(()) });
/// ```
pub async fn read<P: AsRef<Path>>(path: P) -> Result<Vec<u8>> {
    use futures_lite::AsyncReadExt;

    let mut file = File::open(path).await?;
    let mut buf = Vec::new();
    file.read_to_end(&mut buf).await?;
    Ok(buf)
}

/// Reads the entire contents of a file as a string.
///
/// This is a convenience function for reading entire files. It pre-allocates a
/// string based on the file size when available, so it is typically faster than
/// manually opening a file and reading from it.
///
/// If you want to read the contents as raw bytes, use [`read()`] instead.
///
/// # Errors
///
/// An error will be returned in the following situations:
///
/// * `path` does not point to an existing file.
/// * The current process lacks permissions to read the file.
/// * The contents of the file cannot be read as a UTF-8 string.
/// * Some other I/O error occurred.
///
/// # Examples
///
/// ```no_run
/// # futures_lite::future::block_on(async {
/// let contents = browser_fs::read_to_string("a.txt").await?;
/// # std::io::Result::Ok(()) });
/// ```
pub async fn read_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
    use futures_lite::AsyncReadExt;

    let mut file = File::open(path).await?;
    let mut buf = String::new();
    file.read_to_string(&mut buf).await?;
    Ok(buf)
}

/// Writes a slice of bytes as the new contents of a file.
///
/// This function will create a file if it does not exist, and will entirely
/// replace its contents if it does.
///
/// # Errors
///
/// An error will be returned in the following situations:
///
/// * The file's parent directory does not exist.
/// * The current process lacks permissions to write to the file.
/// * Some other I/O error occurred.
///
/// # Examples
///
/// ```no_run
/// # futures_lite::future::block_on(async {
/// browser_fs::write("a.txt", b"Hello world!").await?;
/// # std::io::Result::Ok(()) });
/// ```
pub async fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
    use futures_lite::AsyncWriteExt;

    let mut file = File::create_new(path).await?;
    file.write_all(contents.as_ref()).await?;
    file.flush().await?;
    file.close().await?;
    Ok(())
}