liteboxfs 0.2.0

A modern POSIX filesystem in a SQLite database
Documentation
use std::ffi::{OsStr, OsString};
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::Path;

use crate::file_metadata::RawMetadata;
use crate::tree::DescendantsRow;
use crate::{FileMode, Gid, Uid};
use crate::{
    FileOrigin, RootId,
    block::FileId as StoreFileId,
    file_metadata::{Device, FileKind},
    path::NormalizedPath,
    sql::FileDiscriminant,
    tree::ChildrenRow,
};

use super::store::SqlStore;

#[derive(Debug, Clone, Copy)]
pub struct AncestorPathId(pub(crate) i64);

impl<'conn> SqlStore<'conn> {
    /// Get the path row ID of `base` to use as the starting point for a walk.
    ///
    /// Returns `Error::FileNotFound` if `base` does not exist, and `Error::NotADirectory` if it
    /// is not a directory.
    pub fn get_ancestor_id(
        &self,
        root_id: RootId,
        base: &NormalizedPath,
    ) -> crate::Result<AncestorPathId> {
        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_id = self.resolve_path_id(root_row_id, root_id, base)?;

        let kind: FileDiscriminant = self.db.query_row(
            r#"
            SELECT
                liteboxfs_files.kind
            FROM
                liteboxfs_paths
            JOIN
                liteboxfs_files ON liteboxfs_paths.file = liteboxfs_files.id
            WHERE
                liteboxfs_paths.id = ?;
            "#,
            rusqlite::params![path_id],
            |row| row.get(0),
        )?;

        if kind != FileDiscriminant::Dir {
            return Err(crate::Error::NotADirectory {
                file: FileOrigin::Litebox {
                    root: root_id,
                    locator: base.to_path_buf().into(),
                },
            });
        }

        Ok(AncestorPathId(path_id))
    }

    /// Fetch all immediate children of `ancestor_id` in a single query.
    pub fn get_all_children(&self, ancestor_id: AncestorPathId) -> crate::Result<Vec<ChildrenRow>> {
        let mut stmt = self.db.prepare_cached(
            r#"
            SELECT
                liteboxfs_paths.name,
                liteboxfs_files.id,
                liteboxfs_files.kind,
                liteboxfs_files.mode,
                liteboxfs_files.atime,
                liteboxfs_files.mtime,
                liteboxfs_files.ctime,
                liteboxfs_files.btime,
                liteboxfs_files.uid,
                liteboxfs_files.gid,
                liteboxfs_files.major,
                liteboxfs_files.minor,
                liteboxfs_files.target,
                liteboxfs_paths.id
            FROM
                liteboxfs_paths
            JOIN
                liteboxfs_files ON liteboxfs_paths.file = liteboxfs_files.id
            WHERE
                liteboxfs_paths.parent = ?1
            ORDER BY
                liteboxfs_paths.name ASC;
            "#,
        )?;

        Ok(stmt
            .query_map(rusqlite::params![ancestor_id.0], |row| {
                let name_bytes: Vec<u8> = row.get(0)?;
                let file_id: i64 = row.get(1)?;
                let metadata = RawMetadata {
                    discriminant: row.get(2)?,
                    mode: FileMode::from_bits_truncate(row.get(3)?),
                    atime: row.get(4)?,
                    mtime: row.get(5)?,
                    ctime: row.get(6)?,
                    btime: row.get(7)?,
                    uid: Uid::from_raw(row.get(8)?),
                    gid: Gid::from_raw(row.get(9)?),
                };
                let device_major: Option<u64> = row.get(10)?;
                let device_minor: Option<u64> = row.get(11)?;
                let symlink_target: Option<Vec<u8>> = row.get(12)?;
                let new_ancestor_id = AncestorPathId(row.get(13)?);

                let name = OsString::from_vec(name_bytes);
                let file_id = StoreFileId::from(file_id);

                let kind = match metadata.discriminant {
                    FileDiscriminant::Regular => FileKind::Regular,
                    FileDiscriminant::Dir => FileKind::Dir,
                    FileDiscriminant::Pipe => FileKind::Pipe,
                    FileDiscriminant::Symlink => FileKind::Symlink {
                        target: Path::new(OsStr::from_bytes(
                            symlink_target
                                .as_deref()
                                .expect("Symlink should have a target."),
                        ))
                        .to_path_buf(),
                    },
                    FileDiscriminant::Block => FileKind::Block {
                        dev: Device::new(
                            device_major.expect("Block device should have a major number."),
                            device_minor.expect("Block device should have a minor number."),
                        ),
                    },
                    FileDiscriminant::Char => FileKind::Char {
                        dev: Device::new(
                            device_major.expect("Char device should have a major number."),
                            device_minor.expect("Char device should have a minor number."),
                        ),
                    },
                };

                Ok(ChildrenRow {
                    ancestor_id: new_ancestor_id,
                    name,
                    file_id,
                    kind,
                    metadata,
                })
            })?
            .collect::<Result<Vec<_>, _>>()?)
    }

