fs-transaction 0.2.0

Multi-file filesystem transactions that survive a crash: staged change sets, all-or-nothing apply, write-ahead recovery
Documentation
use std::io;
use std::path::{Path, PathBuf};

use crate::fs::{Capabilities, DirEntry, Durability, Metadata, ReadStorage, StdFs, Storage};

macro_rules! reads_like_stdfs {
    ($ty:ty) => {
        impl ReadStorage for $ty {
            async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
                StdFs.read(path).await
            }

            async fn read_to_string(&self, path: &Path) -> io::Result<String> {
                StdFs.read_to_string(path).await
            }

            async fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
                StdFs.read_dir(path).await
            }

            async fn metadata(&self, path: &Path) -> io::Result<Metadata> {
                StdFs.metadata(path).await
            }

            // Forwarded rather than defaulted: the defaults decline, and a
            // double that reads like `StdFs` must not claim to model less
            // than `StdFs` does — undo capture would believe it.
            async fn executable(&self, path: &Path) -> io::Result<Option<bool>> {
                StdFs.executable(path).await
            }

            async fn read_link(&self, path: &Path) -> io::Result<Option<std::path::PathBuf>> {
                StdFs.read_link(path).await
            }
        }
    };
}

#[derive(Debug)]
pub struct FailAtWrite {
    writes: std::cell::Cell<usize>,
    fail_at: usize,
}

impl FailAtWrite {
    pub fn nth(fail_at: usize) -> Self {
        Self {
            writes: std::cell::Cell::new(0),
            fail_at,
        }
    }
}

reads_like_stdfs!(FailAtWrite);

impl Storage for FailAtWrite {
    async fn write(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
        if crate::journal::Journal::default().owns_path(path) {
            return StdFs.write(path, contents).await;
        }
        let n = self.writes.get();
        self.writes.set(n + 1);
        if n == self.fail_at {
            return Err(io::Error::other("disk full (test)"));
        }
        StdFs.write(path, contents).await
    }

    // Counted against the same budget as `write` — a caller that stages its
    // bytes through either is spending the same kind of I/O, and a fault test
    // should not care which one the code under test happened to reach for.
    async fn create_new(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
        let n = self.writes.get();
        self.writes.set(n + 1);
        if n == self.fail_at {
            return Err(io::Error::other("disk full (test)"));
        }
        StdFs.create_new(path, contents).await
    }

    async fn create_dir_all(&self, path: &Path) -> io::Result<()> {
        StdFs.create_dir_all(path).await
    }

    async fn remove_file(&self, path: &Path) -> io::Result<()> {
        StdFs.remove_file(path).await
    }

    async fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
        StdFs.remove_dir_all(path).await
    }

    async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
        StdFs.rename(from, to).await
    }

    async fn set_executable(&self, path: &Path, executable: bool) -> io::Result<()> {
        StdFs.set_executable(path, executable).await
    }

    async fn set_link(&self, path: &Path, target: &Path) -> io::Result<()> {
        StdFs.set_link(path, target).await
    }

    fn capabilities(&self) -> Capabilities {
        Capabilities::LOCAL_FS
    }

    async fn sync(&self, path: &Path, need: Durability) -> io::Result<()> {
        StdFs.sync(path, need).await
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FsEvent {
    Write(PathBuf),
    CreateNew(PathBuf),
    Sync(PathBuf, Durability),
    Rename(PathBuf, PathBuf),
    Remove(PathBuf),
    SetExecutable(PathBuf, bool),
    SetLink(PathBuf, PathBuf),
}

#[derive(Debug)]
pub struct RecordingFs {
    log: std::cell::RefCell<Vec<FsEvent>>,
    caps: Capabilities,
}

impl RecordingFs {
    pub fn local() -> Self {
        Self {
            log: std::cell::RefCell::new(Vec::new()),
            caps: Capabilities::LOCAL_FS,
        }
    }

    pub fn events(&self) -> Vec<FsEvent> {
        self.log.borrow().clone()
    }
}

reads_like_stdfs!(RecordingFs);

impl Storage for RecordingFs {
    async fn write(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
        self.log
            .borrow_mut()
            .push(FsEvent::Write(path.to_path_buf()));
        StdFs.write(path, contents).await
    }

    async fn create_new(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
        self.log
            .borrow_mut()
            .push(FsEvent::CreateNew(path.to_path_buf()));
        StdFs.create_new(path, contents).await
    }

    async fn create_dir_all(&self, path: &Path) -> io::Result<()> {
        StdFs.create_dir_all(path).await
    }

    async fn remove_file(&self, path: &Path) -> io::Result<()> {
        self.log
            .borrow_mut()
            .push(FsEvent::Remove(path.to_path_buf()));
        StdFs.remove_file(path).await
    }

    async fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
        StdFs.remove_dir_all(path).await
    }

    async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
        self.log
            .borrow_mut()
            .push(FsEvent::Rename(from.to_path_buf(), to.to_path_buf()));
        StdFs.rename(from, to).await
    }

    async fn set_executable(&self, path: &Path, executable: bool) -> io::Result<()> {
        self.log
            .borrow_mut()
            .push(FsEvent::SetExecutable(path.to_path_buf(), executable));
        StdFs.set_executable(path, executable).await
    }

    async fn set_link(&self, path: &Path, target: &Path) -> io::Result<()> {
        self.log
            .borrow_mut()
            .push(FsEvent::SetLink(path.to_path_buf(), target.to_path_buf()));
        StdFs.set_link(path, target).await
    }

    fn capabilities(&self) -> Capabilities {
        self.caps
    }

    async fn sync(&self, path: &Path, need: Durability) -> io::Result<()> {
        self.log
            .borrow_mut()
            .push(FsEvent::Sync(path.to_path_buf(), need));
        StdFs.sync(path, need).await
    }
}