Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cross-Platform Virtual Drive Mounting

Mounting AnyFS backends as real filesystem mount points


Overview

AnyFS backends implementing FsFuse can be mounted as real filesystem drives that any application can access. This is part of the anyfs crate (behind feature flags: fuse for Linux/macOS, winfsp for Windows) because mounting is a core promise of AnyFS, not an optional extra.


Product Promise

Mounting is a core AnyFS promise: make filesystem composition easy, safe, and genuinely enjoyable for programmers. The mount API prioritizes:

  • Easy onboarding (one handle, one builder, minimal boilerplate)
  • Safe defaults (explicit read-only modes, clear errors, no hidden behavior)
  • Delightful DX (predictable behavior, fast feedback, good docs)

Roadmap (MVP to Cross-Platform)

Phase 0: Design and API shape (complete)

  • API spec defines MountHandle, MountBuilder, MountOptions, MountError
  • Platform detection hooks (is_available) and consistent error mapping
  • Examples and docs anchored in this guide Acceptance: Spec review complete; API signatures consistent across docs; error mapping defined.

Phase 1: Linux FUSE MVP (read-only, pending)

  • fuser adapter for lookup/getattr/readdir/read
  • Read-only mount option; write ops return PermissionDenied Acceptance: Mount/unmount works on Linux; read-only operations pass smoke tests; unmount-on-drop is reliable.

Phase 2: Linux FUSE read/write (pending)

  • Full write path: create, write, rename, remove, link operations
  • Capability reporting and correct metadata mapping Acceptance: Conformance tests pass for FsFuse path/inode behavior; no panics; clean shutdown.

Phase 3: macOS parity (macFUSE, pending)

  • Port Linux FUSE adapter to macFUSE requirements
  • Driver detection and install guidance Acceptance: Mount/unmount works on macOS with core read/write flows.

Phase 4: Windows support (WinFsp, optional Dokan, pending)

  • WinFsp adapter with required mapping for Windows semantics
  • Optional Dokan path as alternative provider Acceptance: Mount/unmount works on Windows; driver detection errors are clear and actionable.

Non-goals

  • Kernel drivers or kernel-space code
  • WASM or browser environments
  • Network filesystem protocols (NFS/SMB)

Platform Technologies

PlatformTechnologyRust CrateUser Installation
LinuxFUSEfuserUsually pre-installed
macOSmacFUSEfusermacFUSE
WindowsWinFspwinfspWinFsp
WindowsDokandokanDokan

Key insight: Linux and macOS both use FUSE (via fuser crate), but Windows requires a completely different API (WinFsp or Dokan).


Architecture

Unified Mount Trait

#![allow(unused)]
fn main() {
/// Platform-agnostic mount handle.
/// Drop to unmount.
pub struct MountHandle {
    inner: Box<dyn MountHandleInner>,
}

impl MountHandle {
    /// Mount a backend at the specified path.
    ///
    /// Platform requirements:
    /// - Linux: FUSE (usually available)
    /// - macOS: macFUSE must be installed
    /// - Windows: WinFsp or Dokan must be installed
    pub fn mount<B: FsFuse>(backend: B, path: impl AsRef<Path>) -> Result<Self, MountError> {
        #[cfg(unix)]
        return fuse_mount(backend, path);

        #[cfg(windows)]
        return winfsp_mount(backend, path);

        #[cfg(not(any(unix, windows)))]
        return Err(MountError::PlatformNotSupported);
    }

    /// Check if mounting is available on this platform.
    pub fn is_available() -> bool {
        #[cfg(target_os = "linux")]
        return check_fuse_available();

        #[cfg(target_os = "macos")]
        return check_macfuse_available();

        #[cfg(windows)]
        return check_winfsp_available() || check_dokan_available();

        #[cfg(not(any(unix, windows)))]
        return false;
    }

    /// Unmount the filesystem.
    pub fn unmount(self) -> Result<(), MountError> {
        self.inner.unmount()
    }
}

impl Drop for MountHandle {
    fn drop(&mut self) {
        let _ = self.inner.unmount();
    }
}
}

Platform Adapters

