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};
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()))
}
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()
)))
}