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

AnyFS Backend Guide

anyfs-backend is the foundational crate for the AnyFS ecosystem. It provides a trait-based abstraction for filesystem operations, allowing you to:

  • Write code that works with any filesystem backend
  • Create middleware layers that add cross-cutting functionality
  • Build FUSE filesystems with a clean, type-safe API

Who Is This Guide For?

  • Backend implementers: You want to create a new filesystem backend (S3, Google Drive, in-memory, etc.)
  • Middleware authors: You want to add logging, caching, encryption, or other features as composable layers
  • Library users: You want to understand how to use anyfs effectively

What You’ll Learn

Implementing a Backend

Step-by-step guide to implementing a complete filesystem backend, from basic file operations to full POSIX support.

Implementing Middleware

How to create reusable middleware layers using the Tower-inspired Layer pattern.

Quick Example

use anyfs_backend::{Fs, FsError};
use std::path::Path;

// Write a generic function that works with ANY backend
fn copy_file<B: Fs>(fs: &B, src: &Path, dst: &Path) -> Result<(), FsError> {
    let content = fs.read(src)?;
    fs.write(dst, &content)?;
    Ok(())
}

// Use with any backend that implements Fs
fn main() {
    let fs = my_backend::MyFs::new();
    copy_file(&fs, Path::new("/src.txt"), Path::new("/dst.txt")).unwrap();
}

Design Principles

  1. Trait-based: All operations are defined as traits, enabling generic code
  2. Layered: Traits are organized in layers (Fs → FsFull → FsFuse → FsPosix)
  3. Composable: Middleware layers can be stacked to add functionality
  4. Thread-safe: All traits require Send + Sync for concurrent access
  5. Error-rich: Detailed error types with full context

Quick Start

Add anyfs-backend to your Cargo.toml:

[dependencies]
anyfs-backend = "0.1"

Using a Filesystem Backend

All backends implement the Fs trait (or higher-level traits). Write generic code against these traits:

#![allow(unused)]
fn main() {
use anyfs_backend::{Fs, FsError};
use std::path::Path;

fn list_files<B: Fs>(fs: &B, dir: &Path) -> Result<Vec<String>, FsError> {
    let mut names = Vec::new();
    for entry in fs.read_dir(dir)? {
        let entry = entry?;
        names.push(entry.name);
    }
    Ok(names)
}
}

Creating a Simple Backend

Here’s a minimal in-memory filesystem:

#![allow(unused)]
fn main() {
use anyfs_backend::{FsRead, FsWrite, FsDir, FsError, Metadata, DirEntry, FileType, Permissions, ReadDirIter};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::RwLock;

pub struct SimpleFs {
    files: RwLock<HashMap<PathBuf, Vec<u8>>>,
}

impl SimpleFs {
    pub fn new() -> Self {
        Self { files: RwLock::new(HashMap::new()) }
    }
}

impl FsRead for SimpleFs {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        self.files.read().unwrap()
            .get(path)
            .cloned()
            .ok_or_else(|| FsError::NotFound { path: path.to_path_buf() })
    }

    fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
        let files = self.files.read().unwrap();
        let content = files.get(path)
            .ok_or_else(|| FsError::NotFound { path: path.to_path_buf() })?;
        
        Ok(Metadata {
            path: path.to_path_buf(),
            file_type: FileType::File,
            len: content.len() as u64,
            permissions: Permissions::default(),
            ..Default::default()
        })
    }

    fn exists(&self, path: &Path) -> bool {
        self.files.read().unwrap().contains_key(path)
    }
}

impl FsWrite for SimpleFs {
    fn write(&self, path: &Path, content: &[u8]) -> Result<(), FsError> {
        self.files.write().unwrap().insert(path.to_path_buf(), content.to_vec());
        Ok(())
    }

    fn remove_file(&self, path: &Path) -> Result<(), FsError> {
        self.files.write().unwrap()
            .remove(path)
            .map(|_| ())
            .ok_or_else(|| FsError::NotFound { path: path.to_path_buf() })
    }
}

// FsDir implementation would go here...
}

Using Middleware Layers

Wrap any backend with middleware:

#![allow(unused)]
fn main() {
use anyfs_backend::{Fs, Layer};

// Assuming LoggingLayer is a middleware that logs operations
let fs = SimpleFs::new();
let fs = LoggingLayer::new("MyApp").layer(fs);

// Now all operations are logged
fs.write(Path::new("/hello.txt"), b"Hello!").unwrap();
}

Next Steps

Trait Hierarchy

The anyfs-backend crate organizes filesystem operations into a layered hierarchy of traits. Each layer adds more capabilities.

The Four Layers

┌─────────────────────────────────────────────────────────────┐
│  Layer 4: FsPosix                                           │
│  Full POSIX semantics with handles and locks                │
│  = FsFuse + FsHandles + FsLock + FsXattr                    │
├─────────────────────────────────────────────────────────────┤
│  Layer 3: FsFuse                                            │
│  FUSE-compatible with inode operations                      │
│  = FsFull + FsInode                                         │
├─────────────────────────────────────────────────────────────┤
│  Layer 2: FsFull                                            │
│  Complete filesystem with links, permissions, stats         │
│  = Fs + FsLink + FsPermissions + FsSync + FsStats           │
├─────────────────────────────────────────────────────────────┤
│  Layer 1: Fs                                                │
│  Basic file operations                                      │
│  = FsRead + FsWrite + FsDir                                 │
└─────────────────────────────────────────────────────────────┘

Layer 1: Fs (Basic Operations)

The minimum for a functional filesystem.

TraitPurpose
FsReadRead files, get metadata, check existence
FsWriteWrite files, delete files
FsDirList, create, remove directories; rename

Use Fs when: You need basic file I/O and don’t care about permissions, symlinks, or advanced features.

#![allow(unused)]
fn main() {
fn backup<B: Fs>(fs: &B, src: &Path, dst: &Path) -> Result<(), FsError> {
    let data = fs.read(src)?;
    fs.write(dst, &data)
}
}

Layer 2: FsFull (Complete Filesystem)

Adds features most real filesystems need.

TraitPurpose
FsLinkSymbolic and hard links
FsPermissionsSet permissions and ownership
FsSyncFlush writes to storage
FsStatsFilesystem statistics (space usage)

Use FsFull when: You need symlinks, permissions, or disk usage stats.

#![allow(unused)]
fn main() {
fn create_symlink<B: FsFull>(fs: &B, target: &Path, link: &Path) -> Result<(), FsError> {
    fs.symlink(target, link)
}
}

Layer 3: FsFuse (FUSE Support)

Adds inode-based operations for FUSE implementations.

TraitPurpose
FsInodePath↔inode conversion, lookup by inode

Use FsFuse when: Building a FUSE filesystem that operates on inodes.

#![allow(unused)]
fn main() {
fn get_child<B: FsFuse>(fs: &B, parent_inode: u64, name: &str) -> Result<u64, FsError> {
    fs.lookup(parent_inode, name)
}
}

Layer 4: FsPosix (Full POSIX)

Complete POSIX semantics.

TraitPurpose
FsHandlesOpen files, read/write at offset
FsLockFile locking (shared/exclusive)
FsXattrExtended attributes

Use FsPosix when: You need file handles, locking, or extended attributes.

#![allow(unused)]
fn main() {
fn locked_write<B: FsPosix>(fs: &B, path: &Path, data: &[u8]) -> Result<(), FsError> {
    let handle = fs.open(path, OpenFlags::WRITE | OpenFlags::CREATE)?;
    fs.lock(handle, LockType::Exclusive)?;
    fs.write_at(handle, 0, data)?;
    fs.unlock(handle)?;
    fs.close(handle)
}
}

Choosing the Right Trait Bound

Your needsUse this bound
Basic read/write/listFs
+ symlinks, permissions, statsFsFull
+ inode operations (FUSE)FsFuse
+ file handles, lockingFsPosix

Automatic Implementation

The composite traits (Fs, FsFull, FsFuse, FsPosix) are automatically implemented via blanket impls. You only implement the component traits:

#![allow(unused)]
fn main() {
// You implement these:
impl FsRead for MyBackend { ... }
impl FsWrite for MyBackend { ... }
impl FsDir for MyBackend { ... }

// This is automatic:
// impl Fs for MyBackend {}  // ← Provided by blanket impl
}

Thread Safety

All traits require Send + Sync. This means:

  • Methods take &self, not &mut self
  • Use interior mutability (RwLock, Mutex) for mutable state
  • Safe for concurrent access from multiple threads
#![allow(unused)]
fn main() {
pub struct MyBackend {
    // Use RwLock for mutable state
    files: RwLock<HashMap<PathBuf, Vec<u8>>>,
}

impl FsRead for MyBackend {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        // Acquire read lock
        self.files.read().unwrap().get(path).cloned()
            .ok_or_else(|| FsError::NotFound { path: path.to_path_buf() })
    }
}
}

Implementing a Backend

This tutorial walks you through implementing a complete filesystem backend from scratch. By the end, you’ll have a working in-memory filesystem that implements all traits up to FsPosix.

What You’ll Build

An in-memory filesystem (TutorialFs) that:

  • Stores files and directories in memory
  • Supports symlinks
  • Tracks permissions and timestamps
  • Provides inode-based access for FUSE
  • Implements file handles and locking

Prerequisites

  • Basic Rust knowledge (structs, traits, Result)
  • Understanding of filesystem concepts (files, directories, symlinks)

Tutorial Structure

Each chapter introduces one or more traits:

  1. Core Data Structures - Design the internal state
  2. FsRead - Read files and metadata
  3. FsWrite - Write and delete files
  4. FsDir - Directory operations
  5. The Fs Trait - Combining the basics
  6. FsLink - Symlink support
  7. FsFull - Permissions, sync, stats
  8. FsInode - FUSE inode operations
  9. FsPosix - Handles and locking

Running the Examples

Each chapter has runnable example code. Clone the repository and run:

cargo run --example tutorial_backend_complete

The End Result

After completing this tutorial, you’ll understand:

  • How each trait fits into the hierarchy
  • What each method should do and return
  • Error handling conventions
  • Thread-safety requirements
  • How blanket implementations work

Let’s start with Core Data Structures →

Core Data Structures

Before implementing any traits, we need to design our internal data structures. These hold the actual filesystem state.

Key Design Decisions

1. Thread Safety

All trait methods take &self (not &mut self), so we must use interior mutability:

#![allow(unused)]
fn main() {
// ❌ Won't work - traits don't allow &mut self
impl FsWrite for MyFs {
    fn write(&mut self, path: &Path, content: &[u8]) -> Result<(), FsError> { ... }
}

// ✅ Correct - use interior mutability
impl FsWrite for MyFs {
    fn write(&self, path: &Path, content: &[u8]) -> Result<(), FsError> {
        let mut files = self.files.write().unwrap();  // RwLock
        files.insert(path.to_path_buf(), content.to_vec());
        Ok(())
    }
}
}

We wrap mutable state in RwLock (or Mutex).

2. Path Normalization

Paths like /foo/bar, /foo//bar, and /foo/./bar should all refer to the same file. Always normalize before using as keys:

#![allow(unused)]
fn main() {
fn normalize_path(path: &Path) -> PathBuf {
    let mut result = PathBuf::from("/");
    for component in path.components() {
        match component {
            Component::RootDir => result = PathBuf::from("/"),
            Component::CurDir => {}  // Skip "."
            Component::ParentDir => { result.pop(); }  // Handle ".."
            Component::Normal(name) => { result.push(name); }
            Component::Prefix(_) => {}  // Windows, skip
        }
    }
    result
}
}

3. Inode Design

Even if you only need Fs, designing with inodes from the start makes it easier to add FsFuse later:

#![allow(unused)]
fn main() {
struct FsNode {
    inode: u64,  // Unique identifier
    // ... other fields
}
}

The FsNode Structure

Each node in our filesystem (file, directory, or symlink) has:

#![allow(unused)]
fn main() {
use anyfs_backend::{FileType, Permissions};
use std::path::PathBuf;
use std::time::SystemTime;

/// Represents a single node in the filesystem.
#[derive(Clone)]
struct FsNode {
    /// Type: File, Directory, or Symlink
    file_type: FileType,

    /// File contents (empty for directories)
    content: Vec<u8>,

    /// Permission bits (e.g., 0o644)
    permissions: Permissions,

    /// Symlink target (only for symlinks)
    symlink_target: Option<PathBuf>,

    /// Unique inode number
    inode: u64,

    /// Timestamps
    created: SystemTime,
    modified: SystemTime,
    accessed: SystemTime,
}
}

Factory Methods

Create nodes easily:

#![allow(unused)]
fn main() {
impl FsNode {
    fn new_file(content: Vec<u8>, inode: u64) -> Self {
        let now = SystemTime::now();
        Self {
            file_type: FileType::File,
            content,
            permissions: Permissions::from_mode(0o644),  // rw-r--r--
            symlink_target: None,
            inode,
            created: now,
            modified: now,
            accessed: now,
        }
    }

    fn new_directory(inode: u64) -> Self {
        let now = SystemTime::now();
        Self {
            file_type: FileType::Directory,
            content: Vec::new(),
            permissions: Permissions::from_mode(0o755),  // rwxr-xr-x
            symlink_target: None,
            inode,
            created: now,
            modified: now,
            accessed: now,
        }
    }

    fn new_symlink(target: PathBuf, inode: u64) -> Self {
        let now = SystemTime::now();
        Self {
            file_type: FileType::Symlink,
            content: Vec::new(),
            permissions: Permissions::from_mode(0o777),  // lrwxrwxrwx
            symlink_target: Some(target),
            inode,
            created: now,
            modified: now,
            accessed: now,
        }
    }
}
}

The Inner State

All mutable state goes in a struct wrapped by RwLock:

#![allow(unused)]
fn main() {
use std::collections::HashMap;
use std::path::PathBuf;
use anyfs_backend::{Handle, OpenFlags, LockType};

struct TutorialFsInner {
    /// Maps normalized paths to nodes
    nodes: HashMap<PathBuf, FsNode>,

    /// Maps inodes to paths (for inode-based lookups)
    inode_to_path: HashMap<u64, PathBuf>,

    /// Next available inode number
    next_inode: u64,

    /// Open file handles (for FsHandles)
    handles: HashMap<Handle, HandleState>,

    /// Next available handle ID
    next_handle: u64,

    /// Total filesystem size for stats
    total_size: u64,
}

/// State for an open file handle.
struct HandleState {
    path: PathBuf,
    flags: OpenFlags,
    locked: Option<LockType>,
}
}

The Public Backend Type

The main struct wraps everything in Arc<RwLock<_>>:

#![allow(unused)]
fn main() {
use std::sync::{Arc, RwLock};

/// Our tutorial filesystem backend.
pub struct TutorialFs {
    inner: Arc<RwLock<TutorialFsInner>>,
}

impl TutorialFs {
    pub fn new() -> Self {
        let mut nodes = HashMap::new();
        let mut inode_to_path = HashMap::new();

        // Always create root directory with inode 1 (ROOT_INODE)
        use anyfs_backend::ROOT_INODE;
        let root = FsNode::new_directory(ROOT_INODE);
        nodes.insert(PathBuf::from("/"), root);
        inode_to_path.insert(ROOT_INODE, PathBuf::from("/"));

        Self {
            inner: Arc::new(RwLock::new(TutorialFsInner {
                nodes,
                inode_to_path,
                next_inode: 2,  // Start after ROOT_INODE
                next_handle: 1,
                handles: HashMap::new(),
                total_size: 100 * 1024 * 1024,  // 100 MB virtual size
            })),
        }
    }
}
}

