liteboxfs 0.1.0

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

use crate::{
    FileOrigin, RootId, block::FileId as StoreFileId, path::NormalizedPath, sql::SqlStore,
};

impl<'conn> SqlStore<'conn> {
    pub fn unlink_file_from_path(
        &self,
        root_id: RootId,
        path: &NormalizedPath,
    ) -> crate::Result<StoreFileId> {
        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),
        )?;

        let path_row_id = self.resolve_path_id(root_row_id, root_id, path)?;

        let file_id: i64 = self.db.query_row(
            r#"
            DELETE FROM
                liteboxfs_paths
            WHERE
                id = ?
            RETURNING
                file;
            "#,
            rusqlite::params![path_row_id],
            |row| row.get(0),
        )?;

        Ok(StoreFileId::from(file_id))
    }

    pub fn list_unlinked_files(&self) -> crate::Result<Vec<StoreFileId>> {
        let mut stmt = self.db.prepare_cached(
            r#"
            SELECT
                liteboxfs_files.id
            FROM
                liteboxfs_files
            WHERE
                NOT EXISTS (
                    SELECT
                        1
                    FROM
                        liteboxfs_paths
                    WHERE
                        liteboxfs_paths.file = liteboxfs_files.id
                );
            "#,
        )?;

        Ok(stmt
            .query_map([], |row| row.get(0))?
            .collect::<Result<Vec<_>, rusqlite::Error>>()?)
    }

    pub fn delete_file_if_unlinked(&self, file_id: StoreFileId) -> crate::Result<()> {
        let mut stmt = self.db.prepare_cached(
            r#"
            DELETE FROM
                liteboxfs_files
            WHERE
                id = ?
                AND NOT EXISTS (
                    SELECT
                        1
                    FROM
                        liteboxfs_paths
                    WHERE
                        liteboxfs_paths.file = liteboxfs_files.id
                );
            "#,
        )?;

        stmt.execute(rusqlite::params![file_id])?;

        Ok(())
    }

    pub fn path_has_children(&self, root_id: RootId, path: &NormalizedPath) -> crate::Result<bool> {
        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),
        )?;

        let path_row_id = self.resolve_path_id(root_row_id, root_id, path)?;

        let has_children = self
            .db
            .query_row(
                r#"
                SELECT
                    1
                FROM
                    liteboxfs_paths
                WHERE
                    parent = ?
                LIMIT 1;
                "#,
                rusqlite::params![path_row_id],
                |_| Ok(()),
            )
            .optional()?
            .is_some();

        Ok(has_children)
    }

    pub fn unlink_tree(
        &self,
        root_id: RootId,
        path: &NormalizedPath,
    ) -> crate::Result<Vec<StoreFileId>> {
        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),
        )?;

        let path_row_id = self.resolve_path_id(root_row_id, root_id, path)?;

        let mut stmt = self.db.prepare(
            r#"
            WITH RECURSIVE subtree(id) AS (
                SELECT ?1
                UNION ALL
                SELECT
                    liteboxfs_paths.id
                FROM
                    liteboxfs_paths
                JOIN
                    subtree ON liteboxfs_paths.parent = subtree.id
            )
            DELETE FROM
                liteboxfs_paths
            WHERE
                id IN (SELECT id FROM subtree)
            RETURNING
                file;
            "#,
        )?;

        let file_ids: Vec<StoreFileId> = stmt
            .query_map(rusqlite::params![path_row_id], |row| row.get::<_, i64>(0))?
            .map(|r| r.map(StoreFileId::from))
            .collect::<Result<_, _>>()?;

        if file_ids.is_empty() {
            return Err(crate::Error::FileNotFound {
                file: FileOrigin::Litebox {
                    root: root_id,
                    locator: path.to_owned().into(),
                },
            });
        }

        Ok(file_ids)
    }
}