┌─────────────────────────────────────────────────────────────┐
│                     MountHandle (unified API)               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐ │
│  │ FuseAdapter │  │ FuseAdapter │  │ WinFspAdapter       │ │
│  │   (Linux)   │  │   (macOS)   │  │    (Windows)        │ │
│  └──────┬──────┘  └──────┬──────┘  └──────────┬──────────┘ │
│         │                │                     │            │
│         ▼                ▼                     ▼            │
│    ┌─────────┐      ┌─────────┐         ┌──────────┐       │
│    │  fuser  │      │  fuser  │         │  winfsp  │       │
│    │  crate  │      │  crate  │         │  crate   │       │
│    └────┬────┘      └────┬────┘         └────┬─────┘       │
│         │                │                   │              │
│         ▼                ▼                   ▼              │
│    ┌─────────┐      ┌─────────┐         ┌──────────┐       │
│    │  FUSE   │      │ macFUSE │         │  WinFsp  │       │
│    │ (kernel)│      │ (kext)  │         │ (driver) │       │
│    └─────────┘      └─────────┘         └──────────┘       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Module Structure

Mounting is part of the anyfs crate:

anyfs/
  src/
    mount/
      mod.rs                    # MountHandle, MountError, re-exports
      error.rs                  # MountError definitions
      handle.rs                 # MountHandle, MountOptions, builder

      unix/
        mod.rs                  # cfg(unix)
        fuse_adapter.rs         # FUSE implementation via fuser

      windows/
        mod.rs                  # cfg(windows)
        winfsp_adapter.rs       # WinFsp implementation

Feature Flags in anyfs Cargo.toml

[package]
name = "anyfs"
version = "0.1.0"

[dependencies]
anyfs-backend = { version = "0.1" }

[target.'cfg(unix)'.dependencies]
fuser = { version = "0.14", optional = true }

[target.'cfg(windows)'.dependencies]
winfsp = { version = "0.4", optional = true }

[features]
default = []
fuse = ["dep:fuser"]      # Enable mounting on Linux/macOS
winfsp = ["dep:winfsp"]   # Enable mounting on Windows

FUSE Adapter (Linux/macOS)

The FUSE adapter translates between fuser::Filesystem trait and our FsFuse trait:

#![allow(unused)]
fn main() {
use fuser::{Filesystem, Request, ReplyEntry, ReplyAttr, ReplyData, ReplyDirectory};
use anyfs_backend::{FsFuse, FsError, Metadata, FileType};

pub struct FuseAdapter<B: FsFuse> {
    backend: B,
}

impl<B: FsFuse> Filesystem for FuseAdapter<B> {
    fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
        match self.backend.lookup(parent, name) {
            Ok(inode) => {
                match self.backend.metadata_by_inode(inode) {
                    Ok(meta) => reply.entry(&TTL, &to_fuse_attr(&meta), 0),
                    Err(e) => reply.error(to_errno(&e)),
                }
            }
            Err(e) => reply.error(to_errno(&e)),
        }
    }

    fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) {
        match self.backend.metadata_by_inode(ino) {
            Ok(meta) => reply.attr(&TTL, &to_fuse_attr(&meta)),
            Err(e) => reply.error(to_errno(&e)),
        }
    }

    fn read(&mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, size: u32, _flags: i32, _lock: Option<u64>, reply: ReplyData) {
        let path = match self.backend.inode_to_path(ino) {
            Ok(p) => p,
            Err(e) => return reply.error(to_errno(&e)),
        };

        match self.backend.read_range(&path, offset as u64, size as usize) {
            Ok(data) => reply.data(&data),
            Err(e) => reply.error(to_errno(&e)),
        }
    }

    fn readdir(&mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, mut reply: ReplyDirectory) {
        let path = match self.backend.inode_to_path(ino) {
            Ok(p) => p,
            Err(e) => return reply.error(to_errno(&e)),
        };

        match self.backend.read_dir(&path) {
            Ok(entries) => {
                for (i, entry) in entries.iter().enumerate().skip(offset as usize) {
                    let file_type = match entry.file_type {
                        FileType::File => fuser::FileType::RegularFile,
                        FileType::Directory => fuser::FileType::Directory,
                        FileType::Symlink => fuser::FileType::Symlink,
                    };

                    if reply.add(entry.inode, (i + 1) as i64, file_type, &entry.name) {
                        break;
                    }
                }
                reply.ok();
            }
            Err(e) => reply.error(to_errno(&e)),
        }
    }

    // ... write, create, mkdir, unlink, rmdir, rename, symlink, etc.
}

