async-fs-io 0.2.0

Async-first filesystem primitives, bounded reads, streaming directory access, and temporary resources
Documentation
use crate::{
    AsyncFile,
    DirectoryEntryKind,
    DirectoryReader,
    FsError,
    TempDir,
    atomic_write_string,
    canonicalize,
    read_bounded,
    read_string_bounded,
    read_string_bounded_if_exists,
    remove_file,
    set_permissions,
    symlink_metadata,
    try_exists,
};

async fn test_root() -> TempDir {
    TempDir::create(std::env::temp_dir())
        .await
        .expect("create async test root")
}

#[tokio::test]
async fn bounded_read_rejects_a_file_larger_than_the_declared_limit() {
    let root = test_root().await;
    let path = root.path().join("large.txt");
    let mut file = AsyncFile::create(&path).await.expect("create file");
    file.write_all(b"0123456789").await.expect("write file");
    file.flush().await.expect("flush file");

    let result = read_bounded(&path, 9).await;

    assert!(matches!(result, Err(FsError::InvalidRequest(_))));
    root.remove().await.expect("remove test root");
}

#[tokio::test]
async fn bounded_text_read_accepts_content_within_the_limit() {
    let root = test_root().await;
    let path = root.path().join("text.txt");
    atomic_write_string(&path, "hello")
        .await
        .expect("write text");

    assert_eq!(
        read_string_bounded(&path, 5).await.expect("read text"),
        "hello"
    );
    root.remove().await.expect("remove test root");
}

#[tokio::test]
async fn directory_reader_keeps_one_entry_at_a_time() {
    let root = test_root().await;
    let first = root.path().join("first");
    let second = root.path().join("second");
    AsyncFile::create(&first).await.expect("create first");
    AsyncFile::create(&second).await.expect("create second");

    let mut reader = DirectoryReader::open(root.path())
        .await
        .expect("open directory");
    let mut seen = 0usize;
    while let Some(entry) = reader.next().await.expect("read directory entry") {
        assert_eq!(entry.kind(), DirectoryEntryKind::File);
        seen += 1;
    }

    assert_eq!(seen, 2);
    root.remove().await.expect("remove test root");
}

#[tokio::test]
async fn missing_directory_and_file_are_distinguished_without_blocking_io() {
    let root = test_root().await;
    let missing_dir = root.path().join("missing-directory");
    assert!(
        DirectoryReader::open_if_exists(&missing_dir)
            .await
            .expect("inspect missing directory")
            .is_none()
    );
    assert!(
        !try_exists(&missing_dir)
            .await
            .expect("inspect missing path")
    );
    assert_eq!(
        read_string_bounded_if_exists(&root.path().join("missing.txt"), 32)
            .await
            .expect("read missing text"),
        None
    );
    root.remove().await.expect("remove test root");
}

#[tokio::test]
async fn symlink_metadata_is_available_through_the_async_boundary() {
    let root = test_root().await;
    let path = root.path().join("file.txt");
    AsyncFile::create(&path).await.expect("create file");
    assert!(
        symlink_metadata(&path)
            .await
            .expect("read metadata")
            .is_file()
    );
    root.remove().await.expect("remove test root");
}

#[tokio::test]
async fn canonicalize_and_set_permissions_use_async_filesystem_boundary() {
    let root = test_root().await;
    let path = root.path().join("nested");
    crate::ensure_dir(&path)
        .await
        .expect("create nested directory");

    let canonical = canonicalize(&path).await.expect("canonicalize path");
    assert!(canonical.is_absolute());
    assert_eq!(canonical.file_name(), Some(std::ffi::OsStr::new("nested")));

    let file = path.join("file.txt");
    AsyncFile::create(&file).await.expect("create file");
    let permissions = symlink_metadata(&file)
        .await
        .expect("read metadata")
        .permissions();
    set_permissions(&file, permissions)
        .await
        .expect("set permissions");
    root.remove().await.expect("remove test root");
}

#[tokio::test]
async fn removing_a_missing_file_is_an_error() {
    let root = test_root().await;
    let result = remove_file(root.path().join("missing.txt")).await;
    assert!(matches!(result, Err(FsError::Write { .. })));
    root.remove().await.expect("remove test root");
}

#[tokio::test]
async fn temporary_directory_cleanup_is_explicitly_async() {
    let root = test_root().await;
    let nested = TempDir::create(root.path())
        .await
        .expect("create nested root");
    let nested_path = nested.path().to_owned();
    nested.remove().await.expect("remove nested root");
    assert!(
        AsyncFile::open_if_exists(nested_path)
            .await
            .expect("inspect nested root")
            .is_none()
    );
    root.remove().await.expect("remove test root");
}