Helper Methods

Add utility methods for common operations:

#![allow(unused)]
fn main() {
impl TutorialFs {
    /// Normalize a path for consistent storage and lookup.
    fn normalize_path(path: &Path) -> PathBuf {
        // Implementation from above
    }

    /// Allocate a new inode number.
    fn alloc_inode(inner: &mut TutorialFsInner) -> u64 {
        let inode = inner.next_inode;
        inner.next_inode += 1;
        inode
    }

    /// Allocate a new file handle.
    fn alloc_handle(inner: &mut TutorialFsInner) -> Handle {
        let id = inner.next_handle;
        inner.next_handle += 1;
        Handle(id)
    }
}
}

Converting to Metadata

The traits return Metadata structs. Add a conversion method:

#![allow(unused)]
fn main() {
use anyfs_backend::Metadata;

impl FsNode {
    fn to_metadata(&self, path: &Path) -> Metadata {
        Metadata {
            path: path.to_path_buf(),
            file_type: self.file_type,
            len: self.content.len() as u64,
            permissions: self.permissions.clone(),
            created: Some(self.created),
            modified: Some(self.modified),
            accessed: Some(self.accessed),
            inode: Some(self.inode),
            uid: Some(1000),
            gid: Some(1000),
            nlink: Some(1),
        }
    }
}
}

Summary

We now have:

TypePurpose
FsNodeRepresents a file, directory, or symlink
TutorialFsInnerAll mutable state
TutorialFsPublic backend with Arc<RwLock<Inner>>

Next, we’ll implement FsRead →

FsRead: Reading Files

FsRead provides read-only access to files and metadata. This is the foundation for all filesystem access.

The Trait

#![allow(unused)]
fn main() {
pub trait FsRead: Send + Sync {
    /// Read the entire contents of a file.
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError>;

    /// Get metadata (size, type, timestamps, etc.).
    fn metadata(&self, path: &Path) -> Result<Metadata, FsError>;

    /// Check if a path exists.
    fn exists(&self, path: &Path) -> bool;
}
}

Implementation

read - Read File Contents

#![allow(unused)]
fn main() {
use anyfs_backend::{FsRead, FsError, FileType};

impl FsRead for TutorialFs {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        let path = Self::normalize_path(path);
        let inner = self.inner.read().unwrap();

        // Look up the node
        let node = inner.nodes.get(&path)
            .ok_or_else(|| FsError::NotFound { path: path.clone() })?;

        // Directories can't be read as files
        if node.file_type == FileType::Directory {
            return Err(FsError::IsADirectory { path });
        }

        Ok(node.content.clone())
    }
    // ...
}
}

Key points:

  • Normalize the path first for consistent lookup
  • Return FsError::NotFound if the path doesn’t exist
  • Return FsError::IsADirectory if trying to read a directory

metadata - Get File Information

#![allow(unused)]
fn main() {
    fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
        let path = Self::normalize_path(path);
        let inner = self.inner.read().unwrap();

        let node = inner.nodes.get(&path)
            .ok_or_else(|| FsError::NotFound { path: path.clone() })?;

        Ok(node.to_metadata(&path))
    }
}

The Metadata struct contains:

FieldTypeDescription
pathPathBufThe queried path
file_typeFileTypeFile, Directory, or Symlink
lenu64Size in bytes
permissionsPermissionsPermission bits
createdOption<SystemTime>Creation time
modifiedOption<SystemTime>Last modification
accessedOption<SystemTime>Last access
inodeOption<u64>Inode number
uidOption<u32>Owner user ID
gidOption<u32>Owner group ID
nlinkOption<u32>Hard link count

exists - Check Path Existence

#![allow(unused)]
fn main() {
    fn exists(&self, path: &Path) -> bool {
        let path = Self::normalize_path(path);
        let inner = self.inner.read().unwrap();
        inner.nodes.contains_key(&path)
    }
}

Important: exists never fails. It returns false for any error condition.

Error Handling Guidelines

SituationError to return
Path doesn’t existFsError::NotFound { path }
Path is a directory when file expectedFsError::IsADirectory { path }
Permission deniedFsError::PermissionDenied { path }

Always include the path in error context so callers know what failed.

Testing Your Implementation

#![allow(unused)]
fn main() {
#[test]
fn test_read_existing_file() {
    let fs = TutorialFs::new();
    
    // Setup: Create a file (we'll implement write later)
    {
        let mut inner = fs.inner.write().unwrap();
        let inode = TutorialFs::alloc_inode(&mut inner);
        let node = FsNode::new_file(b"Hello, World!".to_vec(), inode);
        inner.nodes.insert(PathBuf::from("/test.txt"), node);
    }
    
    // Test read
    let content = fs.read(Path::new("/test.txt")).unwrap();
    assert_eq!(content, b"Hello, World!");
}

#[test]
fn test_read_nonexistent_file() {
    let fs = TutorialFs::new();
    
    let result = fs.read(Path::new("/nonexistent.txt"));
    assert!(matches!(result, Err(FsError::NotFound { .. })));
}

#[test]
fn test_read_directory_fails() {
    let fs = TutorialFs::new();
    
    // Root directory exists
    let result = fs.read(Path::new("/"));
    assert!(matches!(result, Err(FsError::IsADirectory { .. })));
}

#[test]
fn test_exists() {
    let fs = TutorialFs::new();
    
    assert!(fs.exists(Path::new("/")));  // Root always exists
    assert!(!fs.exists(Path::new("/nonexistent")));
}
}

Summary

FsRead provides:

  • read() - Get file contents
  • metadata() - Get file/directory information
  • exists() - Quick existence check

Next, we’ll implement FsWrite →

FsWrite: Writing Files

FsWrite provides write operations for files.

The Trait

#![allow(unused)]
fn main() {
pub trait FsWrite: Send + Sync {
    /// Write content to a file, creating it if it doesn't exist.
    fn write(&self, path: &Path, content: &[u8]) -> Result<(), FsError>;

    /// Remove a file.
    fn remove_file(&self, path: &Path) -> Result<(), FsError>;
}
}

Implementation

write - Write File Contents

#![allow(unused)]
fn main() {
use anyfs_backend::{FsWrite, FsError, FileType};

impl FsWrite for TutorialFs {
    fn write(&self, path: &Path, content: &[u8]) -> Result<(), FsError> {
        let path = Self::normalize_path(path);
        let mut inner = self.inner.write().unwrap();

        // Check parent directory exists
        if let Some(parent) = path.parent() {
            let parent = Self::normalize_path(parent);
            match inner.nodes.get(&parent) {
                None => {
                    return Err(FsError::NotFound { path: parent });
                }
                Some(node) if node.file_type != FileType::Directory => {
                    return Err(FsError::NotADirectory { path: parent });
                }
                _ => {}
            }
        }

        // Can't write to a directory
        if let Some(existing) = inner.nodes.get(&path) {
            if existing.file_type == FileType::Directory {
                return Err(FsError::IsADirectory { path });
            }
        }

        // Create or update the file
        let inode = if let Some(existing) = inner.nodes.get(&path) {
            existing.inode  // Reuse existing inode
        } else {
            Self::alloc_inode(&mut inner)
        };

        let mut node = FsNode::new_file(content.to_vec(), inode);
        node.modified = SystemTime::now();

        inner.inode_to_path.insert(inode, path.clone());
        inner.nodes.insert(path, node);

        Ok(())
    }
    // ...
}
}

Key points:

  • Verify parent directory exists before creating file
  • Reuse inode if file already exists (overwrite)
  • Update modification timestamp

remove_file - Delete a File

#![allow(unused)]
fn main() {
    fn remove_file(&self, path: &Path) -> Result<(), FsError> {
        let path = Self::normalize_path(path);
        let mut inner = self.inner.write().unwrap();

        let node = inner.nodes.get(&path)
            .ok_or_else(|| FsError::NotFound { path: path.clone() })?;

        // Can't remove directories with remove_file
        if node.file_type == FileType::Directory {
            return Err(FsError::IsADirectory { path });
        }

        let inode = node.inode;
        inner.nodes.remove(&path);
        inner.inode_to_path.remove(&inode);

        Ok(())
    }
}

Error Handling

SituationError
Parent directory doesn’t existFsError::NotFound { path: parent }
Parent path is a file, not directoryFsError::NotADirectory { path: parent }
Target path is a directoryFsError::IsADirectory { path }
File to remove doesn’t existFsError::NotFound { path }

Testing

#![allow(unused)]
fn main() {
#[test]
fn test_write_creates_file() {
    let fs = TutorialFs::new();
    
    fs.write(Path::new("/hello.txt"), b"Hello!").unwrap();
    
    let content = fs.read(Path::new("/hello.txt")).unwrap();
    assert_eq!(content, b"Hello!");
}

#[test]
fn test_write_overwrites_existing() {
    let fs = TutorialFs::new();
    
    fs.write(Path::new("/file.txt"), b"First").unwrap();
    fs.write(Path::new("/file.txt"), b"Second").unwrap();
    
    let content = fs.read(Path::new("/file.txt")).unwrap();
    assert_eq!(content, b"Second");
}

#[test]
fn test_write_to_nonexistent_parent_fails() {
    let fs = TutorialFs::new();
    
    let result = fs.write(Path::new("/no/such/dir/file.txt"), b"data");
    assert!(matches!(result, Err(FsError::NotFound { .. })));
}

#[test]
fn test_remove_file() {
    let fs = TutorialFs::new();
    
    fs.write(Path::new("/temp.txt"), b"temp").unwrap();
    assert!(fs.exists(Path::new("/temp.txt")));
    
    fs.remove_file(Path::new("/temp.txt")).unwrap();
    assert!(!fs.exists(Path::new("/temp.txt")));
}
}

Summary

FsWrite provides:

  • write() - Create or overwrite file contents
  • remove_file() - Delete a file

Next, we’ll implement FsDir →

FsDir: Directory Operations

FsDir provides directory operations: listing, creating, removing, and renaming.

The Trait

#![allow(unused)]
fn main() {
pub trait FsDir: Send + Sync {
    /// List directory contents.
    fn read_dir(&self, path: &Path) -> Result<ReadDirIter, FsError>;

    /// Create a single directory.
    fn create_dir(&self, path: &Path) -> Result<(), FsError>;

    /// Create directory and all parent directories.
    fn create_dir_all(&self, path: &Path) -> Result<(), FsError>;

    /// Remove an empty directory.
    fn remove_dir(&self, path: &Path) -> Result<(), FsError>;

    /// Remove directory and all contents recursively.
    fn remove_dir_all(&self, path: &Path) -> Result<(), FsError>;

    /// Rename/move a file or directory.
    fn rename(&self, from: &Path, to: &Path) -> Result<(), FsError>;
}
}

The ReadDirIter Type

read_dir returns a ReadDirIter, which is a boxed iterator over Result<DirEntry, FsError>:

#![allow(unused)]
fn main() {
// DirEntry contains info about each directory entry
pub struct DirEntry {
    pub path: PathBuf,       // Full path
    pub name: String,        // Just the filename
    pub file_type: FileType, // File, Directory, or Symlink
    pub inode: Option<u64>,  // Inode if available
}

// ReadDirIter is an iterator
pub struct ReadDirIter(Box<dyn Iterator<Item = Result<DirEntry, FsError>> + Send>);
}

Create a ReadDirIter from a vector:

#![allow(unused)]
fn main() {
let entries = vec![
    Ok(DirEntry { path: PathBuf::from("/foo"), name: "foo".into(), ... }),
    Ok(DirEntry { path: PathBuf::from("/bar"), name: "bar".into(), ... }),
];
ReadDirIter::from_vec(entries)
}

Implementation

read_dir - List Directory Contents

#![allow(unused)]
fn main() {
impl FsDir for TutorialFs {
    fn read_dir(&self, path: &Path) -> Result<ReadDirIter, FsError> {
        let path = Self::normalize_path(path);
        let inner = self.inner.read().unwrap();

        // Verify path exists and is a directory
        let node = inner.nodes.get(&path)
            .ok_or_else(|| FsError::NotFound { path: path.clone() })?;

        if node.file_type != FileType::Directory {
            return Err(FsError::NotADirectory { path });
        }

        // Collect direct children
        let mut entries = Vec::new();
        for (child_path, child_node) in &inner.nodes {
            if let Some(parent) = child_path.parent() {
                if Self::normalize_path(parent) == path && child_path != &path {
                    let name = child_path
                        .file_name()
                        .map(|n| n.to_string_lossy().to_string())
                        .unwrap_or_default();

                    entries.push(Ok(DirEntry {
                        path: child_path.clone(),
                        name,
                        file_type: child_node.file_type,
                        inode: Some(child_node.inode),
                    }));
                }
            }
        }

        // Sort for consistent ordering
        entries.sort_by_key(|e| e.as_ref().map(|e| e.name.clone()).ok());

        Ok(ReadDirIter::from_vec(entries))
    }
    // ...
}
}

create_dir - Create Single Directory

#![allow(unused)]
fn main() {
    fn create_dir(&self, path: &Path) -> Result<(), FsError> {
        let path = Self::normalize_path(path);
        let mut inner = self.inner.write().unwrap();

        // Check if already exists
        if inner.nodes.contains_key(&path) {
            return Err(FsError::AlreadyExists { path });
        }

        // Check parent exists and is a directory
        if let Some(parent) = path.parent() {
            let parent = Self::normalize_path(parent);
            match inner.nodes.get(&parent) {
                None => return Err(FsError::NotFound { path: parent }),
                Some(node) if node.file_type != FileType::Directory => {
                    return Err(FsError::NotADirectory { path: parent });
                }
                _ => {}
            }
        }

        let inode = Self::alloc_inode(&mut inner);
        let node = FsNode::new_directory(inode);
        inner.inode_to_path.insert(inode, path.clone());
        inner.nodes.insert(path, node);

        Ok(())
    }
}

create_dir_all - Create Directory Tree

#![allow(unused)]
fn main() {
    fn create_dir_all(&self, path: &Path) -> Result<(), FsError> {
        let path = Self::normalize_path(path);

        // Build list of directories to create (from root to leaf)
        let mut to_create = Vec::new();
        let mut current = path.clone();

        while current != Path::new("/") {
            if !self.exists(&current) {
                to_create.push(current.clone());
            }
            match current.parent() {
                Some(parent) => current = parent.to_path_buf(),
                None => break,
            }
        }

        // Create from root towards leaf
        to_create.reverse();
        for dir in to_create {
            match self.create_dir(&dir) {
                Ok(()) | Err(FsError::AlreadyExists { .. }) => {}
                Err(e) => return Err(e),
            }
        }

        Ok(())
    }
}

