liteboxfs 0.1.0

A modern POSIX filesystem in a SQLite database
Documentation
use std::{path::Path, sync::Mutex};

use super::{handle::HandleTable, inode::InodeTable, tx::ConnTx};
use crate::{Connection, FileKind, FileOrigin, RootId};

fn populate_inodes(
    fs: &mut crate::Filesystem<'_>,
    dir_path: &Path,
    inodes: &mut InodeTable,
) -> crate::Result<()> {
    for entry in fs.descendants(dir_path)? {
        inodes.insert(entry.path().to_owned(), entry.file_id());
    }
    Ok(())
}

#[derive(Debug)]
struct NonspecificError;

impl From<crate::Error> for NonspecificError {
    fn from(_: crate::Error) -> Self {
        Self
    }
}

#[derive(Debug)]
pub struct FuseState {
    pub inodes: InodeTable,
    pub handles: HandleTable,
}

#[derive(Debug)]
pub struct FuseStateGuard {
    conn_tx: Mutex<ConnTx>,
    state: Mutex<FuseState>,
}

impl FuseStateGuard {
    pub fn new(mut conn: Connection, root_id: RootId, path: &Path) -> crate::Result<Self> {
        let is_dir = conn.exec(|fs| {
            fs.switch_root(root_id)?;
            crate::Result::Ok(fs.open(path)?.kind() == &FileKind::Dir)
        })?;

        if !is_dir {
            return Err(crate::Error::NotADirectory {
                file: FileOrigin::Litebox {
                    root: root_id,
                    locator: path.to_owned().into(),
                },
            });
        }

        let mut inodes = InodeTable::new(path);

        conn.exec(|fs| {
            populate_inodes(fs, path, &mut inodes)?;
            crate::Result::Ok(())
        })?;

        Ok(Self {
            conn_tx: Mutex::new(ConnTx::new(conn, root_id)),
            state: Mutex::new(FuseState {
                inodes,
                handles: HandleTable::new(),
            }),
        })
    }

    // To ensure that filesystem operations are atomic, we execute them in a transaction. However,
    // the FUSE adapter also tracks its own state (inodes, file handles, etc.), which needs to be
    // consistent with the state of the litebox. To enforce this, we use this guard pattern.
    //
    // The first closure `f` has access to the litebox and runs against a savepoint inside the
    // long-lived transaction. If it returns an error, the savepoint is rolled back. It also has
    // a read-only reference to the FUSE state.
    //
    // The second closure `g` does not have access to the litebox, because it is only invoked
    // once the savepoint has been committed. However, it has a mutable reference to the FUSE
    // state, in addition to any values passed from the first closure `f`. This closure must be
    // infallible; it must not return an error.
    //
    // The idea is to enforce a pattern where the FUSE state is infallibly updated only after
    // the database savepoint has been committed.
    //
    // The long-lived transaction itself is committed lazily:
    // - On `fsync()` / `fsyncdir()`.
    // - On `destroy()` (unmount).
    // - After some amount of time has passed since the last activity.
    // - After some amount of time has passed since the last commit.
    pub fn exec<T, F, G>(&self, f: F, g: G)
    where
        F: FnOnce(&mut crate::Filesystem, &FuseState) -> Result<T, ()>,
        G: FnOnce(T, &mut FuseState),
    {
        let state_lock = &mut *self.state.lock().expect("Lock on fuse state poisoned.");
        let conn_tx = &mut *self
            .conn_tx
            .lock()
            .expect("Lock on litebox connection poisoned.");

        conn_tx
            .apply_default_commit_triggers()
            .expect("Time-based commit failed.");

        // Roll back the savepoint. The `f` closure should have already replied with the
        // appropriate errno, so we don't need to return an relevant error here.
        let _: Result<(), NonspecificError> = conn_tx.with_fs(|fs| match f(fs, state_lock) {
            Ok(value) => {
                g(value, state_lock);
                Ok(())
            }
            Err(()) => Err(NonspecificError),
        });
    }

    /// Commit any pending changes accumulated in the long-lived transaction.
    pub fn commit(&self) -> crate::Result<()> {
        let conn_tx = &mut *self
            .conn_tx
            .lock()
            .expect("Lock on litebox connection poisoned.");
        conn_tx.commit_if_dirty()
    }
}