fn to_errno(e: &FsError) -> i32 {
    match e {
        FsError::NotFound { .. } => libc::ENOENT,
        FsError::AlreadyExists { .. } => libc::EEXIST,
        FsError::NotADirectory { .. } => libc::ENOTDIR,
        FsError::NotAFile { .. } => libc::EISDIR,
        FsError::DirectoryNotEmpty { .. } => libc::ENOTEMPTY,
        FsError::AccessDenied { .. } => libc::EACCES,
        FsError::ReadOnly { .. } => libc::EROFS,
        FsError::QuotaExceeded { .. } => libc::ENOSPC,
        _ => libc::EIO,
    }
}
}

WinFsp Adapter (Windows)

WinFsp has a different API but similar concepts:

#![allow(unused)]
fn main() {
use winfsp::filesystem::{FileSystem, FileSystemContext, FileInfo, DirInfo};
use anyfs_backend::{FsFuse, FsError};

pub struct WinFspAdapter<B: FsFuse> {
    backend: B,
}

impl<B: FsFuse> FileSystem for WinFspAdapter<B> {
    fn get_file_info(&self, file_context: &FileContext) -> Result<FileInfo, NTSTATUS> {
        let meta = self.backend.metadata(&file_context.path)
            .map_err(to_ntstatus)?;
        Ok(to_file_info(&meta))
    }

    fn read(&self, file_context: &FileContext, buffer: &mut [u8], offset: u64) -> Result<usize, NTSTATUS> {
        let data = self.backend.read_range(&file_context.path, offset, buffer.len())
            .map_err(to_ntstatus)?;
        buffer[..data.len()].copy_from_slice(&data);
        Ok(data.len())
    }

    fn read_directory(&self, file_context: &FileContext, marker: Option<&str>, callback: impl FnMut(DirInfo)) -> Result<(), NTSTATUS> {
        let entries = self.backend.read_dir(&file_context.path)
            .map_err(to_ntstatus)?;

        for entry in entries {
            // ReadDirIter yields Result<DirEntry, FsError>
            let entry = entry.map_err(to_ntstatus)?;
            callback(to_dir_info(&entry));
        }
        Ok(())
    }

    // ... write, create, delete, rename, etc.
}

fn to_ntstatus(e: FsError) -> NTSTATUS {
    match e {
        FsError::NotFound { .. } => STATUS_OBJECT_NAME_NOT_FOUND,
        FsError::AlreadyExists { .. } => STATUS_OBJECT_NAME_COLLISION,
        FsError::AccessDenied { .. } => STATUS_ACCESS_DENIED,
        FsError::ReadOnly { .. } => STATUS_MEDIA_WRITE_PROTECTED,
        _ => STATUS_INTERNAL_ERROR,
    }
}
}

Usage

Basic Mount

#![allow(unused)]
fn main() {
use anyfs::{MemoryBackend, QuotaLayer, MountHandle};

// Create backend with middleware
let backend = MemoryBackend::new()
    .layer(QuotaLayer::builder()
        .max_total_size(100 * 1024 * 1024)
        .build());

// Mount as drive
let mount = MountHandle::mount(backend, "/mnt/ramdisk")?;

// Now /mnt/ramdisk is a real mount point
// Any application can read/write files there

// Unmount when done (or on drop)
mount.unmount()?;
}

Windows Drive Letter

#![allow(unused)]
fn main() {
#[cfg(windows)]
let mount = MountHandle::mount(backend, "X:")?;

// Now X: is a virtual drive
}

Check Availability

#![allow(unused)]
fn main() {
if MountHandle::is_available() {
    let mount = MountHandle::mount(backend, path)?;
} else {
    eprintln!("Mounting not available. Install:");
    #[cfg(target_os = "macos")]
    eprintln!("  - macFUSE: https://osxfuse.github.io/");
    #[cfg(windows)]
    eprintln!("  - WinFsp: https://winfsp.dev/");
}
}

Mount Options

#![allow(unused)]
fn main() {
let mount = MountHandle::builder(backend)
    .mount_point("/mnt/data")
    .read_only(true)                    // Force read-only mount
    .allow_other(true)                  // Allow other users (Linux/macOS)
    .auto_unmount(true)                 // Unmount on process exit
    .uid(1000)                          // Override UID (Linux/macOS)
    .gid(1000)                          // Override GID (Linux/macOS)
    .mount()?;
}

Error Handling