Note: create_dir_all is idempotent—it succeeds even if the directory exists.

remove_dir - Remove Empty Directory

#![allow(unused)]
fn main() {
    fn remove_dir(&self, path: &Path) -> Result<(), FsError> {
        let path = Self::normalize_path(path);
        let mut inner = self.inner.write().unwrap();

        let node = inner.nodes.get(&path)
            .ok_or_else(|| FsError::NotFound { path: path.clone() })?;

        if node.file_type != FileType::Directory {
            return Err(FsError::NotADirectory { path });
        }

        // Check if directory is empty
        for other_path in inner.nodes.keys() {
            if let Some(parent) = other_path.parent() {
                if Self::normalize_path(parent) == path {
                    return Err(FsError::DirectoryNotEmpty { path });
                }
            }
        }

        let inode = node.inode;
        inner.nodes.remove(&path);
        inner.inode_to_path.remove(&inode);

        Ok(())
    }
}

remove_dir_all - Remove Recursively

#![allow(unused)]
fn main() {
    fn remove_dir_all(&self, path: &Path) -> Result<(), FsError> {
        let path = Self::normalize_path(path);
        let mut inner = self.inner.write().unwrap();

        // Verify it exists and is a directory
        let node = inner.nodes.get(&path)
            .ok_or_else(|| FsError::NotFound { path: path.clone() })?;

        if node.file_type != FileType::Directory {
            return Err(FsError::NotADirectory { path });
        }

        // Collect all paths to remove
        let to_remove: Vec<PathBuf> = inner.nodes.keys()
            .filter(|p| p.starts_with(&path))
            .cloned()
            .collect();

        for p in to_remove {
            if let Some(node) = inner.nodes.remove(&p) {
                inner.inode_to_path.remove(&node.inode);
            }
        }

        Ok(())
    }
}

rename - Move/Rename

#![allow(unused)]
fn main() {
    fn rename(&self, from: &Path, to: &Path) -> Result<(), FsError> {
        let from = Self::normalize_path(from);
        let to = Self::normalize_path(to);
        let mut inner = self.inner.write().unwrap();

        if !inner.nodes.contains_key(&from) {
            return Err(FsError::NotFound { path: from });
        }

        if inner.nodes.contains_key(&to) {
            return Err(FsError::AlreadyExists { path: to });
        }

        // Check destination parent exists
        if let Some(parent) = to.parent() {
            let parent = Self::normalize_path(parent);
            if !inner.nodes.contains_key(&parent) {
                return Err(FsError::NotFound { path: parent });
            }
        }

        if let Some(node) = inner.nodes.remove(&from) {
            inner.inode_to_path.insert(node.inode, to.clone());
            inner.nodes.insert(to, node);
        }

        Ok(())
    }
}

Testing

#![allow(unused)]
fn main() {
#[test]
fn test_create_and_list_dir() {
    let fs = TutorialFs::new();
    
    fs.create_dir(Path::new("/subdir")).unwrap();
    fs.write(Path::new("/subdir/file.txt"), b"content").unwrap();
    
    let entries: Vec<_> = fs.read_dir(Path::new("/subdir"))
        .unwrap()
        .collect();
    
    assert_eq!(entries.len(), 1);
    assert_eq!(entries[0].as_ref().unwrap().name, "file.txt");
}

#[test]
fn test_create_dir_all() {
    let fs = TutorialFs::new();
    
    fs.create_dir_all(Path::new("/a/b/c/d")).unwrap();
    
    assert!(fs.exists(Path::new("/a")));
    assert!(fs.exists(Path::new("/a/b")));
    assert!(fs.exists(Path::new("/a/b/c")));
    assert!(fs.exists(Path::new("/a/b/c/d")));
}

#[test]
fn test_remove_nonempty_dir_fails() {
    let fs = TutorialFs::new();
    
    fs.create_dir(Path::new("/dir")).unwrap();
    fs.write(Path::new("/dir/file.txt"), b"data").unwrap();
    
    let result = fs.remove_dir(Path::new("/dir"));
    assert!(matches!(result, Err(FsError::DirectoryNotEmpty { .. })));
}
}

Summary

FsDir provides:

  • read_dir() - List directory entries
  • create_dir() / create_dir_all() - Create directories
  • remove_dir() / remove_dir_all() - Remove directories
  • rename() - Move or rename entries

Next: The Fs Trait →

The Fs Trait

Fs is the first composite trait. It combines FsRead, FsWrite, and FsDir into a single bound.

Automatic Implementation

Here’s the magic: you don’t implement Fs directly. It’s automatically provided:

#![allow(unused)]
fn main() {
// In anyfs-backend (simplified)
pub trait Fs: FsRead + FsWrite + FsDir + Send + Sync {}

// Blanket implementation
impl<T> Fs for T where T: FsRead + FsWrite + FsDir + Send + Sync {}
}

If your type implements FsRead + FsWrite + FsDir and is Send + Sync, it automatically implements Fs.

Verify Your Implementation

After implementing the three component traits, verify Fs works:

// Compile-time verification
fn use_fs<B: Fs>(_: &B) {}

fn main() {
    let fs = TutorialFs::new();
    use_fs(&fs);  // ✅ Compiles! TutorialFs implements Fs
}

You can also verify Send + Sync at compile time:

#![allow(unused)]
fn main() {
const _: () = {
    const fn assert_send_sync<T: Send + Sync>() {}
    assert_send_sync::<TutorialFs>();
};
}

Using the Fs Bound

Now you can write generic functions that work with any filesystem:

#![allow(unused)]
fn main() {
use anyfs_backend::{Fs, FsError};
use std::path::Path;

/// Copy a file from src to dst.
fn copy_file<B: Fs>(fs: &B, src: &Path, dst: &Path) -> Result<(), FsError> {
    let content = fs.read(src)?;
    fs.write(dst, &content)
}

/// Count files in a directory (non-recursive).
fn count_files<B: Fs>(fs: &B, dir: &Path) -> Result<usize, FsError> {
    let mut count = 0;
    for entry in fs.read_dir(dir)? {
        let entry = entry?;
        if entry.file_type == FileType::File {
            count += 1;
        }
    }
    Ok(count)
}

/// Check if a path is a file.
fn is_file<B: Fs>(fs: &B, path: &Path) -> bool {
    fs.metadata(path)
        .map(|m| m.file_type == FileType::File)
        .unwrap_or(false)
}
}

What Fs Provides

At this point, your backend supports:

OperationMethod
Read file contentsread()
Get metadatametadata()
Check existenceexists()
Write filewrite()
Delete fileremove_file()
List directoryread_dir()
Create directorycreate_dir() / create_dir_all()
Remove directoryremove_dir() / remove_dir_all()
Rename/moverename()

This is sufficient for many use cases!

What’s Missing?

Fs doesn’t include:

  • Symlinks (FsLink)
  • Permissions (FsPermissions)
  • Sync/flush (FsSync)
  • Disk stats (FsStats)
  • Inode operations (FsInode)
  • File handles (FsHandles)
  • Locking (FsLock)

The remaining tutorials add these features.

Integration Test

Here’s a complete test exercising Fs:

#[test]
fn test_fs_workflow() {
    let fs = TutorialFs::new();

    // Create a project structure
    fs.create_dir_all(Path::new("/project/src")).unwrap();
    
    // Write some files
    fs.write(Path::new("/project/README.md"), b"# My Project").unwrap();
    fs.write(Path::new("/project/src/main.rs"), b"fn main() {}").unwrap();
    
    // Verify structure
    assert!(fs.exists(Path::new("/project")));
    assert!(fs.exists(Path::new("/project/src")));
    assert!(fs.exists(Path::new("/project/README.md")));
    
    // Read back
    let readme = fs.read(Path::new("/project/README.md")).unwrap();
    assert_eq!(readme, b"# My Project");
    
    // List directory
    let entries: Vec<_> = fs.read_dir(Path::new("/project"))
        .unwrap()
        .filter_map(|e| e.ok())
        .collect();
    assert_eq!(entries.len(), 2);  // README.md and src/
    
    // Rename
    fs.rename(
        Path::new("/project/README.md"),
        Path::new("/project/README.txt"),
    ).unwrap();
    assert!(!fs.exists(Path::new("/project/README.md")));
    assert!(fs.exists(Path::new("/project/README.txt")));
    
    // Clean up
    fs.remove_dir_all(Path::new("/project")).unwrap();
    assert!(!fs.exists(Path::new("/project")));
}

Summary

  • Fs = FsRead + FsWrite + FsDir + Send + Sync
  • Automatically implemented via blanket impl
  • Provides basic file operations
  • Sufficient for simple use cases

Next: FsLink: Symlinks →

FsLink: Symlinks

FsLink adds symbolic and hard link support to your backend.

The Trait

#![allow(unused)]
fn main() {
pub trait FsLink: Send + Sync {
    /// Create a symbolic link.
    fn symlink(&self, target: &Path, link: &Path) -> Result<(), FsError>;

    /// Read the target of a symbolic link.
    fn read_link(&self, path: &Path) -> Result<PathBuf, FsError>;

    /// Create a hard link.
    fn hard_link(&self, target: &Path, link: &Path) -> Result<(), FsError>;
}
}
AspectSymlinkHard Link
What it storesPath to targetSame inode as target
Target can be…Anything (even nonexistent)Must exist and be a file
Cross-filesystemYesNo
If target deletedBecomes brokenFile still accessible

Implementation

#![allow(unused)]
fn main() {
impl FsLink for TutorialFs {
    fn symlink(&self, target: &Path, link: &Path) -> Result<(), FsError> {
        let link = Self::normalize_path(link);
        let mut inner = self.inner.write().unwrap();

        // Check if link path already exists
        if inner.nodes.contains_key(&link) {
            return Err(FsError::AlreadyExists { path: link });
        }

        // Check parent directory exists
        if let Some(parent) = link.parent() {
            let parent = Self::normalize_path(parent);
            if !inner.nodes.contains_key(&parent) {
                return Err(FsError::NotFound { path: parent });
            }
        }

        // Create the symlink node
        // Note: target is stored as-is, not normalized
        let inode = Self::alloc_inode(&mut inner);
        let node = FsNode::new_symlink(target.to_path_buf(), inode);
        inner.inode_to_path.insert(inode, link.clone());
        inner.nodes.insert(link, node);

        Ok(())
    }
    // ...
}
}

Key points:

  • The target path is stored as-is (can be relative or absolute)
  • The target doesn’t need to exist
  • The link path must not already exist
#![allow(unused)]
fn main() {
    fn read_link(&self, path: &Path) -> Result<PathBuf, FsError> {
        let path = Self::normalize_path(path);
        let inner = self.inner.read().unwrap();

        let node = inner.nodes.get(&path)
            .ok_or_else(|| FsError::NotFound { path: path.clone() })?;

        match &node.symlink_target {
            Some(target) => Ok(target.clone()),
            None => Err(FsError::InvalidData {
                details: format!("{} is not a symbolic link", path.display()),
            }),
        }
    }
}

Hard links are more complex because they share inodes. For simplicity, you can return Unsupported:

#![allow(unused)]
fn main() {
    fn hard_link(&self, _target: &Path, _link: &Path) -> Result<(), FsError> {
        Err(FsError::Unsupported {
            operation: "hard_link".to_string(),
        })
    }
}

Or implement properly by having multiple paths point to the same inode:

#![allow(unused)]
fn main() {
    fn hard_link(&self, target: &Path, link: &Path) -> Result<(), FsError> {
        let target = Self::normalize_path(target);
        let link = Self::normalize_path(link);
        let mut inner = self.inner.write().unwrap();

        // Target must exist and be a file
        let target_node = inner.nodes.get(&target)
            .ok_or_else(|| FsError::NotFound { path: target.clone() })?;
        
        if target_node.file_type != FileType::File {
            return Err(FsError::InvalidData {
                details: "hard links can only target files".to_string(),
            });
        }

        // Link must not exist
        if inner.nodes.contains_key(&link) {
            return Err(FsError::AlreadyExists { path: link });
        }

        // Clone the node (same content, same inode)
        let mut link_node = target_node.clone();
        // Note: In a real impl, you'd track nlink count
        
        inner.nodes.insert(link.clone(), link_node);
        // Don't add to inode_to_path - inode already maps to target

        Ok(())
    }
}

When reading through a symlink, you may need to resolve it:

#![allow(unused)]
fn main() {
/// Resolve a path, following symlinks.
fn resolve_path<B: Fs + FsLink>(fs: &B, path: &Path) -> Result<PathBuf, FsError> {
    let mut current = path.to_path_buf();
    let mut seen = std::collections::HashSet::new();

    loop {
        // Prevent infinite loops
        if !seen.insert(current.clone()) {
            return Err(FsError::InvalidData {
                details: "symlink loop detected".to_string(),
            });
        }

        match fs.metadata(&current) {
            Ok(meta) if meta.file_type == FileType::Symlink => {
                let target = fs.read_link(&current)?;
                current = if target.is_absolute() {
                    target
                } else {
                    current.parent().unwrap_or(Path::new("/")).join(target)
                };
            }
            Ok(_) => return Ok(current),
            Err(e) => return Err(e),
        }
    }
}
}

Testing

#![allow(unused)]
fn main() {
#[test]
fn test_symlink_creation() {
    let fs = TutorialFs::new();
    
    fs.write(Path::new("/original.txt"), b"content").unwrap();
    fs.symlink(Path::new("/original.txt"), Path::new("/link.txt")).unwrap();
    
    // Check metadata shows it's a symlink
    let meta = fs.metadata(Path::new("/link.txt")).unwrap();
    assert_eq!(meta.file_type, FileType::Symlink);
    
    // Read the link target
    let target = fs.read_link(Path::new("/link.txt")).unwrap();
    assert_eq!(target, Path::new("/original.txt"));
}

#[test]
fn test_symlink_to_nonexistent() {
    let fs = TutorialFs::new();
    
    // This should succeed - symlinks can point to nonexistent targets
    fs.symlink(Path::new("/nonexistent"), Path::new("/broken-link")).unwrap();
    
    let target = fs.read_link(Path::new("/broken-link")).unwrap();
    assert_eq!(target, Path::new("/nonexistent"));
}

#[test]
fn test_read_link_on_regular_file_fails() {
    let fs = TutorialFs::new();
    
    fs.write(Path::new("/file.txt"), b"data").unwrap();
    
    let result = fs.read_link(Path::new("/file.txt"));
    assert!(matches!(result, Err(FsError::InvalidData { .. })));
}
}

Summary

FsLink provides:

  • symlink() - Create symbolic links
  • read_link() - Read symlink target
  • hard_link() - Create hard links (optional)

Next: FsFull: Complete Filesystem →

FsFull: Complete Filesystem

FsFull adds permissions, sync, and stats to reach “complete” filesystem semantics.

The Trait

#![allow(unused)]
fn main() {
// FsFull = Fs + FsLink + FsPermissions + FsSync + FsStats
pub trait FsFull: Fs + FsLink + FsPermissions + FsSync + FsStats {}
}

Like Fs, it’s automatically implemented via blanket impl.

Component Traits

FsPermissions

