timefs 0.1.0

Mount a Git repository as a read-only filesystem.
Documentation
//! Filesystem-facing command handlers.

mod head;
mod inode;
pub mod resolve;

use crate::config::{MountConfig, UnmountConfig};
use crate::git::GixRepository;
use anyhow::{anyhow, Context, Result};
use fuser::Session;
use std::io::ErrorKind;
use std::path::Path;
use std::process::Command;
use std::sync::{Arc, Mutex};

/// Mount the read-only Git history filesystem.
pub fn mount(config: MountConfig) -> Result<()> {
    tracing::info!(
        repo = %config.repo.display(),
        mountpoint = %config.mountpoint.display(),
        foreground = config.foreground,
        allow_other = config.allow_other,
        submodules = ?config.submodules,
        lfs = config.lfs,
        ref_snapshot = config.ref_snapshot,
        cache_size_mb = config.cache_size_mb,
        uid = ?config.uid,
        gid = ?config.gid,
        mount_options = ?config.fuse_mount_options(),
        "mounting timefs"
    );

    if !config.foreground {
        tracing::info!("background mode is not implemented yet; running in the foreground");
    }

    let store = GixRepository::open_with_options(
        &config.repo,
        config.cache_size_mb,
        config.lfs,
        config.ref_snapshot,
    )
    .with_context(|| format!("failed to open Git repository {}", config.repo.display()))?;
    let filesystem = head::HeadFilesystem::new(store, &config)?;

    let mountpoint = config.mountpoint.clone();
    let options = config.fuse_mount_options();
    let mut session = Session::new(filesystem, &mountpoint, &options)
        .with_context(|| format!("failed to mount {}", mountpoint.display()))?;

    let unmounter = Arc::new(Mutex::new(Some(session.unmount_callable())));
    ctrlc::set_handler({
        let unmounter = Arc::clone(&unmounter);
        move || {
            if let Ok(mut guard) = unmounter.lock() {
                if let Some(unmounter) = guard.as_mut() {
                    let _ = unmounter.unmount();
                }
            }
        }
    })
    .context("failed to install Ctrl-C handler")?;

    session
        .run()
        .with_context(|| format!("filesystem session failed at {}", mountpoint.display()))
}

/// Unmount a previously mounted `timefs` filesystem.
pub fn unmount(config: UnmountConfig) -> Result<()> {
    tracing::info!(
        mountpoint = %config.mountpoint.display(),
        "unmounting timefs"
    );

    let attempts = [
        ("fusermount3", ["-u"].as_slice()),
        ("fusermount", ["-u"].as_slice()),
        ("umount", &[] as &[&str]),
    ];

    let mut last_error = None;
    for (program, args) in attempts {
        match try_unmount(program, args, &config.mountpoint)? {
            UnmountOutcome::Success => return Ok(()),
            UnmountOutcome::NotFound => {}
            UnmountOutcome::Failed(error) => last_error = Some(error),
        }
    }

    Err(last_error.unwrap_or_else(|| {
        anyhow!("no unmount utility found; tried fusermount3, fusermount, and umount")
    }))
}

enum UnmountOutcome {
    Success,
    NotFound,
    Failed(anyhow::Error),
}

fn try_unmount(program: &str, args: &[&str], mountpoint: &Path) -> Result<UnmountOutcome> {
    let output = match Command::new(program).args(args).arg(mountpoint).output() {
        Ok(output) => output,
        Err(source) if source.kind() == ErrorKind::NotFound => return Ok(UnmountOutcome::NotFound),
        Err(source) => {
            return Err(anyhow!(
                "failed to invoke {program} for {}: {source}",
                mountpoint.display()
            ));
        }
    };

    if output.status.success() {
        return Ok(UnmountOutcome::Success);
    }

    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned();
    let detail = if !stderr.is_empty() {
        stderr
    } else if !stdout.is_empty() {
        stdout
    } else {
        format!("exit status {}", output.status)
    };

    Ok(UnmountOutcome::Failed(anyhow!(
        "{program} failed to unmount {}: {detail}",
        mountpoint.display()
    )))
}