switchy_fs 0.2.0

Switchy File-system package
Documentation

Switchy Filesystem (switchy_fs)

Cross-platform filesystem abstraction with sync and async operations.

Overview

The switchy_fs package provides:

  • Sync and Async APIs: Both synchronous and asynchronous filesystem operations
  • Cross-platform: Abstraction over different filesystem implementations
  • Feature-gated Backends: Standard library, Tokio, and simulator implementations
  • File Operations: Create, read, write, seek, and delete operations
  • Directory Operations: Create, remove, read, and walk directories with deterministic ordering
  • Temporary Directories: Automatic cleanup of temporary directories
  • Flexible Options: Configurable file open options
  • Path Utilities: Check path existence across backends

Features

Operation Modes

  • Synchronous: Blocking filesystem operations via sync module
  • Asynchronous: Non-blocking operations via unsync module

Backend Implementations

  • Standard Library: std::fs based implementation
  • Tokio: tokio::fs based async implementation
  • Simulator: Mock filesystem for testing

File Operations

  • File Creation: Create new files with various options
  • File Reading: Read file contents to string
  • File Writing: Write data to files
  • File Seeking: Random access file positioning
  • Directory Management: Create, remove, read, and walk directories
  • Directory Traversal: Sorted directory reading and recursive walking
  • Path Checking: Check if files or directories exist

Open Options

  • Create: Create file if it doesn't exist
  • Append: Append to existing file content
  • Read: Open file for reading
  • Write: Open file for writing
  • Truncate: Clear existing file content

Installation

Add this to your Cargo.toml:

[dependencies]
switchy_fs = { path = "../fs" }

# With specific features
switchy_fs = {
    path = "../fs",
    features = ["sync", "async", "std", "tokio"]
}

# For testing
switchy_fs = {
    path = "../fs",
    features = ["simulator", "sync", "async"]
}

Usage

Synchronous File Operations

use switchy_fs::sync::{File, OpenOptions, read_to_string, create_dir_all, remove_dir_all};
use std::io::{Read, Write, Seek, SeekFrom};

fn sync_file_operations() -> std::io::Result<()> {
    // Create directory
    create_dir_all("./data")?;

    // Create and write to file
    let mut file = OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(true)
        .open("./data/example.txt")?;

    file.write_all(b"Hello, World!")?;

    // Read file contents
    let contents = read_to_string("./data/example.txt")?;
    println!("File contents: {}", contents);

    // Append to file
    let mut file = OpenOptions::new()
        .append(true)
        .write(true)
        .open("./data/example.txt")?;

    file.write_all(b"\nAppended line")?;

    // Read with seeking
    let mut file = OpenOptions::new()
        .read(true)
        .open("./data/example.txt")?;

    file.seek(SeekFrom::Start(0))?;
    let mut buffer = String::new();
    file.read_to_string(&mut buffer)?;
    println!("Full contents: {}", buffer);

    // Clean up
    remove_dir_all("./data")?;

    Ok(())
}

Asynchronous File Operations

use switchy_fs::unsync::{File, OpenOptions, read_to_string, create_dir_all, remove_dir_all};
use switchy_async::io::{AsyncReadExt, AsyncWriteExt, AsyncSeekExt};
use std::io::SeekFrom;

async fn async_file_operations() -> std::io::Result<()> {
    // Create directory
    create_dir_all("./async_data").await?;

    // Create and write to file
    let mut file = OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(true)
        .open("./async_data/example.txt")
        .await?;

    file.write_all(b"Hello, Async World!").await?;

    // Read file contents
    let contents = read_to_string("./async_data/example.txt").await?;
    println!("File contents: {}", contents);

    // Append to file
    let mut file = OpenOptions::new()
        .append(true)
        .write(true)
        .open("./async_data/example.txt")
        .await?;

    file.write_all(b"\nAsync appended line").await?;

    // Read with seeking
    let mut file = OpenOptions::new()
        .read(true)
        .open("./async_data/example.txt")
        .await?;

    file.seek(SeekFrom::Start(0)).await?;
    let mut buffer = String::new();
    file.read_to_string(&mut buffer).await?;
    println!("Full contents: {}", buffer);

    // Clean up
    remove_dir_all("./async_data").await?;

    Ok(())
}

File Open Options

use switchy_fs::sync::OpenOptions;

// Create new file, fail if exists
let file = OpenOptions::new()
    .create(true)
    .write(true)
    .open("new_file.txt")?;

// Open existing file for reading
let file = OpenOptions::new()
    .read(true)
    .open("existing_file.txt")?;

// Open file for appending
let file = OpenOptions::new()
    .append(true)
    .write(true)
    .open("log_file.txt")?;

// Create or truncate file for writing
let file = OpenOptions::new()
    .create(true)
    .write(true)
    .truncate(true)
    .open("output_file.txt")?;

// Read and write access
let file = OpenOptions::new()
    .read(true)
    .write(true)
    .open("data_file.txt")?;

Directory Operations

use switchy_fs::sync::{create_dir_all, remove_dir_all, read_dir_sorted, walk_dir_sorted};
use switchy_fs::exists;

// Create nested directories
create_dir_all("./deep/nested/directory/structure")?;

// Check if path exists
if exists("./deep") {
    println!("Directory exists");
}

// Read directory entries (sorted by filename)
let entries = read_dir_sorted("./deep")?;
for entry in entries {
    println!("{:?}", entry.path());
}