#![allow(unused)]
fn main() {
pub trait FsPermissions: Send + Sync {
    /// Set file/directory permissions.
    fn set_permissions(&self, path: &Path, permissions: Permissions) -> Result<(), FsError>;

    /// Set owner UID and/or GID.
    fn set_owner(&self, path: &Path, uid: Option<u32>, gid: Option<u32>) -> Result<(), FsError>;
}
}

Implementation:

#![allow(unused)]
fn main() {
impl FsPermissions for TutorialFs {
    fn set_permissions(&self, path: &Path, permissions: Permissions) -> Result<(), FsError> {
        let path = Self::normalize_path(path);
        let mut inner = self.inner.write().unwrap();

        let node = inner.nodes.get_mut(&path)
            .ok_or_else(|| FsError::NotFound { path: path.clone() })?;

        node.permissions = permissions;
        node.modified = SystemTime::now();

        Ok(())
    }

    fn set_owner(&self, path: &Path, uid: Option<u32>, gid: Option<u32>) -> Result<(), FsError> {
        let path = Self::normalize_path(path);
        let inner = self.inner.read().unwrap();

        // Verify path exists
        if !inner.nodes.contains_key(&path) {
            return Err(FsError::NotFound { path });
        }

        // In-memory fs: we don't actually store uid/gid changes
        // A real implementation would update the node
        Ok(())
    }
}
}

FsSync

#![allow(unused)]
fn main() {
pub trait FsSync: Send + Sync {
    /// Flush pending writes for a specific file.
    fn sync(&self, path: &Path) -> Result<(), FsError>;

    /// Flush all pending writes.
    fn sync_all(&self) -> Result<(), FsError>;
}
}

Implementation:

#![allow(unused)]
fn main() {
impl FsSync for TutorialFs {
    fn sync(&self, path: &Path) -> Result<(), FsError> {
        let path = Self::normalize_path(path);
        let inner = self.inner.read().unwrap();

        // Verify file exists
        if !inner.nodes.contains_key(&path) {
            return Err(FsError::NotFound { path });
        }

        // In-memory: nothing to sync
        Ok(())
    }

    fn sync_all(&self) -> Result<(), FsError> {
        // In-memory: nothing to sync
        Ok(())
    }
}
}

For a real disk-backed filesystem, you’d call fsync() or equivalent.

FsStats

#![allow(unused)]
fn main() {
pub trait FsStats: Send + Sync {
    /// Get filesystem statistics.
    fn statfs(&self) -> Result<StatFs, FsError>;
}

pub struct StatFs {
    pub total_bytes: u64,
    pub available_bytes: u64,
    pub used_bytes: u64,
    pub total_inodes: Option<u64>,
    pub available_inodes: Option<u64>,
    pub used_inodes: Option<u64>,
    pub block_size: Option<u64>,
}
}

Implementation:

#![allow(unused)]
fn main() {
impl FsStats for TutorialFs {
    fn statfs(&self) -> Result<StatFs, FsError> {
        let inner = self.inner.read().unwrap();

        // Calculate used space
        let used_bytes: u64 = inner.nodes.values()
            .map(|n| n.content.len() as u64)
            .sum();

        let used_inodes = inner.nodes.len() as u64;

        Ok(StatFs {
            total_bytes: inner.total_size,
            available_bytes: inner.total_size.saturating_sub(used_bytes),
            used_bytes,
            total_inodes: Some(1_000_000),
            available_inodes: Some(1_000_000 - used_inodes),
            used_inodes: Some(used_inodes),
            block_size: Some(4096),
        })
    }
}
}

Verify FsFull

After implementing all component traits:

fn use_fs_full<B: FsFull>(_: &B) {}

fn main() {
    let fs = TutorialFs::new();
    use_fs_full(&fs);  // ✅ Compiles!
}

Using FsFull

#![allow(unused)]
fn main() {
use anyfs_backend::{FsFull, FsError, Permissions};

/// Make a file read-only.
fn make_readonly<B: FsFull>(fs: &B, path: &Path) -> Result<(), FsError> {
    fs.set_permissions(path, Permissions::from_mode(0o444))
}

/// Get disk usage percentage.
fn disk_usage<B: FsFull>(fs: &B) -> Result<f64, FsError> {
    let stats = fs.statfs()?;
    Ok((stats.used_bytes as f64 / stats.total_bytes as f64) * 100.0)
}

/// Write and flush immediately.
fn write_sync<B: FsFull>(fs: &B, path: &Path, data: &[u8]) -> Result<(), FsError> {
    fs.write(path, data)?;
    fs.sync(path)
}
}

Testing

#![allow(unused)]
fn main() {
#[test]
fn test_set_permissions() {
    let fs = TutorialFs::new();
    
    fs.write(Path::new("/file.txt"), b"data").unwrap();
    fs.set_permissions(Path::new("/file.txt"), Permissions::from_mode(0o600)).unwrap();
    
    let meta = fs.metadata(Path::new("/file.txt")).unwrap();
    assert_eq!(meta.permissions.mode(), 0o600);
}

#[test]
fn test_statfs() {
    let fs = TutorialFs::new();
    
    // Write some data
    fs.write(Path::new("/a.txt"), b"Hello").unwrap();
    fs.write(Path::new("/b.txt"), b"World!").unwrap();
    
    let stats = fs.statfs().unwrap();
    assert!(stats.used_bytes >= 11);  // At least "Hello" + "World!"
    assert!(stats.available_bytes < stats.total_bytes);
}
}

Summary

FsFull = Fs + FsLink + FsPermissions + FsSync + FsStats

TraitPurpose
FsPermissionsSet mode and ownership
FsSyncFlush data to storage
FsStatsDisk space information

This is suitable for most real-world applications.

Next: FsInode: FUSE Support →

FsInode: FUSE Support

FsInode provides inode-based operations, essential for FUSE (Filesystem in Userspace) implementations.

Why Inodes?

FUSE operates on inodes rather than paths:

Path-based:  read("/home/user/file.txt")
Inode-based: read(inode=12345, offset=0, len=100)

Benefits:

  • Efficiency: Inode lookup is O(1), path resolution is O(n)
  • Handles edge cases: Deleted-but-open files, renamed files
  • FUSE requirement: FUSE kernel module uses inodes

The Trait

#![allow(unused)]
fn main() {
pub trait FsInode: Send + Sync {
    /// Convert a path to its inode number.
    fn path_to_inode(&self, path: &Path) -> Result<u64, FsError>;

    /// Convert an inode to its path.
    fn inode_to_path(&self, inode: u64) -> Result<PathBuf, FsError>;

    /// Get metadata by inode.
    fn metadata_by_inode(&self, inode: u64) -> Result<Metadata, FsError>;

    /// Look up child by name within a parent directory.
    fn lookup(&self, parent_inode: u64, name: &str) -> Result<u64, FsError>;
}
}

The ROOT_INODE Constant

Root directory always has inode 1:

#![allow(unused)]
fn main() {
use anyfs_backend::ROOT_INODE;

assert_eq!(ROOT_INODE, 1);
}

Implementation

path_to_inode

#![allow(unused)]
fn main() {
impl FsInode for TutorialFs {
    fn path_to_inode(&self, path: &Path) -> Result<u64, FsError> {
        let path = Self::normalize_path(path);
        let inner = self.inner.read().unwrap();

        let node = inner.nodes.get(&path)
            .ok_or_else(|| FsError::NotFound { path: path.clone() })?;

        Ok(node.inode)
    }
    // ...
}
}

inode_to_path

#![allow(unused)]
fn main() {
    fn inode_to_path(&self, inode: u64) -> Result<PathBuf, FsError> {
        let inner = self.inner.read().unwrap();

        inner.inode_to_path.get(&inode)
            .cloned()
            .ok_or(FsError::InodeNotFound { inode })
    }
}

Note the special error type FsError::InodeNotFound.

metadata_by_inode

#![allow(unused)]
fn main() {
    fn metadata_by_inode(&self, inode: u64) -> Result<Metadata, FsError> {
        let inner = self.inner.read().unwrap();

        let path = inner.inode_to_path.get(&inode)
            .ok_or(FsError::InodeNotFound { inode })?;

        let node = inner.nodes.get(path)
            .ok_or(FsError::InodeNotFound { inode })?;

        Ok(node.to_metadata(path))
    }
}

lookup - The Key FUSE Operation

This is how FUSE navigates directories:

#![allow(unused)]
fn main() {
    fn lookup(&self, parent_inode: u64, name: &str) -> Result<u64, FsError> {
        let inner = self.inner.read().unwrap();

        // Get parent path
        let parent_path = inner.inode_to_path.get(&parent_inode)
            .ok_or(FsError::InodeNotFound { inode: parent_inode })?;

        // Build child path
        let child_path = parent_path.join(name);

        // Look up child
        let child_node = inner.nodes.get(&child_path)
            .ok_or_else(|| FsError::NotFound { path: child_path })?;

        Ok(child_node.inode)
    }
}

How FUSE Uses This

When a user accesses /home/user/file.txt, FUSE:

  1. Starts at ROOT_INODE (1)
  2. Calls lookup(1, "home") → inode 2
  3. Calls lookup(2, "user") → inode 5
  4. Calls lookup(5, "file.txt") → inode 12
  5. Calls metadata_by_inode(12) → file metadata

FsFuse Trait

FsFuse combines everything:

#![allow(unused)]
fn main() {
pub trait FsFuse: FsFull + FsInode {}
}

It’s automatically implemented:

fn use_fs_fuse<B: FsFuse>(_: &B) {}

fn main() {
    let fs = TutorialFs::new();
    use_fs_fuse(&fs);  // ✅ Works!
}

Testing

#![allow(unused)]
fn main() {
#[test]
fn test_path_to_inode() {
    let fs = TutorialFs::new();
    
    // Root is always inode 1
    let root_inode = fs.path_to_inode(Path::new("/")).unwrap();
    assert_eq!(root_inode, ROOT_INODE);
}

#[test]
fn test_inode_roundtrip() {
    let fs = TutorialFs::new();
    
    fs.create_dir(Path::new("/mydir")).unwrap();
    
    let inode = fs.path_to_inode(Path::new("/mydir")).unwrap();
    let path = fs.inode_to_path(inode).unwrap();
    
    assert_eq!(path, Path::new("/mydir"));
}

#[test]
fn test_lookup() {
    let fs = TutorialFs::new();
    
    fs.create_dir(Path::new("/parent")).unwrap();
    fs.write(Path::new("/parent/child.txt"), b"data").unwrap();
    
    let parent_inode = fs.path_to_inode(Path::new("/parent")).unwrap();
    let child_inode = fs.lookup(parent_inode, "child.txt").unwrap();
    
    let child_meta = fs.metadata_by_inode(child_inode).unwrap();
    assert_eq!(child_meta.file_type, FileType::File);
}

#[test]
fn test_lookup_from_root() {
    let fs = TutorialFs::new();
    
    fs.create_dir(Path::new("/documents")).unwrap();
    
    let docs_inode = fs.lookup(ROOT_INODE, "documents").unwrap();
    assert!(docs_inode > ROOT_INODE);  // Should be a new inode
}

#[test]
fn test_inode_not_found() {
    let fs = TutorialFs::new();
    
    let result = fs.inode_to_path(99999);
    assert!(matches!(result, Err(FsError::InodeNotFound { inode: 99999 })));
}
}

Summary

FsInode provides inode-based access:

  • path_to_inode() / inode_to_path() - Convert between paths and inodes
  • metadata_by_inode() - Get metadata by inode
  • lookup() - Find child in directory by name

FsFuse = FsFull + FsInode - Ready for FUSE implementation.

Next: FsPosix: Full POSIX →

FsPosix: Full POSIX Semantics

FsPosix is the final layer, adding file handles, locking, and extended attributes for complete POSIX semantics.

The Trait

#![allow(unused)]
fn main() {
pub trait FsPosix: FsFuse + FsHandles + FsLock + FsXattr {}
}

Component Traits

FsHandles - File Handle Operations

Instead of reading/writing entire files, handles allow:

  • Opening a file once, performing many operations
  • Reading/writing at specific offsets
  • Keeping files open across operations
#![allow(unused)]
fn main() {
pub trait FsHandles: Send + Sync {
    /// Open a file and return a handle.
    fn open(&self, path: &Path, flags: OpenFlags) -> Result<Handle, FsError>;

    /// Close a file handle.
    fn close(&self, handle: Handle) -> Result<(), FsError>;

    /// Read from a file at a specific offset.
    fn read_at(&self, handle: Handle, offset: u64, len: usize) -> Result<Vec<u8>, FsError>;

    /// Write to a file at a specific offset.
    fn write_at(&self, handle: Handle, offset: u64, data: &[u8]) -> Result<usize, FsError>;
}
}

OpenFlags

#![allow(unused)]
fn main() {
bitflags! {
    pub struct OpenFlags: u32 {
        const READ = 0b0001;
        const WRITE = 0b0010;
        const CREATE = 0b0100;
        const TRUNCATE = 0b1000;
    }
}
}

Implementation

#![allow(unused)]
fn main() {
impl FsHandles for TutorialFs {
    fn open(&self, path: &Path, flags: OpenFlags) -> Result<Handle, FsError> {
        let path = Self::normalize_path(path);
        let mut inner = self.inner.write().unwrap();

        let exists = inner.nodes.contains_key(&path);

        // Handle creation
        if flags.contains(OpenFlags::CREATE) && !exists {
            let inode = Self::alloc_inode(&mut inner);
            let node = FsNode::new_file(Vec::new(), inode);
            inner.inode_to_path.insert(inode, path.clone());
            inner.nodes.insert(path.clone(), node);
        } else if !exists {
            return Err(FsError::NotFound { path });
        }

        // Truncate if requested
        if flags.contains(OpenFlags::TRUNCATE) {
            if let Some(node) = inner.nodes.get_mut(&path) {
                node.content.clear();
            }
        }

        // Allocate handle
        let handle = Self::alloc_handle(&mut inner);
        inner.handles.insert(handle, HandleState {
            path,
            flags,
            locked: None,
        });

        Ok(handle)
    }

    fn close(&self, handle: Handle) -> Result<(), FsError> {
        let mut inner = self.inner.write().unwrap();
        
        inner.handles.remove(&handle)
            .ok_or(FsError::InvalidHandle { handle })?;
        
        Ok(())
    }

    fn read_at(&self, handle: Handle, offset: u64, len: usize) -> Result<Vec<u8>, FsError> {
        let inner = self.inner.read().unwrap();

        let state = inner.handles.get(&handle)
            .ok_or(FsError::InvalidHandle { handle })?;

        // Check read permission
        if !state.flags.contains(OpenFlags::READ) {
            return Err(FsError::PermissionDenied { path: state.path.clone() });
        }

        let node = inner.nodes.get(&state.path)
            .ok_or_else(|| FsError::NotFound { path: state.path.clone() })?;

        let start = offset as usize;
        if start >= node.content.len() {
            return Ok(Vec::new());  // EOF
        }

        let end = (start + len).min(node.content.len());
        Ok(node.content[start..end].to_vec())
    }

    fn write_at(&self, handle: Handle, offset: u64, data: &[u8]) -> Result<usize, FsError> {
        let mut inner = self.inner.write().unwrap();

        // Get path from handle
        let path = {
            let state = inner.handles.get(&handle)
                .ok_or(FsError::InvalidHandle { handle })?;

            if !state.flags.contains(OpenFlags::WRITE) {
                return Err(FsError::PermissionDenied { path: state.path.clone() });
            }
            state.path.clone()
        };

        let node = inner.nodes.get_mut(&path)
            .ok_or_else(|| FsError::NotFound { path })?;

        let start = offset as usize;

        // Extend file if necessary
        if start + data.len() > node.content.len() {
            node.content.resize(start + data.len(), 0);
        }

        node.content[start..start + data.len()].copy_from_slice(data);
        node.modified = SystemTime::now();

        Ok(data.len())
    }
}
}

