liteboxfs 0.2.0

A modern POSIX filesystem in a SQLite database
Documentation
#[cfg(all(feature = "fs", target_os = "linux"))]
use std::{
    fmt::{self, Debug},
    path::PathBuf,
};

use std::os::fd::OwnedFd;

#[cfg(all(feature = "fs", target_os = "linux"))]
use nix::{
    errno::Errno,
    fcntl::{FcntlArg, fcntl},
    libc,
    unistd::Uid,
};

#[cfg(all(feature = "fs", target_os = "linux"))]
use crate::FileId;

use crate::{FilesystemId, block::FileId as StoreFileId};

#[cfg(all(feature = "fs", target_os = "linux"))]
#[cfg_attr(coverage_nightly, coverage(off))]
fn runtime_dir() -> PathBuf {
    std::env::var_os("XDG_RUNTIME_DIR")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from(format!("/run/user/{}", Uid::current())))
}

#[derive(Debug)]
pub enum LockResult {
    #[cfg_attr(not(all(feature = "fs", target_os = "linux")), allow(dead_code))]
    Acquired(LockHandle),
    Locked,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LockType {
    Read,
    Write,
}

#[derive(Debug, Clone)]
pub struct LockHandler {
    #[cfg(all(feature = "fs", target_os = "linux"))]
    base_dir: PathBuf,
}

#[derive(Debug)]
pub struct LockHandle {
    _fd: OwnedFd,
}

impl LockHandle {
    #[cfg_attr(not(all(feature = "fs", target_os = "linux")), allow(dead_code))]
    fn new(fd: OwnedFd) -> Self {
        Self { _fd: fd }
    }

    /// Attempt to atomically upgrade this handle from a shared lock to an exclusive lock.
    ///
    /// On success, returns `Ok(Ok(handle))` with the (now exclusive) handle. If another holder has
    /// a conflicting lock, returns `Ok(Err(handle))` and the original shared lock is preserved
    /// unchanged.
    #[cfg(all(feature = "fs", target_os = "linux"))]
    #[cfg_attr(not(all(feature = "fuse", target_os = "linux")), allow(dead_code))]
    pub fn try_upgrade(self) -> crate::Result<Result<LockHandle, LockHandle>> {
        use std::io;

        let result = fcntl(
            &self._fd,
            FcntlArg::F_OFD_SETLK(&libc::flock {
                l_type: libc::F_WRLCK as i16,
                l_whence: libc::SEEK_SET as i16,
                l_start: 0,
                l_len: 0,
                l_pid: 0,
            }),
        );

        match result {
            Ok(_) => Ok(Ok(self)),
            Err(Errno::EAGAIN) | Err(Errno::EACCES) => Ok(Err(self)),
            Err(e) => Err(crate::Error::from(io::Error::from_raw_os_error(e as i32))),
        }
    }
}

impl LockHandler {
    #[cfg(all(feature = "fs", target_os = "linux"))]
    pub fn new(fs: FilesystemId) -> Self {
        Self {
            base_dir: runtime_dir()
                .join("liteboxfs")
                .join("fs")
                .join(fs.to_string()),
        }
    }

    #[cfg(not(all(feature = "fs", target_os = "linux")))]
    pub fn new(_fs: FilesystemId) -> Self {
        Self {}
    }

    #[cfg(all(feature = "fs", target_os = "linux"))]
    fn handles_lock_path(&self) -> PathBuf {
        self.base_dir.join("handles.lock")
    }

    #[cfg(all(feature = "fs", target_os = "linux"))]
    fn litebox_lock_path(&self) -> PathBuf {
        self.base_dir.join("litebox.lock")
    }

    #[cfg(all(feature = "fs", target_os = "linux"))]
    fn open_lock_file(&self, path: &std::path::Path) -> crate::Result<OwnedFd> {
        use std::fs;

        fs::create_dir_all(&self.base_dir)?;

        Ok(fs::File::options()
            .create(true)
            .read(true)
            .append(true)
            .open(path)?
            .into())
    }

