liteboxfs 0.1.0

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

use super::{SqlStore, exclusive::ExclusiveFileId};
use crate::{RootId, block::FileId as StoreFileId};

impl<'conn> SqlStore<'conn> {
    /// Ensure the given file's `liteboxfs_files` row is private to `root_id`.
    ///
    /// If the file is currently shared (i.e. paths in other roots also reference this file ID),
    /// a new `liteboxfs_files` row is inserted (copying all metadata and associated
    /// `liteboxfs_file_blocks`, `liteboxfs_merkle_nodes`, and `liteboxfs_xattrs` rows), and the
    /// `liteboxfs_paths` rows for `root_id` are updated to point at the new row. The returned
    /// [`ExclusiveFileId`] always refers to a row that is exclusively owned by `root_id` at the
    /// time of the call.
    pub fn materialize_file(
        &self,
        file_id: StoreFileId,
        root_id: RootId,
    ) -> crate::Result<ExclusiveFileId> {
        let root_row_id: i64 = self.db.query_row(
            r#"
            SELECT
                id
            FROM
                liteboxfs_roots
            WHERE
                uuid = ?;
            "#,
            rusqlite::params![root_id.to_string()],
            |row| row.get(0),
        )?;

        // A file is shared only if another root references it. Multiple paths within the same root
        // are hard links: they're meant to share the underlying file row, so mutations should be
        // visible through every link. COW'ing in that case would allocate a new file ID and break
        // identity for callers that track files by ID.
        let is_shared: bool = self
            .db
            .query_row(
                r#"
                SELECT
                    1
                FROM
                    liteboxfs_paths
                WHERE
                    file = ?
                    AND root != ?
                LIMIT 1;
                "#,
                rusqlite::params![file_id, root_row_id],
                |_| Ok(()),
            )
            .optional()?
            .is_some();

        if !is_shared {
            return Ok(ExclusiveFileId::new(file_id));
        }

        // The file is shared. Insert a new liteboxfs_files row copying all metadata.
        let new_file_id: StoreFileId = self.db.query_row(
            r#"
            INSERT INTO
                liteboxfs_files (kind, mode, atime, mtime, ctime, btime, uid, gid, major, minor, target)
            SELECT
                kind, mode, atime, mtime, ctime, btime, uid, gid, major, minor, target
            FROM
                liteboxfs_files
            WHERE
                id = ?
            RETURNING
                id;
            "#,
            rusqlite::params![file_id],
            |row| Ok(StoreFileId::from(row.get::<_, i64>(0)?)),
        )?;

        // Copy associated per-file tables.
        self.copy_file_blocks(file_id, new_file_id)?; // defined in root.rs
        self.copy_merkle_nodes(file_id, new_file_id)?;
        self.copy_xattrs(file_id, new_file_id)?;

        // Re-point all paths in this root to the new file ID.
        self.db.execute(
            r#"
            UPDATE
                liteboxfs_paths
            SET
                file = ?
            WHERE
                file = ?
                AND root = ?;
            "#,
            rusqlite::params![new_file_id, file_id, root_row_id],
        )?;

        Ok(ExclusiveFileId::new(new_file_id))
    }

    /// Delete all `liteboxfs_files` rows that are no longer referenced by any `liteboxfs_paths`
    /// row.
    ///
    /// This is called after a root is deleted: the root deletion cascades through paths, but files
    /// are not tied to roots directly, so orphaned files must be cleaned up separately.
    pub fn delete_all_unlinked_files(&self) -> crate::Result<()> {
        self.db.execute(
            r#"
            DELETE FROM
                liteboxfs_files
            WHERE
                NOT EXISTS (
                    SELECT
                        1
                    FROM
                        liteboxfs_paths
                    WHERE
                        liteboxfs_paths.file = liteboxfs_files.id
                );
            "#,
            [],
        )?;

        Ok(())
    }
}