FsLock - File Locking

Prevents concurrent access conflicts:

#![allow(unused)]
fn main() {
pub trait FsLock: Send + Sync {
    /// Acquire a lock (blocks until available).
    fn lock(&self, handle: Handle, lock_type: LockType) -> Result<(), FsError>;

    /// Try to acquire a lock (non-blocking).
    fn try_lock(&self, handle: Handle, lock_type: LockType) -> Result<bool, FsError>;

    /// Release a lock.
    fn unlock(&self, handle: Handle) -> Result<(), FsError>;
}

pub enum LockType {
    Shared,     // Multiple readers allowed
    Exclusive,  // Single writer, no readers
}
}

Implementation

#![allow(unused)]
fn main() {
impl FsLock for TutorialFs {
    fn lock(&self, handle: Handle, lock_type: LockType) -> Result<(), FsError> {
        let mut inner = self.inner.write().unwrap();

        let state = inner.handles.get_mut(&handle)
            .ok_or(FsError::InvalidHandle { handle })?;

        // Simple implementation: just record the lock
        // Real implementation would check for conflicts
        state.locked = Some(lock_type);

        Ok(())
    }

    fn try_lock(&self, handle: Handle, lock_type: LockType) -> Result<bool, FsError> {
        // For simplicity, always succeed
        self.lock(handle, lock_type)?;
        Ok(true)
    }

    fn unlock(&self, handle: Handle) -> Result<(), FsError> {
        let mut inner = self.inner.write().unwrap();

        let state = inner.handles.get_mut(&handle)
            .ok_or(FsError::InvalidHandle { handle })?;

        state.locked = None;

        Ok(())
    }
}
}

FsXattr - Extended Attributes

Store arbitrary metadata on files (like Linux xattr):

#![allow(unused)]
fn main() {
pub trait FsXattr: Send + Sync {
    fn get_xattr(&self, path: &Path, name: &str) -> Result<Vec<u8>, FsError>;
    fn set_xattr(&self, path: &Path, name: &str, value: &[u8]) -> Result<(), FsError>;
    fn remove_xattr(&self, path: &Path, name: &str) -> Result<(), FsError>;
    fn list_xattr(&self, path: &Path) -> Result<Vec<String>, FsError>;
}
}

For simplicity, you can return Unsupported:

#![allow(unused)]
fn main() {
impl FsXattr for TutorialFs {
    fn get_xattr(&self, _path: &Path, _name: &str) -> Result<Vec<u8>, FsError> {
        Err(FsError::Unsupported { operation: "xattr".to_string() })
    }
    // ... same for other methods
}
}

Putting It Together

With all traits implemented, verify FsPosix:

fn use_fs_posix<B: FsPosix>(_: &B) {}

fn main() {
    let fs = TutorialFs::new();
    use_fs_posix(&fs);  // ✅ Full POSIX support!
}

Usage Example

#![allow(unused)]
fn main() {
use anyfs_backend::{FsPosix, OpenFlags, LockType, FsError};

fn atomic_update<B: FsPosix>(
    fs: &B,
    path: &Path,
    updater: impl FnOnce(&[u8]) -> Vec<u8>,
) -> Result<(), FsError> {
    // Open with read/write
    let handle = fs.open(path, OpenFlags::READ | OpenFlags::WRITE)?;
    
    // Lock exclusively
    fs.lock(handle, LockType::Exclusive)?;
    
    // Read current content
    let current = fs.read_at(handle, 0, usize::MAX)?;
    
    // Apply update
    let new_content = updater(&current);
    
    // Write back (truncate by writing from offset 0)
    fs.write_at(handle, 0, &new_content)?;
    
    // Unlock and close
    fs.unlock(handle)?;
    fs.close(handle)?;
    
    Ok(())
}
}

Testing

#![allow(unused)]
fn main() {
#[test]
fn test_handle_read_write() {
    let fs = TutorialFs::new();
    
    // Create and open file
    let handle = fs.open(
        Path::new("/file.txt"),
        OpenFlags::READ | OpenFlags::WRITE | OpenFlags::CREATE,
    ).unwrap();
    
    // Write
    fs.write_at(handle, 0, b"Hello, World!").unwrap();
    
    // Read back
    let data = fs.read_at(handle, 0, 5).unwrap();
    assert_eq!(data, b"Hello");
    
    // Read with offset
    let data = fs.read_at(handle, 7, 5).unwrap();
    assert_eq!(data, b"World");
    
    fs.close(handle).unwrap();
}

#[test]
fn test_locking() {
    let fs = TutorialFs::new();
    
    let handle = fs.open(
        Path::new("/locked.txt"),
        OpenFlags::WRITE | OpenFlags::CREATE,
    ).unwrap();
    
    fs.lock(handle, LockType::Exclusive).unwrap();
    fs.write_at(handle, 0, b"Protected data").unwrap();
    fs.unlock(handle).unwrap();
    
    fs.close(handle).unwrap();
}

#[test]
fn test_invalid_handle() {
    let fs = TutorialFs::new();
    
    let invalid = Handle(99999);
    
    assert!(matches!(
        fs.read_at(invalid, 0, 10),
        Err(FsError::InvalidHandle { .. })
    ));
}
}

Summary

You’ve implemented a complete filesystem backend!

LayerTraits
FsFsRead + FsWrite + FsDir
FsFullFs + FsLink + FsPermissions + FsSync + FsStats
FsFuseFsFull + FsInode
FsPosixFsFuse + FsHandles + FsLock + FsXattr

Your TutorialFs now supports:

  • ✅ File read/write
  • ✅ Directory operations
  • ✅ Symlinks
  • ✅ Permissions
  • ✅ Filesystem stats
  • ✅ Inode-based access
  • ✅ File handles
  • ✅ File locking

🎉 Congratulations! You’ve built a full-featured filesystem backend.

Next, learn how to create middleware: Implementing Middleware →

Implementing Middleware

This tutorial teaches you how to create middleware layers that wrap filesystem backends to add cross-cutting functionality.

What You’ll Learn

  • The Layer pattern (inspired by Tower)
  • How to wrap any backend with additional behavior
  • Creating logging, metrics, caching, and access control layers
  • Composing multiple layers together

Prerequisites

  • Completed the backend tutorial or understand the trait hierarchy
  • Familiarity with Rust traits and generics

The Layer Pattern

Middleware wraps a backend to intercept operations:

              Request
                 │
                 ▼
     ┌─────────────────────┐
     │   Logging Layer     │ ← Logs all operations
     └─────────────────────┘
                 │
                 ▼
     ┌─────────────────────┐
     │   Caching Layer     │ ← Serves from cache
     └─────────────────────┘
                 │
                 ▼
     ┌─────────────────────┐
     │   Actual Backend    │ ← Real filesystem
     └─────────────────────┘
                 │
                 ▼
             Response

Tutorial Structure

  1. The Layer Pattern - Understanding the Layer trait
  2. Logging Layer - Log all operations
  3. Metrics Layer - Collect statistics
  4. Caching Layer - Cache read results
  5. Access Control Layer - Restrict operations
  6. Composing Layers - Stack layers together

Quick Example

#![allow(unused)]
fn main() {
use anyfs_backend::{Fs, Layer};

// Create a backend
let fs = InMemoryFs::new();

// Wrap with logging
let fs = LoggingLayer::new("MyApp").layer(fs);

// Wrap with caching
let fs = CachingLayer::new(Duration::from_secs(60)).layer(fs);

// Use normally - logging and caching are automatic
fs.write(Path::new("/file.txt"), b"Hello").unwrap();
let data = fs.read(Path::new("/file.txt")).unwrap();  // Cached!
}

Let’s start with The Layer Pattern →

The Layer Pattern

The Layer pattern allows you to wrap a backend with additional behavior without modifying the backend itself.

The Layer Trait

#![allow(unused)]
fn main() {
pub trait Layer<B> {
    /// The resulting wrapped type.
    type Wrapped;

    /// Wrap the backend, producing a new type.
    fn layer(self, inner: B) -> Self::Wrapped;
}
}
  • B: The inner backend type being wrapped
  • Wrapped: The resulting wrapped type (must implement same traits)
  • layer(): Consumes the layer config and backend, produces wrapped backend

Basic Structure

Every layer needs two types:

  1. Layer type: Configuration/factory (e.g., LoggingLayer)
  2. Wrapped type: The actual wrapper (e.g., LoggingFs<B>)
#![allow(unused)]
fn main() {
/// Layer configuration (the factory)
pub struct MyLayer {
    // Configuration options
}

/// The wrapped backend
pub struct MyWrapper<B> {
    inner: B,
    // Layer state
}

impl<B> Layer<B> for MyLayer {
    type Wrapped = MyWrapper<B>;

    fn layer(self, inner: B) -> Self::Wrapped {
        MyWrapper {
            inner,
            // Initialize state from config
        }
    }
}
}

Pass-Through Layer Example

The simplest layer does nothing—it just forwards all calls:

#![allow(unused)]
fn main() {
use anyfs_backend::{Layer, FsRead, FsWrite, FsDir, FsError, Metadata, ReadDirIter};
use std::path::Path;

/// A layer that does nothing.
pub struct PassThroughLayer;

/// Wraps any backend, forwarding all calls.
pub struct PassThrough<B> {
    inner: B,
}

impl<B> Layer<B> for PassThroughLayer {
    type Wrapped = PassThrough<B>;

    fn layer(self, inner: B) -> Self::Wrapped {
        PassThrough { inner }
    }
}

// Forward all trait methods to inner

impl<B: FsRead> FsRead for PassThrough<B> {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        self.inner.read(path)
    }

    fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
        self.inner.metadata(path)
    }

    fn exists(&self, path: &Path) -> bool {
        self.inner.exists(path)
    }
}

impl<B: FsWrite> FsWrite for PassThrough<B> {
    fn write(&self, path: &Path, content: &[u8]) -> Result<(), FsError> {
        self.inner.write(path, content)
    }

    fn remove_file(&self, path: &Path) -> Result<(), FsError> {
        self.inner.remove_file(path)
    }
}

impl<B: FsDir> FsDir for PassThrough<B> {
    fn read_dir(&self, path: &Path) -> Result<ReadDirIter, FsError> {
        self.inner.read_dir(path)
    }

    fn create_dir(&self, path: &Path) -> Result<(), FsError> {
        self.inner.create_dir(path)
    }

    fn create_dir_all(&self, path: &Path) -> Result<(), FsError> {
        self.inner.create_dir_all(path)
    }

    fn remove_dir(&self, path: &Path) -> Result<(), FsError> {
        self.inner.remove_dir(path)
    }

    fn remove_dir_all(&self, path: &Path) -> Result<(), FsError> {
        self.inner.remove_dir_all(path)
    }

    fn rename(&self, from: &Path, to: &Path) -> Result<(), FsError> {
        self.inner.rename(from, to)
    }
}
}

Why This Pattern?

1. Composition

Layers can be stacked:

#![allow(unused)]
fn main() {
let fs = backend
    .layer(LoggingLayer::new())
    .layer(CachingLayer::new())
    .layer(MetricsLayer::new());
}

2. Separation of Concerns

Each layer handles one thing:

  • Logging layer: only logs
  • Caching layer: only caches
  • Metrics layer: only counts

3. Reusability

Write once, use with any backend:

#![allow(unused)]
fn main() {
let memory_fs = InMemoryFs::new().layer(LoggingLayer::new());
let disk_fs = DiskFs::new("/").layer(LoggingLayer::new());
let s3_fs = S3Fs::new(bucket).layer(LoggingLayer::new());
}

The LayerExt Trait

For convenient chaining, use LayerExt:

#![allow(unused)]
fn main() {
use anyfs_backend::LayerExt;

// Instead of:
let fs = LoggingLayer::new().layer(backend);

// You can write:
let fs = backend.layer(LoggingLayer::new());
}

LayerExt is automatically implemented for all types.

Trait Bounds

The wrapper only implements traits that the inner backend implements:

#![allow(unused)]
fn main() {
impl<B: FsRead> FsRead for MyWrapper<B> { ... }
//      ^^^^^^
//      Only if B implements FsRead
}

This means:

  • Wrapping an Fs backend gives you an Fs wrapper
  • Wrapping an FsFull backend gives you an FsFull wrapper
  • The wrapper “inherits” the inner backend’s capabilities

Key Points

  1. Layer = Configuration + Factory
  2. Wrapped = The actual wrapper struct
  3. Forward traits you want to preserve
  4. Add behavior in the forwarding methods
  5. Use generics to work with any backend

Next: Logging Layer →

Logging Layer

A logging layer prints information about each filesystem operation. Useful for:

  • Debugging
  • Auditing
  • Understanding access patterns

Design

#![allow(unused)]
fn main() {
pub struct LoggingLayer {
    prefix: String,  // Prefix for log messages
}

pub struct LoggingFs<B> {
    inner: B,
    prefix: String,
}
}

Implementation

#![allow(unused)]
fn main() {
use anyfs_backend::{Layer, FsRead, FsWrite, FsDir, FsError, Metadata, ReadDirIter};
use std::path::Path;
use std::time::Instant;

pub struct LoggingLayer {
    prefix: String,
}

impl LoggingLayer {
    pub fn new(prefix: impl Into<String>) -> Self {
        Self { prefix: prefix.into() }
    }
}

pub struct LoggingFs<B> {
    inner: B,
    prefix: String,
}

impl<B> Layer<B> for LoggingLayer {
    type Wrapped = LoggingFs<B>;

    fn layer(self, inner: B) -> Self::Wrapped {
        LoggingFs {
            inner,
            prefix: self.prefix,
        }
    }
}
}

Logging FsRead

#![allow(unused)]
fn main() {
impl<B: FsRead> FsRead for LoggingFs<B> {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        let start = Instant::now();
        println!("[{}] read: {}", self.prefix, path.display());
        
        let result = self.inner.read(path);
        let elapsed = start.elapsed();
        
        match &result {
            Ok(data) => println!(
                "[{}] read: {} → {} bytes ({:?})",
                self.prefix, path.display(), data.len(), elapsed
            ),
            Err(e) => println!(
                "[{}] read: {} → ERROR: {} ({:?})",
                self.prefix, path.display(), e, elapsed
            ),
        }
        
        result
    }

    fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
        println!("[{}] metadata: {}", self.prefix, path.display());
        self.inner.metadata(path)
    }

    fn exists(&self, path: &Path) -> bool {
        let result = self.inner.exists(path);
        println!("[{}] exists: {} → {}", self.prefix, path.display(), result);
        result
    }
}
}