    // Acquire a lock on the LiteboxFS file with the given ID, returning a handle. The lock is
    // released when the handle is dropped.
    #[cfg(all(feature = "fs", target_os = "linux"))]
    pub fn acquire_file_lock(
        &self,
        file: StoreFileId,
        kind: LockType,
    ) -> crate::Result<LockResult> {
        use std::io;

        let lock_fd = self.open_lock_file(&self.handles_lock_path())?;

        // We use Linux file descriptor locks, acquiring a lock on a single byte corresponding to
        // the file ID. This allows us to lock any number of LiteboxFS files within a single lock
        // file on the host filesystem.
        let result = fcntl(
            &lock_fd,
            FcntlArg::F_OFD_SETLK(&libc::flock {
                l_type: match kind {
                    LockType::Read => libc::F_RDLCK as i16,
                    LockType::Write => libc::F_WRLCK as i16,
                },
                l_whence: libc::SEEK_SET as i16,
                l_start: file.into(),
                l_len: 1,

                // This is only used when testing for the existence of a lock, not when acquiring
                // it. It returns the PID of the locking process, which is not relevant when
                // locking. This may remain "unset", which in Rust just means initialized to 0
                l_pid: 0,
            }),
        );

        match result {
            Ok(_) => Ok(LockResult::Acquired(LockHandle::new(lock_fd))),
            // The Linux API can return either of these errors to indicate a lock is already held;
            // portable implementations are required to handle both.
            Err(Errno::EAGAIN) | Err(Errno::EACCES) => Ok(LockResult::Locked),
            Err(e) => Err(crate::Error::from(io::Error::from_raw_os_error(e as i32))),
        }
    }

    #[cfg(not(all(feature = "fs", target_os = "linux")))]
    pub fn acquire_file_lock(
        &self,
        _file: StoreFileId,
        _kind: LockType,
    ) -> crate::Result<LockResult> {
        // If the `fs` feature is disabled, we should act as if all files could be open in other
        // transactions, because we have no way of knowing.
        Ok(LockResult::Locked)
    }

    // Acquire a whole-litebox lock, used to keep the database stable for the duration of a FUSE
    // mount. Connection holders take this as `LockType::Read` (shared); mounting upgrades to
    // `LockType::Write` (exclusive) via [`LockHandle::try_upgrade_to_exclusive`].
    #[cfg(all(feature = "fs", target_os = "linux"))]
    pub fn acquire_litebox_lock(&self, kind: LockType) -> crate::Result<LockResult> {
        use std::io;

        let lock_fd = self.open_lock_file(&self.litebox_lock_path())?;

        let result = fcntl(
            &lock_fd,
            FcntlArg::F_OFD_SETLK(&libc::flock {
                l_type: match kind {
                    LockType::Read => libc::F_RDLCK as i16,
                    LockType::Write => libc::F_WRLCK as i16,
                },
                l_whence: libc::SEEK_SET as i16,
                l_start: 0,
                l_len: 0,
                l_pid: 0,
            }),
        );

        match result {
            Ok(_) => Ok(LockResult::Acquired(LockHandle::new(lock_fd))),
            Err(Errno::EAGAIN) | Err(Errno::EACCES) => Ok(LockResult::Locked),
            Err(e) => Err(crate::Error::from(io::Error::from_raw_os_error(e as i32))),
        }
    }
}

/// A raw handle to a file which can be used to prevent its deletion.
///
/// This handle can be used to keep a file which has been unlinked with [`Filesystem::unlink`] from
/// being deleted without needing to hold a [`File`], which holds an exclusive reference to the
/// filesystem.
///
/// However, unlike [`File`], **this handle does not automatically delete the file when it is
/// dropped**. For that, you must use [`Filesystem::release`]. If you fail to call
/// [`Filesystem::release`] before dropping this handle, the file will be orphaned and remain on
/// disk until either 1) another connection with an open handle deletes it or 2) it is garbage
/// collected at an unspecified later time.
///
/// You can get a [`RawFd`] by calling [`File::leak_fd`].
///
/// [`File`]: crate::File
/// [`Filesystem::unlink`]: crate::Filesystem::unlink
/// [`File::leak_fd`]: crate::File::leak_fd
/// [`Filesystem::release`]: crate::Filesystem::release
#[cfg(all(feature = "fs", target_os = "linux"))]
pub struct RawFd {
    _handle: LockHandle,
    file_id: StoreFileId,
    filesystem_id: FilesystemId,
}

#[cfg(all(feature = "fs", target_os = "linux"))]
impl RawFd {
    pub(crate) fn new(
        handle: LockHandle,
        file_id: StoreFileId,
        filesystem_id: FilesystemId,
    ) -> Self {
        Self {
            _handle: handle,
            file_id,
            filesystem_id,
        }
    }

    pub(crate) fn into_file_id(self) -> StoreFileId {
        self.file_id
    }

    /// The [`FileId`] of the file associated with this raw file descriptor.
    pub fn file_id(&self) -> FileId {
        FileId::new(self.file_id, self.filesystem_id)
    }
}

#[cfg(all(feature = "fs", target_os = "linux"))]
#[cfg_attr(coverage_nightly, coverage(off))]
impl Debug for RawFd {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("RawFd").field(&self.file_id).finish()
    }
}