#![allow(unused)]
fn main() {
pub enum MountError {
    /// Platform doesn't support mounting (e.g., WASM)
    PlatformNotSupported,

    /// Required driver not installed (macFUSE, WinFsp)
    DriverNotInstalled {
        driver: &'static str,
        install_url: &'static str,
    },

    /// Mount point doesn't exist or isn't accessible
    InvalidMountPoint { path: PathBuf },

    /// Mount point already in use
    MountPointBusy { path: PathBuf },

    /// Permission denied (need root/admin)
    PermissionDenied,

    /// Backend error during mount
    Backend(FsError),

    /// Platform-specific error
    Platform(String),

    /// Missing mount point in options
    MissingMountPoint,
}
}

Integration with Middleware

All middleware works transparently when mounted:

#![allow(unused)]
fn main() {
use anyfs::{MemoryBackend, QuotaLayer, PathFilterLayer, TracingLayer, RateLimitLayer, MountHandle};

// Build secure, audited, rate-limited mount
let backend = MemoryBackend::new()
    .layer(QuotaLayer::builder()
        .max_total_size(1024 * 1024 * 1024)  // 1 GB
        .build())
    .layer(PathFilterLayer::builder()
        .deny("**/.git/**")
        .deny("**/.env")
        .build())
    .layer(RateLimitLayer::builder()
        .max_ops(10000)
        .per_second()
        .build())
    .layer(TracingLayer::new());

let mount = MountHandle::mount(backend, "/mnt/secure")?;

// External apps see a normal filesystem
// But all operations are:
// - Quota-limited
// - Path-filtered
// - Rate-limited
// - Traced/audited

// Imagine: A mounted "USB drive" that reports real-time IOPS
// to a Prometheus dashboard!
}

Real-Time Observability

Because the mount point sits on top of your middleware stack, you get live visibility into OS operations:

  • Metrics: See valid IOPS, throughput, and latency for your virtual drive in Grafana.
  • Audit Logs: Record every file your legacy app touches.
  • Virus Scanning: Scan files as the OS writes them, rejecting malware in real-time.

---

## Use Cases

### Temporary Workspace

```rust
let workspace = MemoryBackend::new();
let mount = MountHandle::mount(workspace, "/tmp/workspace")?;

// Run build tools that expect real filesystem
std::process::Command::new("cargo")
    .current_dir("/tmp/workspace")
    .arg("build")
    .status()?;

Portable Database as Drive

#![allow(unused)]
fn main() {
use anyfs_sqlite::SqliteBackend;  // Ecosystem crate

// User's files stored in SQLite
let db = SqliteBackend::open("user_files.db")?;
let mount = MountHandle::mount(db, "U:")?;

// User can browse U: in Explorer
// Files are actually in SQLite database
}

Network Storage

#![allow(unused)]
fn main() {
// Remote backend (future anyfs-s3, anyfs-sftp, etc.)
let remote = S3Backend::new("my-bucket")?;
let cached = remote.layer(CacheLayer::builder()
    .max_size(100 * 1024 * 1024)
    .build());
let mount = MountHandle::mount(cached, "/mnt/cloud")?;

// Local apps see /mnt/cloud as regular filesystem
// Actually reads/writes to S3 with local caching
}

Platform Requirements Summary

PlatformDriverInstall Command / URL
LinuxFUSEUsually pre-installed. If not: apt install fuse3
macOSmacFUSEhttps://osxfuse.github.io/
WindowsWinFsphttps://winfsp.dev/ (recommended)
WindowsDokanhttps://dokan-dev.github.io/ (alternative)

Limitations

  1. Requires external driver - Users must install macFUSE (macOS) or WinFsp (Windows)
  2. Root/admin may be required - Some mount operations need elevated privileges
  3. Not available on WASM - Browser environment has no filesystem mounting
  4. Performance overhead - Userspace filesystem has kernel boundary crossing overhead
  5. Backend must implement FsFuse - Requires FsInode trait for inode operations

Alternative: No Mount Needed

For many use cases, mounting isn’t necessary. AnyFS backends can be used directly:

NeedWith MountingWithout Mounting
Build toolsMount, run toolsUse tool’s VFS plugin if available
File browserMount as driveBuild custom UI with AnyFS API
BackupMount, use rsyncUse AnyFS API directly
DatabaseMount for SQL toolsQuery SQLite directly

Rule of thumb: Only mount when you need compatibility with external applications that expect real filesystem paths.