Logging FsWrite

#![allow(unused)]
fn main() {
impl<B: FsWrite> FsWrite for LoggingFs<B> {
    fn write(&self, path: &Path, content: &[u8]) -> Result<(), FsError> {
        println!(
            "[{}] write: {} ({} bytes)",
            self.prefix, path.display(), content.len()
        );
        
        let result = self.inner.write(path, content);
        
        match &result {
            Ok(()) => println!("[{}] write: {} → OK", self.prefix, path.display()),
            Err(e) => println!("[{}] write: {} → ERROR: {}", self.prefix, path.display(), e),
        }
        
        result
    }

    fn remove_file(&self, path: &Path) -> Result<(), FsError> {
        println!("[{}] remove_file: {}", self.prefix, path.display());
        self.inner.remove_file(path)
    }
}
}

Logging FsDir

#![allow(unused)]
fn main() {
impl<B: FsDir> FsDir for LoggingFs<B> {
    fn read_dir(&self, path: &Path) -> Result<ReadDirIter, FsError> {
        println!("[{}] read_dir: {}", self.prefix, path.display());
        self.inner.read_dir(path)
    }

    fn create_dir(&self, path: &Path) -> Result<(), FsError> {
        println!("[{}] create_dir: {}", self.prefix, path.display());
        self.inner.create_dir(path)
    }

    fn create_dir_all(&self, path: &Path) -> Result<(), FsError> {
        println!("[{}] create_dir_all: {}", self.prefix, path.display());
        self.inner.create_dir_all(path)
    }

    fn remove_dir(&self, path: &Path) -> Result<(), FsError> {
        println!("[{}] remove_dir: {}", self.prefix, path.display());
        self.inner.remove_dir(path)
    }

    fn remove_dir_all(&self, path: &Path) -> Result<(), FsError> {
        println!("[{}] remove_dir_all: {}", self.prefix, path.display());
        self.inner.remove_dir_all(path)
    }

    fn rename(&self, from: &Path, to: &Path) -> Result<(), FsError> {
        println!(
            "[{}] rename: {} → {}",
            self.prefix, from.display(), to.display()
        );
        self.inner.rename(from, to)
    }
}
}

Usage

#![allow(unused)]
fn main() {
use anyfs_backend::LayerExt;

let fs = InMemoryFs::new()
    .layer(LoggingLayer::new("FS"));

fs.create_dir(Path::new("/docs")).unwrap();
fs.write(Path::new("/docs/readme.txt"), b"Hello").unwrap();
let _ = fs.read(Path::new("/docs/readme.txt")).unwrap();
}

Output:

[FS] create_dir: /docs
[FS] write: /docs/readme.txt (5 bytes)
[FS] write: /docs/readme.txt → OK
[FS] read: /docs/readme.txt
[FS] read: /docs/readme.txt → 5 bytes (45µs)

Variations

Log Level Support

#![allow(unused)]
fn main() {
pub struct LoggingLayer {
    prefix: String,
    level: LogLevel,
}

pub enum LogLevel {
    Debug,
    Info,
    Warn,
}

impl<B: FsRead> FsRead for LoggingFs<B> {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        if self.level <= LogLevel::Debug {
            println!("[{}] read: {}", self.prefix, path.display());
        }
        // ...
    }
}
}

Log to File/Custom Logger

#![allow(unused)]
fn main() {
use std::sync::Arc;

pub trait Logger: Send + Sync {
    fn log(&self, message: &str);
}

pub struct LoggingLayer<L> {
    logger: Arc<L>,
}

pub struct LoggingFs<B, L> {
    inner: B,
    logger: Arc<L>,
}
}

Filter by Path

#![allow(unused)]
fn main() {
pub struct LoggingLayer {
    prefix: String,
    filter: Option<PathBuf>,  // Only log operations under this path
}
}

Key Points

  1. Log before and after - Shows timing and results
  2. Include context - Path, size, duration
  3. Log errors - Don’t suppress, just log and forward
  4. Configurable - Prefix, level, filter

Next: Metrics Layer →

Metrics Layer

A metrics layer collects statistics about filesystem usage:

  • Operation counts
  • Bytes read/written
  • Error counts
  • Latency histograms

Design

Use atomic counters for thread safety:

#![allow(unused)]
fn main() {
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;

#[derive(Debug, Default)]
pub struct Metrics {
    pub reads: AtomicU64,
    pub writes: AtomicU64,
    pub bytes_read: AtomicU64,
    pub bytes_written: AtomicU64,
    pub errors: AtomicU64,
}

impl Metrics {
    pub fn new() -> Arc<Self> {
        Arc::new(Self::default())
    }

    pub fn summary(&self) -> String {
        format!(
            "reads={}, writes={}, bytes_read={}, bytes_written={}, errors={}",
            self.reads.load(Ordering::Relaxed),
            self.writes.load(Ordering::Relaxed),
            self.bytes_read.load(Ordering::Relaxed),
            self.bytes_written.load(Ordering::Relaxed),
            self.errors.load(Ordering::Relaxed),
        )
    }
}
}

Implementation

#![allow(unused)]
fn main() {
use anyfs_backend::{Layer, FsRead, FsWrite, FsDir, FsError, Metadata, ReadDirIter};
use std::path::Path;

pub struct MetricsLayer {
    metrics: Arc<Metrics>,
}

impl MetricsLayer {
    pub fn new(metrics: Arc<Metrics>) -> Self {
        Self { metrics }
    }
}

pub struct MetricsFs<B> {
    inner: B,
    metrics: Arc<Metrics>,
}

impl<B> Layer<B> for MetricsLayer {
    type Wrapped = MetricsFs<B>;

    fn layer(self, inner: B) -> Self::Wrapped {
        MetricsFs {
            inner,
            metrics: self.metrics,
        }
    }
}
}

Counting FsRead

#![allow(unused)]
fn main() {
impl<B: FsRead> FsRead for MetricsFs<B> {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        self.metrics.reads.fetch_add(1, Ordering::Relaxed);
        
        match self.inner.read(path) {
            Ok(data) => {
                self.metrics.bytes_read
                    .fetch_add(data.len() as u64, Ordering::Relaxed);
                Ok(data)
            }
            Err(e) => {
                self.metrics.errors.fetch_add(1, Ordering::Relaxed);
                Err(e)
            }
        }
    }

    fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
        self.inner.metadata(path)
    }

    fn exists(&self, path: &Path) -> bool {
        self.inner.exists(path)
    }
}
}

Counting FsWrite

#![allow(unused)]
fn main() {
impl<B: FsWrite> FsWrite for MetricsFs<B> {
    fn write(&self, path: &Path, content: &[u8]) -> Result<(), FsError> {
        self.metrics.writes.fetch_add(1, Ordering::Relaxed);
        
        match self.inner.write(path, content) {
            Ok(()) => {
                self.metrics.bytes_written
                    .fetch_add(content.len() as u64, Ordering::Relaxed);
                Ok(())
            }
            Err(e) => {
                self.metrics.errors.fetch_add(1, Ordering::Relaxed);
                Err(e)
            }
        }
    }

    fn remove_file(&self, path: &Path) -> Result<(), FsError> {
        self.inner.remove_file(path)
    }
}
}

Forwarding FsDir

#![allow(unused)]
fn main() {
impl<B: FsDir> FsDir for MetricsFs<B> {
    fn read_dir(&self, path: &Path) -> Result<ReadDirIter, FsError> {
        self.inner.read_dir(path)
    }

    fn create_dir(&self, path: &Path) -> Result<(), FsError> {
        self.inner.create_dir(path)
    }

    fn create_dir_all(&self, path: &Path) -> Result<(), FsError> {
        self.inner.create_dir_all(path)
    }

    fn remove_dir(&self, path: &Path) -> Result<(), FsError> {
        self.inner.remove_dir(path)
    }

    fn remove_dir_all(&self, path: &Path) -> Result<(), FsError> {
        self.inner.remove_dir_all(path)
    }

    fn rename(&self, from: &Path, to: &Path) -> Result<(), FsError> {
        self.inner.rename(from, to)
    }
}
}

Usage

#![allow(unused)]
fn main() {
use anyfs_backend::LayerExt;

// Create shared metrics
let metrics = Metrics::new();

let fs = InMemoryFs::new()
    .layer(MetricsLayer::new(metrics.clone()));

// Use the filesystem
fs.write(Path::new("/a.txt"), b"Hello").unwrap();
fs.write(Path::new("/b.txt"), b"World!").unwrap();
fs.read(Path::new("/a.txt")).unwrap();
fs.read(Path::new("/b.txt")).unwrap();
fs.read(Path::new("/a.txt")).unwrap();

// Check metrics
println!("{}", metrics.summary());
// Output: reads=3, writes=2, bytes_read=16, bytes_written=11, errors=0
}

Advanced Metrics

Latency Tracking

#![allow(unused)]
fn main() {
use std::time::{Duration, Instant};
use std::sync::RwLock;

pub struct DetailedMetrics {
    pub reads: AtomicU64,
    pub read_latencies: RwLock<Vec<Duration>>,  // For percentile calculations
}

impl<B: FsRead> FsRead for MetricsFs<B> {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        let start = Instant::now();
        let result = self.inner.read(path);
        let elapsed = start.elapsed();
        
        self.metrics.reads.fetch_add(1, Ordering::Relaxed);
        self.metrics.read_latencies.write().unwrap().push(elapsed);
        
        result
    }
}
}

Per-Path Metrics

#![allow(unused)]
fn main() {
use std::collections::HashMap;

pub struct PathMetrics {
    pub by_path: RwLock<HashMap<PathBuf, u64>>,
}

impl<B: FsRead> FsRead for PathMetricsFs<B> {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        let result = self.inner.read(path);
        
        if result.is_ok() {
            let mut by_path = self.metrics.by_path.write().unwrap();
            *by_path.entry(path.to_path_buf()).or_default() += 1;
        }
        
        result
    }
}
}

Prometheus/OpenTelemetry Integration

#![allow(unused)]
fn main() {
// Pseudo-code for real metrics systems
pub struct PrometheusMetrics {
    reads: prometheus::Counter,
    bytes_read: prometheus::Counter,
    read_duration: prometheus::Histogram,
}

impl<B: FsRead> FsRead for PrometheusFs<B> {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        let timer = self.metrics.read_duration.start_timer();
        let result = self.inner.read(path);
        timer.observe_duration();
        
        self.metrics.reads.inc();
        if let Ok(data) = &result {
            self.metrics.bytes_read.inc_by(data.len() as f64);
        }
        
        result
    }
}
}

Key Points

  1. Use atomics for thread-safe counting
  2. Share metrics via Arc to read from outside
  3. Count before/after for accurate error tracking
  4. Consider scope - global vs per-path vs per-operation

Next: Caching Layer →

Caching Layer

A caching layer stores read results to avoid repeated backend access. Essential for:

  • Remote backends (S3, network filesystems)
  • Slow storage
  • Reducing API calls/costs

Design Decisions

  1. What to cache: File contents, metadata, or both
  2. TTL: How long entries remain valid
  3. Invalidation: When to remove stale entries
  4. Size limits: Maximum cache size (LRU eviction)

Basic Implementation

#![allow(unused)]
fn main() {
use anyfs_backend::{Layer, FsRead, FsWrite, FsDir, FsError, Metadata, ReadDirIter};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::RwLock;
use std::time::{Duration, Instant};

pub struct CachingLayer {
    ttl: Duration,
}

impl CachingLayer {
    pub fn new(ttl: Duration) -> Self {
        Self { ttl }
    }
}

struct CacheEntry {
    data: Vec<u8>,
    expires_at: Instant,
}

pub struct CachingFs<B> {
    inner: B,
    cache: RwLock<HashMap<PathBuf, CacheEntry>>,
    ttl: Duration,
}

impl<B> Layer<B> for CachingLayer {
    type Wrapped = CachingFs<B>;

    fn layer(self, inner: B) -> Self::Wrapped {
        CachingFs {
            inner,
            cache: RwLock::new(HashMap::new()),
            ttl: self.ttl,
        }
    }
}
}

Caching Reads

#![allow(unused)]
fn main() {
impl<B: FsRead> FsRead for CachingFs<B> {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        let path_buf = path.to_path_buf();

        // Check cache first
        {
            let cache = self.cache.read().unwrap();
            if let Some(entry) = cache.get(&path_buf) {
                if entry.expires_at > Instant::now() {
                    return Ok(entry.data.clone());  // Cache hit!
                }
            }
        }

        // Cache miss - read from backend
        let data = self.inner.read(path)?;

        // Store in cache
        {
            let mut cache = self.cache.write().unwrap();
            cache.insert(path_buf, CacheEntry {
                data: data.clone(),
                expires_at: Instant::now() + self.ttl,
            });
        }

        Ok(data)
    }

    fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
        // Could cache metadata too
        self.inner.metadata(path)
    }

    fn exists(&self, path: &Path) -> bool {
        self.inner.exists(path)
    }
}
}

Invalidating on Write

Critical: Writes must invalidate cached entries!

#![allow(unused)]
fn main() {
impl<B: FsWrite> FsWrite for CachingFs<B> {
    fn write(&self, path: &Path, content: &[u8]) -> Result<(), FsError> {
        // Invalidate cache entry BEFORE writing
        {
            let mut cache = self.cache.write().unwrap();
            cache.remove(path);
        }

        self.inner.write(path, content)
    }

    fn remove_file(&self, path: &Path) -> Result<(), FsError> {
        {
            let mut cache = self.cache.write().unwrap();
            cache.remove(path);
        }

        self.inner.remove_file(path)
    }
}
}

Handling Directory Operations

#![allow(unused)]
fn main() {
impl<B: FsDir> FsDir for CachingFs<B> {
    fn read_dir(&self, path: &Path) -> Result<ReadDirIter, FsError> {
        self.inner.read_dir(path)
    }

    fn create_dir(&self, path: &Path) -> Result<(), FsError> {
        self.inner.create_dir(path)
    }

    fn create_dir_all(&self, path: &Path) -> Result<(), FsError> {
        self.inner.create_dir_all(path)
    }

    fn remove_dir(&self, path: &Path) -> Result<(), FsError> {
        self.inner.remove_dir(path)
    }

    fn remove_dir_all(&self, path: &Path) -> Result<(), FsError> {
        // Invalidate ALL entries under this path
        {
            let mut cache = self.cache.write().unwrap();
            cache.retain(|p, _| !p.starts_with(path));
        }

        self.inner.remove_dir_all(path)
    }

    fn rename(&self, from: &Path, to: &Path) -> Result<(), FsError> {
        // Invalidate both paths
        {
            let mut cache = self.cache.write().unwrap();
            cache.remove(from);
            cache.remove(to);
        }

        self.inner.rename(from, to)
    }
}
}

Usage

