liteboxfs 0.2.0

A modern POSIX filesystem in a SQLite database
Documentation
use rusqlite::DropBehavior;

use crate::{Filesystem, RootId, settings::Settings, sql::SqlStore};

/// The behavior of a SQLite transaction.
///
/// See the [SQLite documentation](https://sqlite.org/lang_transaction.html) for more information.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TransactionBehavior {
    /// `DEFERRED` means that the transaction does not actually start until the database is first
    /// accessed.
    Deferred,

    /// `IMMEDIATE` cause the database connection to start a new write immediately, without waiting
    /// for a writes statement.
    Immediate,

    /// `EXCLUSIVE` prevents other database connections from reading the database while the
    /// transaction is underway.
    Exclusive,
}

#[doc(hidden)]
impl From<TransactionBehavior> for rusqlite::TransactionBehavior {
    fn from(value: TransactionBehavior) -> Self {
        match value {
            TransactionBehavior::Deferred => rusqlite::TransactionBehavior::Deferred,
            TransactionBehavior::Immediate => rusqlite::TransactionBehavior::Immediate,
            TransactionBehavior::Exclusive => rusqlite::TransactionBehavior::Exclusive,
        }
    }
}

/// An open transaction on a [`Filesystem`].
///
/// If a [`Transaction`] is dropped without committing, the transaction is rolled back.
#[derive(Debug)]
pub struct Transaction<'conn> {
    tx: rusqlite::Transaction<'conn>,
    settings: Settings,
    default_root_id: RootId,
    changes_at_start: u64,
}

impl<'conn> Transaction<'conn> {
    pub(super) fn new(
        tx: rusqlite::Transaction<'conn>,
        settings: Settings,
        default_root_id: RootId,
    ) -> Self {
        let changes_at_start = tx.total_changes();
        Self {
            tx,
            settings,
            default_root_id,
            changes_at_start,
        }
    }

    /// Delete orphaned blocks.
    ///
    /// We don't need to do this on every write, because data is only freed up on disk when the
    /// transaction commits anyways. As a performance optimization, we can defer this until right
    /// before we commit the transaction.
    fn clean(&self) -> crate::Result<()> {
        let mut stmt = self.tx.prepare_cached(
            r#"
            DELETE FROM
                liteboxfs_blocks
            WHERE
                id NOT IN (SELECT block FROM liteboxfs_file_blocks);
            "#,
        )?;

        stmt.execute([])?;

        Ok(())
    }

    /// Execute the given function within this transaction.
    ///
    /// This calls the given function, passing a [`Filesystem`] holding this transaction. If the
    /// function returns [`Ok`], this transaction is committed. If the function returns [`Err`],
    /// this transaction is rolled back.
    pub fn exec<T, E, F>(mut self, f: F) -> Result<T, E>
    where
        F: FnOnce(&mut Filesystem) -> Result<T, E>,
        E: From<crate::Error>,
    {
        let result = {
            let mut savepoint = self.tx.savepoint().map_err(crate::Error::from)?;
            savepoint.set_drop_behavior(DropBehavior::Commit);
            let store = SqlStore::new(savepoint, self.settings.clone());
            let mut fs = Filesystem::new(self.default_root_id, self.settings.clone(), store);

            f(&mut fs)
        }?;

        if self.tx.total_changes() != self.changes_at_start {
            self.clean()?;
        }

        self.tx.commit().map_err(crate::Error::from)?;

        Ok(result)
    }

    /// Get the [`Filesystem`] holding this transaction.
    pub fn fs(&mut self) -> crate::Result<Filesystem<'_>> {
        let mut savepoint = self.tx.savepoint().map_err(crate::Error::from)?;
        savepoint.set_drop_behavior(DropBehavior::Commit);
        let store = SqlStore::new(savepoint, self.settings.clone());

        Ok(Filesystem::new(
            self.default_root_id,
            self.settings.clone(),
            store,
        ))
    }

    /// Roll back this transaction.
    pub fn rollback(self) -> crate::Result<()> {
        Ok(self.tx.rollback()?)
    }

    /// Commit this transaction.
    pub fn commit(self) -> crate::Result<()> {
        if self.tx.total_changes() != self.changes_at_start {
            self.clean()?;
        }
        Ok(self.tx.commit()?)
    }
}