// Recursively walk directory tree (sorted by path)
let all_entries = walk_dir_sorted("./deep")?;
for entry in all_entries {
    println!("{:?}", entry.path());
}

// Remove directory and all contents
remove_dir_all("./deep")?;

// Async versions
use switchy_fs::unsync::{create_dir_all, remove_dir_all};

create_dir_all("./async/nested/dirs").await?;

remove_dir_all("./async").await?;

Cross-module Compatibility

// Convert between sync and async options
use switchy_fs::{sync, unsync};

let async_options = unsync::OpenOptions::new()
    .create(true)
    .write(true);

// Convert to sync options
let sync_options: sync::OpenOptions = async_options.into();

// Or use explicit conversion
let sync_options = async_options.into_sync();

Simulator Mode (Testing)

#[cfg(test)]
mod tests {
    use switchy_fs::sync::{File, OpenOptions, read_to_string};
    use std::io::Write;

    #[test]
    fn test_file_operations() {
        // When simulator feature is enabled, all operations use mock filesystem
        let mut file = OpenOptions::new()
            .create(true)
            .write(true)
            .open("test_file.txt")
            .unwrap();

        file.write_all(b"test data").unwrap();

        let contents = read_to_string("test_file.txt").unwrap();
        assert_eq!(contents, "test data");
    }
}

Simulator Real Filesystem Access

With the simulator-real-fs feature, you can temporarily access the real filesystem within simulator mode:

#[cfg(all(feature = "simulator", feature = "simulator-real-fs"))]
use switchy_fs::{sync::read_to_string, with_real_fs};

// In simulator mode, but need to access real filesystem temporarily
let real_data = with_real_fs(|| {
    std::fs::read_to_string("/path/to/real/file.txt")
}).unwrap();

// Back to simulated filesystem
let sim_data = read_to_string("/simulated/file.txt")?;

Seeding Simulator from Real Filesystem

When loading fixtures into the simulator, use the seeding APIs:

  • seed_from_real_fs(real_path, sim_path): Recursively copy a real filesystem path into a simulator path
  • seed_from_real_fs_same_path(path): Seed using the same path for both real and simulator locations
  • seed_relative_to(base, relative_paths): Seed multiple paths relative to a base directory
  • is_simulator_enabled(): Check whether the crate was compiled with simulator support
use switchy_fs::{seed_from_real_fs, seed_relative_to};

// Seed one path from real filesystem into a simulator path
seed_from_real_fs("./tests/fixtures", "/fixtures")?;

// Seed multiple paths relative to a project root
seed_relative_to(
    env!("CARGO_MANIFEST_DIR"),
    ["tests/fixtures", "tests/scripts"],
)?;

Temporary Directories

The package provides temporary directory support that automatically cleans up when dropped:

use switchy_fs::{TempDir, tempdir, tempdir_in};

// Create a temporary directory
let temp = tempdir()?;
let path = temp.path();

// Use the directory
std::fs::write(path.join("test.txt"), b"data")?;

// Directory is automatically cleaned up when temp is dropped

// Create temp directory with prefix
let temp = TempDir::with_prefix("my-prefix-")?;

// Create temp directory with suffix
let temp = TempDir::with_suffix("-my-suffix")?;

// Create temp directory in specific location
let temp = tempdir_in("/custom/temp/location")?;

// Persist the directory (prevent automatic cleanup)
let kept_path = temp.keep();

Feature Flags

Operation Modes

  • sync: Enable synchronous filesystem operations
  • async: Enable asynchronous filesystem operations

Backend Implementations

  • std: Use standard library filesystem implementation
  • tokio: Use Tokio async filesystem implementation
  • simulator: Use mock filesystem for testing
  • simulator-real-fs: Enable real filesystem access within simulator mode using with_real_fs

Other Features

  • fail-on-warnings: Treat warnings as errors during compilation

Default features: async, simulator, std, sync, tokio

Backend Selection

The package automatically selects the appropriate backend based on enabled features:

  1. Simulator Mode: When simulator feature is enabled, all operations use mock filesystem
  2. Standard Library: When std feature is enabled (and not simulator), uses std::fs
  3. Tokio: When tokio feature is enabled (and not simulator), uses tokio::fs

Error Handling

use switchy_fs::sync::{File, OpenOptions};
use std::io::ErrorKind;

match OpenOptions::new().read(true).open("nonexistent.txt") {
    Ok(file) => {
        // File opened successfully
    }
    Err(e) => match e.kind() {
        ErrorKind::NotFound => {
            println!("File not found");
        }
        ErrorKind::PermissionDenied => {
            println!("Permission denied");
        }
        _ => {
            println!("Other error: {}", e);
        }
    }
}

Dependencies

  • log: Logging facade for diagnostic messages
  • switchy_async: Async I/O trait abstractions (optional)
  • bytes: Byte buffer utilities for simulator backend (optional)
  • scoped-tls: Thread-local storage for simulator real-fs mode (optional)
  • tempfile: Temporary file management for standard library backend (optional)
  • tokio: Async filesystem operations (optional)

Use Cases

  • Configuration Files: Read and write application configuration
  • Data Persistence: Store and retrieve application data
  • Log Files: Append-only log file operations
  • Temporary Directories: Create and manage temporary directories with automatic cleanup
  • Testing: Mock filesystem operations in unit tests with simulator mode
  • Cross-platform Applications: Unified filesystem interface across different backends
  • Async Applications: Non-blocking filesystem operations with Tokio
  • Deterministic Testing: Sorted directory reading and walking for reproducible test results