#![allow(unused)]
fn main() {
use anyfs_backend::LayerExt;
use std::time::Duration;

let fs = InMemoryFs::new()
    .layer(CachingLayer::new(Duration::from_secs(60)));

// First read - cache miss, reads from backend
let data1 = fs.read(Path::new("/file.txt")).unwrap();

// Second read - cache hit, returns cached data
let data2 = fs.read(Path::new("/file.txt")).unwrap();

// After write, cache is invalidated
fs.write(Path::new("/file.txt"), b"new content").unwrap();

// This read fetches fresh data
let data3 = fs.read(Path::new("/file.txt")).unwrap();
}

Advanced Features

LRU Eviction

#![allow(unused)]
fn main() {
use std::collections::VecDeque;

struct LruCache {
    entries: HashMap<PathBuf, CacheEntry>,
    order: VecDeque<PathBuf>,  // Oldest first
    max_size: usize,
}

impl LruCache {
    fn insert(&mut self, path: PathBuf, data: Vec<u8>, ttl: Duration) {
        // Evict if at capacity
        while self.entries.len() >= self.max_size {
            if let Some(oldest) = self.order.pop_front() {
                self.entries.remove(&oldest);
            }
        }
        
        self.entries.insert(path.clone(), CacheEntry {
            data,
            expires_at: Instant::now() + ttl,
        });
        self.order.push_back(path);
    }
}
}

Size-Limited Cache

#![allow(unused)]
fn main() {
struct SizeAwareCache {
    entries: HashMap<PathBuf, CacheEntry>,
    total_bytes: usize,
    max_bytes: usize,
}

impl SizeAwareCache {
    fn insert(&mut self, path: PathBuf, data: Vec<u8>, ttl: Duration) {
        let size = data.len();
        
        // Don't cache if too large
        if size > self.max_bytes / 10 {
            return;
        }
        
        // Evict until space available
        while self.total_bytes + size > self.max_bytes {
            // Evict oldest/smallest/least-used
        }
        
        self.total_bytes += size;
        self.entries.insert(path, CacheEntry { data, expires_at: ... });
    }
}
}

Negative Caching

Cache “not found” results to avoid repeated lookups:

#![allow(unused)]
fn main() {
enum CachedResult {
    Found(Vec<u8>),
    NotFound,
}

fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
    if let Some(cached) = self.get_cached(path) {
        match cached {
            CachedResult::Found(data) => return Ok(data),
            CachedResult::NotFound => return Err(FsError::NotFound { ... }),
        }
    }
    
    match self.inner.read(path) {
        Ok(data) => {
            self.cache(path, CachedResult::Found(data.clone()));
            Ok(data)
        }
        Err(FsError::NotFound { .. }) => {
            self.cache(path, CachedResult::NotFound);
            Err(FsError::NotFound { path: path.to_path_buf() })
        }
        Err(e) => Err(e),
    }
}
}

Cache Consistency

Strong Consistency

Invalidate on every write operation. Safe but may miss external changes.

Eventual Consistency

Use short TTLs. Allows stale reads but simpler.

Write-Through

Update cache on write instead of invalidating:

#![allow(unused)]
fn main() {
fn write(&self, path: &Path, content: &[u8]) -> Result<(), FsError> {
    self.inner.write(path, content)?;
    
    // Update cache with new content
    let mut cache = self.cache.write().unwrap();
    cache.insert(path.to_path_buf(), CacheEntry {
        data: content.to_vec(),
        expires_at: Instant::now() + self.ttl,
    });
    
    Ok(())
}
}

Key Points

  1. Always invalidate on writes - Stale cache is worse than no cache
  2. Consider TTL carefully - Too long = stale data, too short = no benefit
  3. Handle directory operations - remove_dir_all affects many paths
  4. Memory limits - Unbounded caches can cause OOM

Next: Access Control Layer →

Access Control Layer

An access control layer restricts operations based on rules:

  • Read-only mode
  • Path restrictions
  • User-based permissions

Design

#![allow(unused)]
fn main() {
pub enum AccessRule {
    /// Deny all write operations
    ReadOnly,
    
    /// Only allow operations under a specific path
    RestrictToPath(PathBuf),
    
    /// Custom rule function
    Custom(Box<dyn Fn(&Path, Operation) -> bool + Send + Sync>),
}

pub enum Operation {
    Read,
    Write,
    Delete,
    List,
    Create,
}
}

Read-Only Implementation

The simplest access control: block all writes.

#![allow(unused)]
fn main() {
use anyfs_backend::{Layer, FsRead, FsWrite, FsDir, FsError, Metadata, ReadDirIter};
use std::path::Path;

pub struct ReadOnlyLayer;

pub struct ReadOnlyFs<B> {
    inner: B,
}

impl<B> Layer<B> for ReadOnlyLayer {
    type Wrapped = ReadOnlyFs<B>;

    fn layer(self, inner: B) -> Self::Wrapped {
        ReadOnlyFs { inner }
    }
}

// Forward all reads unchanged
impl<B: FsRead> FsRead for ReadOnlyFs<B> {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        self.inner.read(path)
    }

    fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
        self.inner.metadata(path)
    }

    fn exists(&self, path: &Path) -> bool {
        self.inner.exists(path)
    }
}

// Block all writes
impl<B: FsWrite> FsWrite for ReadOnlyFs<B> {
    fn write(&self, path: &Path, _content: &[u8]) -> Result<(), FsError> {
        Err(FsError::PermissionDenied { path: path.to_path_buf() })
    }

    fn remove_file(&self, path: &Path) -> Result<(), FsError> {
        Err(FsError::PermissionDenied { path: path.to_path_buf() })
    }
}

impl<B: FsDir> FsDir for ReadOnlyFs<B> {
    fn read_dir(&self, path: &Path) -> Result<ReadDirIter, FsError> {
        self.inner.read_dir(path)  // Reading is allowed
    }

    fn create_dir(&self, path: &Path) -> Result<(), FsError> {
        Err(FsError::PermissionDenied { path: path.to_path_buf() })
    }

    fn create_dir_all(&self, path: &Path) -> Result<(), FsError> {
        Err(FsError::PermissionDenied { path: path.to_path_buf() })
    }

    fn remove_dir(&self, path: &Path) -> Result<(), FsError> {
        Err(FsError::PermissionDenied { path: path.to_path_buf() })
    }

    fn remove_dir_all(&self, path: &Path) -> Result<(), FsError> {
        Err(FsError::PermissionDenied { path: path.to_path_buf() })
    }

    fn rename(&self, from: &Path, _to: &Path) -> Result<(), FsError> {
        Err(FsError::PermissionDenied { path: from.to_path_buf() })
    }
}
}

Path-Restricted Implementation

Only allow access under certain paths:

#![allow(unused)]
fn main() {
use std::path::PathBuf;

pub struct PathRestrictedLayer {
    allowed_paths: Vec<PathBuf>,
}

impl PathRestrictedLayer {
    pub fn new(paths: Vec<PathBuf>) -> Self {
        Self { allowed_paths: paths }
    }
    
    pub fn single(path: impl Into<PathBuf>) -> Self {
        Self { allowed_paths: vec![path.into()] }
    }
}

pub struct PathRestrictedFs<B> {
    inner: B,
    allowed_paths: Vec<PathBuf>,
}

impl<B> PathRestrictedFs<B> {
    fn check_path(&self, path: &Path) -> Result<(), FsError> {
        for allowed in &self.allowed_paths {
            if path.starts_with(allowed) {
                return Ok(());
            }
        }
        Err(FsError::PermissionDenied { path: path.to_path_buf() })
    }
}

impl<B> Layer<B> for PathRestrictedLayer {
    type Wrapped = PathRestrictedFs<B>;

    fn layer(self, inner: B) -> Self::Wrapped {
        PathRestrictedFs {
            inner,
            allowed_paths: self.allowed_paths,
        }
    }
}

impl<B: FsRead> FsRead for PathRestrictedFs<B> {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        self.check_path(path)?;
        self.inner.read(path)
    }

    fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
        self.check_path(path)?;
        self.inner.metadata(path)
    }

    fn exists(&self, path: &Path) -> bool {
        if self.check_path(path).is_err() {
            return false;  // Pretend it doesn't exist
        }
        self.inner.exists(path)
    }
}

impl<B: FsWrite> FsWrite for PathRestrictedFs<B> {
    fn write(&self, path: &Path, content: &[u8]) -> Result<(), FsError> {
        self.check_path(path)?;
        self.inner.write(path, content)
    }

    fn remove_file(&self, path: &Path) -> Result<(), FsError> {
        self.check_path(path)?;
        self.inner.remove_file(path)
    }
}

// FsDir implementation similar...
}

Usage

Read-Only Mode

#![allow(unused)]
fn main() {
use anyfs_backend::LayerExt;

// Populate the filesystem first
let fs = InMemoryFs::new();
fs.create_dir(Path::new("/data")).unwrap();
fs.write(Path::new("/data/file.txt"), b"content").unwrap();

// Now make it read-only
let fs = fs.layer(ReadOnlyLayer);

// Reading works
let data = fs.read(Path::new("/data/file.txt")).unwrap();

// Writing fails
match fs.write(Path::new("/data/new.txt"), b"test") {
    Err(FsError::PermissionDenied { .. }) => println!("Blocked!"),
    _ => panic!("Should have been blocked"),
}
}

Path Restriction

#![allow(unused)]
fn main() {
let fs = InMemoryFs::new();
fs.create_dir_all(Path::new("/allowed/subdir")).unwrap();
fs.create_dir(Path::new("/forbidden")).unwrap();
fs.write(Path::new("/allowed/file.txt"), b"ok").unwrap();
fs.write(Path::new("/forbidden/secret.txt"), b"hidden").unwrap();

// Restrict to /allowed only
let fs = fs.layer(PathRestrictedLayer::single("/allowed"));

// This works
let data = fs.read(Path::new("/allowed/file.txt")).unwrap();

// This fails
match fs.read(Path::new("/forbidden/secret.txt")) {
    Err(FsError::PermissionDenied { .. }) => println!("Access denied!"),
    _ => panic!("Should have been denied"),
}
}

Advanced: Custom Rules

#![allow(unused)]
fn main() {
pub struct CustomAccessLayer<F> {
    checker: F,
}

impl<F> CustomAccessLayer<F>
where
    F: Fn(&Path, Operation) -> bool + Send + Sync + Clone,
{
    pub fn new(checker: F) -> Self {
        Self { checker }
    }
}

// Example: Allow reads anywhere, writes only to /tmp
let fs = backend.layer(CustomAccessLayer::new(|path, op| {
    match op {
        Operation::Read | Operation::List => true,
        Operation::Write | Operation::Create | Operation::Delete => {
            path.starts_with("/tmp")
        }
    }
}));
}

Combining with Other Layers

Access control should usually be the outermost layer:

#![allow(unused)]
fn main() {
let fs = InMemoryFs::new()
    .layer(CachingLayer::new(Duration::from_secs(60)))
    .layer(MetricsLayer::new(metrics.clone()))
    .layer(LoggingLayer::new("FS"))
    .layer(ReadOnlyLayer);  // Outermost - checked first

// Flow: ReadOnly -> Logging -> Metrics -> Caching -> Backend
}

This way:

  1. Access control rejects unauthorized requests immediately
  2. Logging sees the rejection
  3. Metrics don’t count blocked requests (if desired)

Key Points

  1. Return PermissionDenied for blocked operations
  2. Check early - Don’t do work before validating access
  3. Consider exists() - Should forbidden paths appear to not exist?
  4. Layer order matters - Put access control outermost

Next: Composing Layers →

Composing Layers

The power of layers comes from composition. Stack multiple layers to combine functionality.

Basic Composition

#![allow(unused)]
fn main() {
use anyfs_backend::LayerExt;

let fs = InMemoryFs::new()
    .layer(CachingLayer::new(Duration::from_secs(60)))
    .layer(MetricsLayer::new(metrics.clone()))
    .layer(LoggingLayer::new("FS"));
}

Understanding Layer Order

When you compose layers:

#![allow(unused)]
fn main() {
backend.layer(A).layer(B).layer(C)
}

The result is: C wraps B wraps A wraps backend

Request → C → B → A → Backend → A → B → C → Response
          ↓   ↓   ↓            ↑   ↑   ↑
        (1) (2) (3)          (3) (2) (1)

Outer layers see requests first and responses last.

Order Matters!

Example: Logging + Caching

#![allow(unused)]
fn main() {
// Option 1: Logging outside Caching
let fs = backend
    .layer(CachingLayer::new(ttl))
    .layer(LoggingLayer::new("FS"));

// Log shows: read /file.txt (even for cache hits)
// Because logging is outside, it sees ALL requests
}
#![allow(unused)]
fn main() {
// Option 2: Caching outside Logging
let fs = backend
    .layer(LoggingLayer::new("FS"))
    .layer(CachingLayer::new(ttl));

// Log shows: read /file.txt (only for cache misses)
// Because cache handles request before it reaches logging
}

Example: Metrics + Caching

#![allow(unused)]
fn main() {
// Metrics outside Caching - counts ALL reads
let fs = backend
    .layer(CachingLayer::new(ttl))
    .layer(MetricsLayer::new(m.clone()));
// metrics.reads = 100 (total requests)

// Caching outside Metrics - counts only cache misses
let fs = backend
    .layer(MetricsLayer::new(m.clone()))
    .layer(CachingLayer::new(ttl));
// metrics.reads = 10 (backend hits only)
}

From innermost to outermost:

#![allow(unused)]
fn main() {
let fs = backend
    // 1. Transformations (encryption, compression)
    .layer(EncryptionLayer::new(key))
    
    // 2. Caching (after transformation)
    .layer(CachingLayer::new(Duration::from_secs(60)))
    
    // 3. Retry/resilience
    .layer(RetryLayer::new(3))
    
    // 4. Metrics (count actual operations)
    .layer(MetricsLayer::new(metrics.clone()))
    
    // 5. Logging (see everything)
    .layer(LoggingLayer::new("FS"))
    
    // 6. Access control (reject early)
    .layer(AccessControlLayer::new(rules));
}

Reasoning:

  • Encryption must wrap raw backend to encrypt all data
  • Caching stores encrypted data (or plaintext, depending on requirements)
  • Retry retries failed operations
  • Metrics count operations that reach this point
  • Logging logs everything including rejections
  • Access control rejects unauthorized requests immediately

Type Complexity

Each layer adds a wrapper type:

#![allow(unused)]
fn main() {
let fs: LoggingFs<MetricsFs<CachingFs<InMemoryFs>>> = ...;
}

This can get unwieldy. Solutions:

1. Type Alias

#![allow(unused)]
fn main() {
type MyFs = LoggingFs<MetricsFs<CachingFs<InMemoryFs>>>;

fn create_fs() -> MyFs {
    InMemoryFs::new()
        .layer(CachingLayer::new(ttl))
        .layer(MetricsLayer::new(metrics))
        .layer(LoggingLayer::new("FS"))
}
}

2. Box with dyn Fs

#![allow(unused)]
fn main() {
fn create_fs() -> Box<dyn Fs> {
    let fs = InMemoryFs::new()
        .layer(CachingLayer::new(ttl))
        .layer(MetricsLayer::new(metrics))
        .layer(LoggingLayer::new("FS"));
    
    Box::new(fs)
}
}

3. impl Trait