    /// Fetch all descendants of `ancestor_id` in a single query.
    ///
    /// Results are ordered parents-before-children (BFS order), which is required for the
    /// caller's path reconstruction logic to work correctly.
    pub fn get_all_descendants(
        &self,
        ancestor_id: AncestorPathId,
    ) -> crate::Result<Vec<DescendantsRow>> {
        let mut stmt = self.db.prepare_cached(
            r#"
            WITH RECURSIVE descendants(id, file, name, parent_id, depth) AS (
                SELECT
                    liteboxfs_paths.id,
                    liteboxfs_paths.file,
                    liteboxfs_paths.name,
                    liteboxfs_paths.parent,
                    0
                FROM
                    liteboxfs_paths
                WHERE
                    liteboxfs_paths.parent = ?1

                UNION ALL

                SELECT
                    liteboxfs_paths.id,
                    liteboxfs_paths.file,
                    liteboxfs_paths.name,
                    liteboxfs_paths.parent,
                    descendants.depth + 1
                FROM
                    liteboxfs_paths
                JOIN
                    descendants ON liteboxfs_paths.parent = descendants.id
            )
            SELECT
                descendants.id,
                descendants.name,
                descendants.parent_id,
                liteboxfs_files.id,
                liteboxfs_files.kind,
                liteboxfs_files.mode,
                liteboxfs_files.atime,
                liteboxfs_files.mtime,
                liteboxfs_files.ctime,
                liteboxfs_files.btime,
                liteboxfs_files.uid,
                liteboxfs_files.gid,
                liteboxfs_files.major,
                liteboxfs_files.minor,
                liteboxfs_files.target
            FROM
                descendants
            JOIN
                liteboxfs_files ON descendants.file = liteboxfs_files.id
            ORDER BY
                descendants.depth ASC,
                descendants.id ASC;
            "#,
        )?;

        Ok(stmt
            .query_map(rusqlite::params![ancestor_id.0], |row| {
                let path_id: i64 = row.get(0)?;
                let name_bytes: Vec<u8> = row.get(1)?;
                let parent_path_id: i64 = row.get(2)?;
                let file_id: i64 = row.get(3)?;
                let metadata = RawMetadata {
                    discriminant: row.get(4)?,
                    mode: FileMode::from_bits_truncate(row.get(5)?),
                    atime: row.get(6)?,
                    mtime: row.get(7)?,
                    ctime: row.get(8)?,
                    btime: row.get(9)?,
                    uid: Uid::from_raw(row.get(10)?),
                    gid: Gid::from_raw(row.get(11)?),
                };
                let device_major: Option<u64> = row.get(12)?;
                let device_minor: Option<u64> = row.get(13)?;
                let symlink_target: Option<Vec<u8>> = row.get(14)?;

                let name = OsString::from_vec(name_bytes);
                let file_id = StoreFileId::from(file_id);

                let kind = match metadata.discriminant {
                    FileDiscriminant::Regular => FileKind::Regular,
                    FileDiscriminant::Dir => FileKind::Dir,
                    FileDiscriminant::Pipe => FileKind::Pipe,
                    FileDiscriminant::Symlink => FileKind::Symlink {
                        target: Path::new(OsStr::from_bytes(
                            symlink_target
                                .as_deref()
                                .expect("Symlink should have a target."),
                        ))
                        .to_path_buf(),
                    },
                    FileDiscriminant::Block => FileKind::Block {
                        dev: Device::new(
                            device_major.expect("Block device should have a major number."),
                            device_minor.expect("Block device should have a minor number."),
                        ),
                    },
                    FileDiscriminant::Char => FileKind::Char {
                        dev: Device::new(
                            device_major.expect("Char device should have a major number."),
                            device_minor.expect("Char device should have a minor number."),
                        ),
                    },
                };

                Ok(DescendantsRow {
                    path_id,
                    name,
                    parent_path_id,
                    file_id,
                    kind,
                    metadata,
                })
            })?
            .collect::<Result<Vec<_>, _>>()?)
    }
}