liteboxfs 0.1.0

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

use rusqlite::{ToSql, types::FromSql};

use super::{path::PathInsertResult, store::SqlStore};
use crate::{
    FileLocator, FileMode, FileOrigin, Gid, RootId, Uid,
    block::FileId as StoreFileId,
    errors::InternalError,
    file_metadata::{Device, FileKind, FileMetadata, RawMetadata},
    path::NormalizedPath,
    util::system_time_to_nanos,
};

/// A [`FileKind`] without associated data.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileDiscriminant {
    /// A regular file.
    Regular,

    /// A directory.
    Dir,

    /// A symbolic link.
    Symlink,

    /// A block device.
    Block,

    /// A character device.
    Char,

    /// A named pipe (FIFO).
    Pipe,
}

impl FileKind {
    /// Get the [`FileDiscriminant`] for this [`FileKind`].
    pub fn discriminant(&self) -> FileDiscriminant {
        match self {
            FileKind::Regular => FileDiscriminant::Regular,
            FileKind::Dir => FileDiscriminant::Dir,
            FileKind::Symlink { .. } => FileDiscriminant::Symlink,
            FileKind::Block { .. } => FileDiscriminant::Block,
            FileKind::Char { .. } => FileDiscriminant::Char,
            FileKind::Pipe => FileDiscriminant::Pipe,
        }
    }
}

#[doc(hidden)]
impl FromSql for FileDiscriminant {
    fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
        match value.as_i64()? {
            1 => Ok(FileDiscriminant::Regular),
            2 => Ok(FileDiscriminant::Dir),
            3 => Ok(FileDiscriminant::Symlink),
            4 => Ok(FileDiscriminant::Block),
            5 => Ok(FileDiscriminant::Char),
            6 => Ok(FileDiscriminant::Pipe),
            other => Err(rusqlite::types::FromSqlError::Other(Box::new(
                InternalError::InvalidFileKind {
                    kind: other.to_string(),
                },
            ))),
        }
    }
}

#[doc(hidden)]
impl ToSql for FileDiscriminant {
    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
        let s = match self {
            FileDiscriminant::Regular => 1,
            FileDiscriminant::Dir => 2,
            FileDiscriminant::Symlink => 3,
            FileDiscriminant::Block => 4,
            FileDiscriminant::Char => 5,
            FileDiscriminant::Pipe => 6,
        };

        Ok(rusqlite::types::ToSqlOutput::from(s))
    }
}

impl<'conn> SqlStore<'conn> {
    #[cfg(all(feature = "fs", target_os = "linux"))]
    pub fn create_temp_file(&self, metadata: FileMetadata) -> crate::Result<StoreFileId> {
        let mut stmt = self.db.prepare_cached(
            r#"
                INSERT INTO
                    liteboxfs_files (kind, mode, atime, mtime, ctime, btime, uid, gid)
                VALUES
                    (
                        ?,
                        ?,
                        ?,
                        ?,
                        ?,
                        ?,
                        ?,
                        ?
                    )
                RETURNING
                    id;
                "#,
        )?;

        let params = rusqlite::params![
            FileDiscriminant::Regular,
            metadata.mode().bits(),
            system_time_to_nanos(metadata.accessed())?,
            system_time_to_nanos(metadata.modified())?,
            system_time_to_nanos(metadata.changed())?,
            metadata.created().map(system_time_to_nanos).transpose()?,
            metadata.user().as_raw(),
            metadata.group().as_raw(),
        ];

        Ok(stmt.query_one(params, |row| Ok(StoreFileId::from(row.get::<_, i64>(0)?)))?)
    }

    pub fn create_file(
        &self,
        root: RootId,
        path: &NormalizedPath,
        kind: &FileKind,
        metadata: FileMetadata,
    ) -> crate::Result<StoreFileId> {
        let file_id = {
            let mut stmt = self.db.prepare_cached(
                r#"
                INSERT INTO
                    liteboxfs_files (kind, mode, atime, mtime, ctime, btime, uid, gid, major, minor, target)
                VALUES
                    (
                        ?,
                        ?,
                        ?,
                        ?,
                        ?,
                        ?,
                        ?,
                        ?,
                        ?,
                        ?,
                        ?
                    )
                RETURNING
                    id;
                "#,
            )?;

            let params = rusqlite::params![
                kind.discriminant(),
                metadata.mode().bits(),
                system_time_to_nanos(metadata.accessed())?,
                system_time_to_nanos(metadata.modified())?,
                system_time_to_nanos(metadata.changed())?,
                metadata.created().map(system_time_to_nanos).transpose()?,
                metadata.user().as_raw(),
                metadata.group().as_raw(),
                match kind {
                    FileKind::Block { dev } | FileKind::Char { dev } => Some(dev.major()),
                    _ => None,
                },
                match kind {
                    FileKind::Block { dev } | FileKind::Char { dev } => Some(dev.minor()),
                    _ => None,
                },
                match kind {
                    FileKind::Symlink { target } => Some(target.as_os_str().as_encoded_bytes()),
                    _ => None,
                },
            ];

            stmt.query_one(params, |row| Ok(StoreFileId::from(row.get::<_, i64>(0)?)))?
        };

        if self.insert_path(root, path, file_id)? == PathInsertResult::AlreadyExists {
            // If an error is returned, the caller should roll back to the most recent savepoint,
            // undoing the inserts we've done so far.
            return Err(crate::Error::FileAlreadyExists {
                path: FileOrigin::Litebox {
                    root,
                    locator: path.to_path_buf(),
                },
            });
        }

        Ok(file_id)
    }