#![allow(unused)]
fn main() {
fn create_fs() -> impl Fs {
    InMemoryFs::new()
        .layer(CachingLayer::new(ttl))
        .layer(MetricsLayer::new(metrics))
        .layer(LoggingLayer::new("FS"))
}
}

Runtime Composition

For dynamic layer selection:

#![allow(unused)]
fn main() {
fn create_fs(config: &Config) -> Box<dyn Fs> {
    let mut fs: Box<dyn Fs> = Box::new(InMemoryFs::new());
    
    if config.enable_caching {
        fs = Box::new(CachingLayer::new(config.cache_ttl).layer(fs));
    }
    
    if config.enable_logging {
        fs = Box::new(LoggingLayer::new(&config.log_prefix).layer(fs));
    }
    
    if config.read_only {
        fs = Box::new(ReadOnlyLayer.layer(fs));
    }
    
    fs
}
}

Note: This requires layers to work with Box<dyn Fs>, which means implementing traits for the boxed type.

Complete Example

use anyfs_backend::{Fs, LayerExt};
use std::path::Path;
use std::time::Duration;

fn main() {
    // Create shared metrics
    let metrics = Metrics::new();
    
    // Build the layered filesystem
    let fs = InMemoryFs::new()
        .layer(CachingLayer::new(Duration::from_secs(60)))
        .layer(MetricsLayer::new(metrics.clone()))
        .layer(LoggingLayer::new("APP"));
    
    // Setup
    fs.create_dir(Path::new("/data")).unwrap();
    fs.write(Path::new("/data/config.json"), b"{}").unwrap();
    
    // Multiple reads - watch cache behavior
    for i in 0..5 {
        let _ = fs.read(Path::new("/data/config.json"));
        println!("After read {}: {}", i + 1, metrics.summary());
    }
    
    // Output shows:
    // - All 5 reads logged (logging is outermost)
    // - Only 1 read in metrics (cache handles the rest)
}

Testing Layered Systems

#![allow(unused)]
fn main() {
#[test]
fn test_layer_composition() {
    let metrics = Metrics::new();
    
    let fs = InMemoryFs::new()
        .layer(CachingLayer::new(Duration::from_secs(60)))
        .layer(MetricsLayer::new(metrics.clone()));
    
    fs.write(Path::new("/test.txt"), b"data").unwrap();
    
    // First read - cache miss, hits metrics
    fs.read(Path::new("/test.txt")).unwrap();
    assert_eq!(metrics.reads.load(Ordering::Relaxed), 1);
    
    // Second read - cache hit, doesn't hit metrics
    fs.read(Path::new("/test.txt")).unwrap();
    assert_eq!(metrics.reads.load(Ordering::Relaxed), 1);  // Still 1!
    
    // Write invalidates cache
    fs.write(Path::new("/test.txt"), b"new").unwrap();
    
    // Next read - cache miss again
    fs.read(Path::new("/test.txt")).unwrap();
    assert_eq!(metrics.reads.load(Ordering::Relaxed), 2);
}
}

Summary

  1. Compose with .layer() - Clean, fluent API
  2. Order matters - Outer layers see requests first
  3. Think about what each layer should see - Metrics before or after cache?
  4. Use type aliases or impl Trait - Manage type complexity
  5. Test the composition - Verify layers interact correctly

🎉 Congratulations! You now know how to:

  • Create middleware layers
  • Log, measure, cache, and control access
  • Compose layers for powerful, reusable functionality

Go build something awesome!

Error Types

AnyFS defines a unified error type for all filesystem operations.

FsError

The core error type used by all traits:

#![allow(unused)]
fn main() {
use anyfs_backend::FsError;
use std::path::Path;

fn handle_error(e: FsError) {
    match e {
        FsError::NotFound { path, operation } => {
            println!("{} not found during {}", path.display(), operation);
        }
        FsError::AlreadyExists { path, operation } => {
            println!("{} already exists during {}", path.display(), operation);
        }
        FsError::PermissionDenied { path, operation } => {
            println!("Permission denied: {} during {}", path.display(), operation);
        }
        FsError::Io { source, path, operation } => {
            println!("IO error on {}: {} during {}", 
                path.map(|p| p.display().to_string()).unwrap_or_default(),
                source, operation);
        }
        // ... handle other variants
    }
}
}

Error Variants

VariantWhen UsedRequired Fields
NotFoundFile or directory doesn’t existpath, operation
AlreadyExistsCreating something that existspath, operation
PermissionDeniedAccess not allowedpath, operation
IsDirectoryExpected file, got directorypath, operation
NotDirectoryExpected directory, got filepath, operation
DirectoryNotEmptyRemoving non-empty directorypath, operation
InvalidPathMalformed pathpath, operation, reason
TooManySymlinksSymlink loop detectedpath, operation
ReadOnlyWrite on read-only filesystempath, operation
CrossDeviceCross-filesystem operationsource, destination, operation
IoGeneral I/O errorsource, path (optional), operation
OtherUnclassified errorsmessage, operation

Creating Errors

Use the constructor methods for clean error creation:

#![allow(unused)]
fn main() {
use anyfs_backend::FsError;
use std::path::Path;

// NotFound
let err = FsError::not_found(Path::new("/missing.txt"), "read");

// AlreadyExists
let err = FsError::already_exists(Path::new("/exists"), "create_dir");

// PermissionDenied
let err = FsError::permission_denied(Path::new("/secret"), "read");

// IsDirectory
let err = FsError::is_directory(Path::new("/folder"), "read");

// NotDirectory
let err = FsError::not_directory(Path::new("/file.txt"), "read_dir");

// DirectoryNotEmpty
let err = FsError::directory_not_empty(Path::new("/folder"), "remove_dir");

// InvalidPath
let err = FsError::invalid_path(
    Path::new("/bad\0path"),
    "contains null byte",
    "open"
);

// ReadOnly
let err = FsError::read_only(Path::new("/file.txt"), "write");

// IO error
let err = FsError::io(
    std::io::Error::new(std::io::ErrorKind::Other, "disk full"),
    Some(Path::new("/file.txt")),
    "write"
);
}

Error Conversion

From std::io::Error

#![allow(unused)]
fn main() {
use anyfs_backend::FsError;
use std::io;

fn from_io_error(e: io::Error, path: &Path, op: &str) -> FsError {
    FsError::io(e, Some(path), op)
}

// Or use From trait (without path context)
let io_err = io::Error::new(io::ErrorKind::NotFound, "not found");
let fs_err: FsError = io_err.into();
}

To std::io::Error

#![allow(unused)]
fn main() {
use anyfs_backend::FsError;
use std::io;

let fs_err = FsError::not_found(Path::new("/missing"), "read");
let io_err: io::Error = fs_err.into();

assert_eq!(io_err.kind(), io::ErrorKind::NotFound);
}

Error Display

All errors implement Display with helpful messages:

#![allow(unused)]
fn main() {
let err = FsError::not_found(Path::new("/file.txt"), "read");
println!("{}", err);
// Output: not found: /file.txt (during read)

let err = FsError::permission_denied(Path::new("/secret"), "open");
println!("{}", err);
// Output: permission denied: /secret (during open)
}

Best Practices

1. Always Include Operation Context

#![allow(unused)]
fn main() {
// ✓ Good - includes operation
FsError::not_found(path, "read")

// ✗ Bad - no context
FsError::NotFound { path: path.into(), operation: String::new() }
}

2. Include Path When Available

#![allow(unused)]
fn main() {
// ✓ Good - includes path
FsError::io(e, Some(path), "write")

// ✗ Avoid - loses context
FsError::io(e, None, "write")
}

3. Use Specific Error Types

#![allow(unused)]
fn main() {
// ✓ Good - specific error
if path.exists() {
    return Err(FsError::already_exists(path, "create"));
}

// ✗ Avoid - generic error
return Err(FsError::other("file exists", "create"));
}

4. Pattern Match for Handling

#![allow(unused)]
fn main() {
match fs.read(path) {
    Ok(data) => process(data),
    Err(FsError::NotFound { .. }) => create_default(),
    Err(FsError::PermissionDenied { .. }) => request_access(),
    Err(e) => return Err(e),
}
}

Core Types

This reference documents the core types used throughout AnyFS.

Metadata

File and directory metadata:

#![allow(unused)]
fn main() {
use anyfs_backend::Metadata;

let meta = fs.metadata(path)?;

// Check type
if meta.is_file() {
    println!("Size: {} bytes", meta.len());
} else if meta.is_dir() {
    println!("Directory");
} else if meta.is_symlink() {
    println!("Symlink");
}

// Timestamps (optional - may be None for some backends)
if let Some(created) = meta.created() {
    println!("Created: {:?}", created);
}
if let Some(modified) = meta.modified() {
    println!("Modified: {:?}", modified);
}
if let Some(accessed) = meta.accessed() {
    println!("Accessed: {:?}", accessed);
}

// Permissions (if available)
if let Some(perms) = meta.permissions() {
    println!("Readonly: {}", perms.readonly());
}
}

Metadata Fields

FieldTypeDescription
file_typeFileTypeFile, directory, or symlink
lenu64Size in bytes (0 for directories)
createdOption<SystemTime>Creation time
modifiedOption<SystemTime>Last modification
accessedOption<SystemTime>Last access
permissionsOption<Permissions>Permission info

Creating Metadata

For backend implementations:

#![allow(unused)]
fn main() {
use anyfs_backend::{Metadata, FileType, Permissions};
use std::time::SystemTime;

// File metadata
let meta = Metadata::file(1024)
    .with_created(SystemTime::now())
    .with_modified(SystemTime::now())
    .with_permissions(Permissions::readonly(false));

// Directory metadata
let meta = Metadata::dir()
    .with_modified(SystemTime::now());

// Symlink metadata
let meta = Metadata::symlink()
    .with_modified(SystemTime::now());
}

FileType

Enumeration of filesystem entry types:

#![allow(unused)]
fn main() {
use anyfs_backend::FileType;

let ft = metadata.file_type();

match ft {
    FileType::File => println!("Regular file"),
    FileType::Dir => println!("Directory"),
    FileType::Symlink => println!("Symbolic link"),
}

// Convenience methods
assert!(FileType::File.is_file());
assert!(FileType::Dir.is_dir());
assert!(FileType::Symlink.is_symlink());
}

DirEntry

Entry returned when reading directories:

#![allow(unused)]
fn main() {
use anyfs_backend::DirEntry;

for entry in fs.read_dir(path)? {
    let entry = entry?;
    
    // Name of the entry (not full path)
    println!("Name: {}", entry.name());
    
    // Full path
    println!("Path: {}", entry.path().display());
    
    // Type (if available without extra syscall)
    if let Some(ft) = entry.file_type() {
        println!("Type: {:?}", ft);
    }
    
    // Full metadata (may require extra syscall)
    let meta = entry.metadata()?;
    println!("Size: {}", meta.len());
}
}

DirEntry Fields

MethodReturn TypeDescription
name()&strEntry name (not path)
path()&PathFull path
file_type()Option<FileType>Type if known cheaply
metadata()Result<Metadata, FsError>Full metadata

Permissions

File permission information:

#![allow(unused)]
fn main() {
use anyfs_backend::Permissions;

// Create permissions
let perms = Permissions::readonly(false);  // read-write
let perms = Permissions::readonly(true);   // read-only

// Check permissions
if perms.readonly() {
    println!("File is read-only");
}

// POSIX mode (if supported)
#[cfg(unix)]
{
    let perms = Permissions::from_mode(0o755);
    println!("Mode: {:o}", perms.mode());
}
}

Extended Permissions (Unix)

For backends that support POSIX permissions:

#![allow(unused)]
fn main() {
use anyfs_backend::Permissions;

// From mode bits
let perms = Permissions::from_mode(0o644);

// Check mode
let mode = perms.mode();  // 0o644

// Permission bits
let owner_read = (mode & 0o400) != 0;
let owner_write = (mode & 0o200) != 0;
let owner_exec = (mode & 0o100) != 0;
}

OpenOptions

Options for opening files:

#![allow(unused)]
fn main() {
use anyfs_backend::OpenOptions;

// Read only (default)
let opts = OpenOptions::new().read(true);

// Write, create if missing
let opts = OpenOptions::new()
    .write(true)
    .create(true);

// Append mode
let opts = OpenOptions::new()
    .append(true)
    .create(true);

// Create new (fail if exists)
let opts = OpenOptions::new()
    .write(true)
    .create_new(true);

// Truncate existing
let opts = OpenOptions::new()
    .write(true)
    .truncate(true);
}

OpenOptions Fields

MethodDefaultDescription
read(bool)trueOpen for reading
write(bool)falseOpen for writing
append(bool)falseAppend to end
create(bool)falseCreate if missing
create_new(bool)falseCreate, fail if exists
truncate(bool)falseTruncate to zero length

SeekFrom

Position for seeking within files:

#![allow(unused)]
fn main() {
use std::io::SeekFrom;

// From start of file
let pos = SeekFrom::Start(100);

// From end of file (negative offset)
let pos = SeekFrom::End(-50);

// From current position
let pos = SeekFrom::Current(25);
}

Used with file handles:

#![allow(unused)]
fn main() {
use std::io::{Read, Seek, SeekFrom};

let mut handle = fs.open_read(path)?;

// Jump to offset 100
handle.seek(SeekFrom::Start(100))?;

// Read from there
let mut buf = [0u8; 50];
handle.read(&mut buf)?;
}

FileTimes

For setting file timestamps:

#![allow(unused)]
fn main() {
use anyfs_backend::FileTimes;
use std::time::SystemTime;

let times = FileTimes::new()
    .set_accessed(SystemTime::now())
    .set_modified(SystemTime::now());

fs.set_times(path, times)?;
}

FsStats

Filesystem statistics (capacity, usage):

#![allow(unused)]
fn main() {
use anyfs_backend::FsStats;

let stats: FsStats = fs.stats()?;

println!("Total: {} bytes", stats.total_bytes);
println!("Free: {} bytes", stats.free_bytes);
println!("Available: {} bytes", stats.available_bytes);
println!("Used: {}%", 
    (stats.total_bytes - stats.available_bytes) * 100 / stats.total_bytes
);
}

FsStats Fields

FieldTypeDescription
total_bytesu64Total capacity
free_bytesu64Free space
available_bytesu64Available to non-root
total_inodesOption<u64>Total inodes (Unix)
free_inodesOption<u64>Free inodes (Unix)

InodeId

Unique identifier for files (used by FsInode trait):

#![allow(unused)]
fn main() {
use anyfs_backend::InodeId;

let inode = fs.inode(path)?;
println!("Inode: {}", inode);

// Compare inodes to check if same file
let inode1 = fs.inode(path1)?;
let inode2 = fs.inode(path2)?;
if inode1 == inode2 {
    println!("Same file (hard links)");
}
}

Summary

TypePurpose
MetadataFile/directory attributes
FileTypeFile, Dir, or Symlink
DirEntryDirectory listing entry
PermissionsAccess permissions
OpenOptionsFile open configuration
FileTimesTimestamp modification
FsStatsFilesystem capacity
InodeIdUnique file identifier
FsErrorError handling (see Errors)