#![allow(clippy::doc_markdown)]
mod fusefs;
use fusefs::FuseFS;
use abscissa_core::{
Command, FrameworkError, FrameworkErrorKind::ParseError, Runnable, Shutdown, config::Override,
};
use anyhow::{Result, bail};
use clap::Parser;
use conflate::{Merge, MergeFrom};
use fuse_mt::{FuseMT, mount};
use log::info;
use rustic_core::vfs::{FilePolicy, IdenticalSnapshot, Latest, Vfs};
use std::{ffi::OsStr, path::PathBuf};
use crate::{
Application, RUSTIC_APP, RusticConfig,
repository::{IndexedRepo, get_filtered_snapshots},
status_err,
};
#[derive(Clone, Debug, Default, Command, Parser, Merge, serde::Serialize, serde::Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct MountCmd {
#[clap(long)]
#[merge(strategy=conflate::option::overwrite_none)]
path_template: Option<String>,
#[clap(long)]
#[merge(strategy=conflate::option::overwrite_none)]
time_template: Option<String>,
#[clap(short, long)]
#[merge(strategy=conflate::bool::overwrite_false)]
exclusive: bool,
#[clap(long)]
#[merge(strategy=conflate::option::overwrite_none)]
file_access: Option<FilePolicy>,
#[clap(value_name = "PATH")]
#[merge(strategy=conflate::option::overwrite_none)]
mount_point: Option<PathBuf>,
#[clap(value_name = "SNAPSHOT[:PATH]")]
#[merge(strategy=conflate::option::overwrite_none)]
snapshot_path: Option<String>,
#[clap(short, long = "option", value_name = "OPTION")]
#[merge(strategy = conflate::vec::overwrite_empty)]
options: Vec<String>,
}
impl Override<RusticConfig> for MountCmd {
fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
let self_config = self
.clone()
.merge_from(config.mount)
.merge_from(Self::with_default_config());
if self_config.mount_point.is_none() {
return Err(ParseError
.context("Please specify a valid mount point!")
.into());
}
config.mount = self_config;
Ok(config)
}
}
impl Runnable for MountCmd {
fn run(&self) {
if let Err(err) = RUSTIC_APP
.config()
.repository
.run_indexed(|repo| self.inner_run(repo))
{
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
};
}
}
impl MountCmd {
fn with_default_config() -> Self {
Self {
path_template: Some(String::from("[{hostname}]/[{label}]/{time}")),
time_template: Some(String::from("%Y-%m-%d_%H-%M-%S")),
options: Vec::new(),
..Default::default()
}
}
fn inner_run(&self, repo: IndexedRepo) -> Result<()> {
let config = RUSTIC_APP.config();
let Some(path_template) = config.mount.path_template.clone() else {
bail!("Please specify a path template!");
};
let Some(time_template) = config.mount.time_template.clone() else {
bail!("Please specify a time template!");
};
let Some(mount_point) = config.mount.mount_point.clone() else {
bail!("Please specify a mount point!");
};
let vfs = if let Some(snap) = &config.mount.snapshot_path {
let node =
repo.node_from_snapshot_path(snap, |sn| config.snapshot_filter.matches(sn))?;
Vfs::from_dir_node(&node)
} else {
let snapshots = get_filtered_snapshots(&repo)?;
Vfs::from_snapshots(
snapshots,
&path_template,
&time_template,
Latest::AsLink,
IdenticalSnapshot::AsLink,
)?
};
let mut mount_options = config.mount.options.clone();
mount_options.push(format!("fsname=rusticfs:{}", repo.config().id));
if !config.mount.exclusive {
mount_options
.extend_from_slice(&["allow_other".to_string(), "default_permissions".to_string()]);
}
let file_access = config.mount.file_access.as_ref().map_or_else(
|| {
if repo.config().is_hot == Some(true) {
FilePolicy::Forbidden
} else {
FilePolicy::Read
}
},
|s| *s,
);
let fs = FuseMT::new(FuseFS::new(repo, vfs, file_access), 1);
mount_options.sort_unstable();
mount_options.dedup();
let opt_string = format!("-o{}", mount_options.join(","));
info!(
"mounting {}, press Ctrl-C to cancel...",
mount_point.display()
);
mount(fs, mount_point, &[OsStr::new(&opt_string)])?;
Ok(())
}
}