    pub fn get_file(
        &self,
        locator: FileLocator,
        root_id: RootId,
    ) -> crate::Result<(StoreFileId, FileKind, RawMetadata)> {
        let map_row = |row: &rusqlite::Row| {
            let file_id = StoreFileId::from(row.get::<_, i64>(0)?);
            let discriminant: FileDiscriminant = row.get(1)?;
            let metadata = RawMetadata {
                discriminant,
                mode: FileMode::from_bits_truncate(row.get(2)?),
                atime: row.get(3)?,
                mtime: row.get(4)?,
                ctime: row.get(5)?,
                btime: row.get(6)?,
                uid: Uid::from_raw(row.get(7)?),
                gid: Gid::from_raw(row.get(8)?),
            };

            match discriminant {
                FileDiscriminant::Regular => Ok((file_id, FileKind::Regular, metadata)),
                FileDiscriminant::Dir => Ok((file_id, FileKind::Dir, metadata)),
                FileDiscriminant::Pipe => Ok((file_id, FileKind::Pipe, metadata)),
                FileDiscriminant::Symlink => {
                    let target: Vec<u8> = row.get::<_, Vec<u8>>(11)?;
                    Ok((
                        file_id,
                        FileKind::Symlink {
                            target: Path::new(OsStr::from_bytes(&target)).to_path_buf(),
                        },
                        metadata,
                    ))
                }
                FileDiscriminant::Block | FileDiscriminant::Char => {
                    let major: u64 = row.get(9)?;
                    let minor: u64 = row.get(10)?;
                    let device = Device::new(major, minor);
                    Ok((
                        file_id,
                        match discriminant {
                            FileDiscriminant::Block => FileKind::Block { dev: device },
                            FileDiscriminant::Char => FileKind::Char { dev: device },
                            _ => unreachable!(),
                        },
                        metadata,
                    ))
                }
            }
        };

        let query_result = match &locator {
            FileLocator::Path(path) => {
                let normalized = NormalizedPath::new(path.as_ref());
                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, &normalized)?;
                self.db.query_one(
                    r#"
                    SELECT
                        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
                        liteboxfs_files
                    JOIN
                        liteboxfs_paths ON liteboxfs_files.id = liteboxfs_paths.file
                    WHERE
                        liteboxfs_paths.id = ?;
                    "#,
                    rusqlite::params![path_row_id],
                    map_row,
                )
            }
            FileLocator::Id(id) => self.db.query_one(
                r#"
                SELECT
                    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
                    liteboxfs_files
                WHERE
                    liteboxfs_files.id = ?;
                "#,
                // No root check: a FileId may refer to an unlinked file that has no paths.
                // The FileId type encodes the filesystem UUID, preventing cross-database access.
                rusqlite::params![*id],
                map_row,
            ),
        };

        match query_result {
            Err(rusqlite::Error::QueryReturnedNoRows) => Err(crate::Error::FileNotFound {
                file: FileOrigin::Litebox {
                    root: root_id,
                    locator: locator.into_normalized(),
                },
            }),
            Err(err) => Err(err.into()),
            Ok(tuple) => Ok(tuple),
        }
    }

    #[cfg(all(feature = "fs", target_family = "unix"))]
    pub fn get_file_discriminant(
        &self,
        path: &NormalizedPath,
        root_id: RootId,
    ) -> crate::Result<FileDiscriminant> {
        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)?;

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

    pub fn get_file_count(&self, root_id: RootId) -> crate::Result<u64> {
        let mut stmt = self.db.prepare_cached(
            r#"
                SELECT
                    COUNT(DISTINCT liteboxfs_paths.file)
                FROM
                    liteboxfs_paths
                JOIN
                    liteboxfs_roots ON liteboxfs_paths.root = liteboxfs_roots.id
                WHERE
                    liteboxfs_roots.uuid = ?;
            "#,
        )?;

        Ok(stmt.query_one(rusqlite::params![root_id], |row| row.get(0))?)
    }
}