#![allow(clippy::redundant_field_names)]
#[macro_use]
extern crate lazy_static;
use anyhow::{bail, Context, Result};
use clap::*;
use daemonize::*;
use log::*;
use simplelog::*;
pub mod fs;
use fs::*;
#[cfg(not(feature = "notifications"))]
fn notify<S: AsRef<str>>(_: S) {}
#[cfg(feature = "notifications")]
fn notify<S: AsRef<str>>(msg: S) {
use notify_rust::Notification;
Notification::new()
.summary("FUSTA")
.body(msg.as_ref())
.show()
.unwrap();
}
#[derive(Debug, Clone)]
struct RunEnvironment {
mountpoint: std::path::PathBuf,
created_mountpoint: bool,
}
fn main() -> Result<()> {
human_panic::setup_panic!();
let args =
App::new("fusta")
.setting(AppSettings::ColoredHelp)
.setting(AppSettings::ColorAuto)
.setting(AppSettings::UnifiedHelpMessage)
.version(crate_version!())
.author(crate_authors!())
.arg(Arg::with_name("FASTA")
.help("A (multi)FASTA file containing the sequences to mount")
.required(true)
.index(1))
.arg(Arg::with_name("verbose")
.short('v')
.action(ArgAction::Count)
.help("Sets the level of verbosity"))
.arg(Arg::with_name("mountpoint")
.short('o')
.long("mountpoint")
.help("Specifies the directory to use as mountpoint; it will be created if it does not exist")
.takes_value(true))
.arg(Arg::with_name("nodaemon")
.short('D')
.long("no-daemon")
.help("Do not daemonize"))
.arg(Arg::with_name("max-cache")
.short('C')
.long("max-cache")
.help("Set the maximum amount of memory to use to cache writes (MB)")
.default_value("500")
.takes_value(true))
.arg(Arg::with_name("cache")
.long("cache")
.help("Use either mmap, fseek(2) or memory-backed cache to extract sequences from FASTA files. WARNING: memory caching use as much RAM as the size of the FASTA file should be available.")
.possible_values(&["file", "mmap", "memory"])
.default_value("mmap"))
.arg(Arg::with_name("csv-separator")
.short('S')
.long("sep")
.help("Set the separator to use in CSV files")
.default_value(",")
.takes_value(true))
.arg(Arg::with_name("overwrite")
.short('W')
.long("allow-overwrite")
.help("allow FUSTA to overwrite existing sequences, when (i) appending new sequences conflicting with an existing ID, (ii) renaming sequences"))
.get_matches();
let log_level = match args.get_one::<u8>("verbose").copied().unwrap_or_default() {
0 => LevelFilter::Info,
1 => LevelFilter::Debug,
2 => LevelFilter::Trace,
_ => LevelFilter::Trace,
};
let log_config = ConfigBuilder::new().build();
let mut loggers: Vec<Box<dyn SharedLogger>> = vec![TermLogger::new(
log_level,
log_config.clone(),
TerminalMode::Mixed,
simplelog::ColorChoice::Auto,
)];
if !args.is_present("nodaemon") {
let log_file_path = tempfile::Builder::new()
.prefix("fusta-")
.suffix(".log")
.tempfile()
.context("Unable to create a temporary file")?;
println!(
"Logs ({:?}) available in {}",
log_level,
log_file_path.path().display()
);
loggers.push(WriteLogger::new(log_level, log_config, log_file_path));
}
CombinedLogger::init(loggers).context("Unable to init logger")?;
let fasta_file = value_t!(args, "FASTA", String)?;
let mountpoint = value_t!(args, "mountpoint", String).unwrap_or(format!(
"fusta-{}",
std::path::Path::new(&fasta_file)
.file_stem()
.and_then(|s| s.to_str())
.context(format!("{:?} is not a valid path", &fasta_file))?
));
let fuse_options: Vec<fuser::MountOption> = vec![
fuser::MountOption::FSName("FUSTA".to_string()),
fuser::MountOption::DefaultPermissions,
];
let settings = FustaSettings {
cache: match args.value_of("cache").unwrap() {
"mmap" => fs::Cache::Mmap,
"file" => fs::Cache::File,
"memory" => fs::Cache::RAM,
_ => unreachable!(),
},
concretize_threshold: value_t!(args, "max-cache", usize).unwrap() * 1024 * 1024,
csv_separator: value_t!(args, "csv-separator", String).unwrap(),
no_overwrite: args.is_present("overwrite"),
};
info!("Caching method: {:#?}", settings.cache);
let fs = FustaFS::new(settings, &fasta_file)?;
let mut env = RunEnvironment {
mountpoint: std::path::PathBuf::from(mountpoint),
created_mountpoint: false,
};
if !env.mountpoint.exists() {
std::fs::create_dir(&env.mountpoint)?;
env.created_mountpoint = true;
}
if !env.mountpoint.is_dir() {
bail!("mount point `{:?}` is not a directory", env.mountpoint);
}
if std::fs::read_dir(&env.mountpoint)?.take(1).count() != 0 {
bail!("mount point {:?} is not empty.", env.mountpoint);
}
let umount_msg = if cfg!(target_os = "freebsd") || cfg!(target_os = "macos") {
format!(
"Please use `umount {:?}` to exit.",
&env.mountpoint.canonicalize().unwrap()
)
} else {
format!(
"Please use `fusermount -u {0:?}` or `umount {0:?}` to exit.",
&env.mountpoint.canonicalize().unwrap()
)
};
info!("{}", &umount_msg);
{
ctrlc::set_handler(move || {
error!("{}", umount_msg);
})?;
}
if !args.is_present("nodaemon") {
let pid_file = tempfile::Builder::new()
.prefix("fusta-")
.suffix(".pid")
.tempfile()
.context("Unable to create a temporary PID file")?;
Daemonize::new()
.pid_file(pid_file)
.working_directory(std::env::current_dir().context("Unable to read current directory")?)
.start()?;
}
notify(format!(
"{} is now available in {:#?}",
&fasta_file, &env.mountpoint
));
match fuser::mount2(fs, &env.mountpoint, &fuse_options) {
Ok(()) => {}
_ => {
error!("Unable to mount the FUSE filesystem");
std::process::exit(1);
}
}
cleanup(&env)?;
Ok(())
}
fn cleanup(env: &RunEnvironment) -> Result<()> {
notify("Successfully unmounted");
if env.created_mountpoint {
notify(format!(
"You can now safely remove the {:?} directory",
env.mountpoint
));
warn!(
"You can now safely delete the {:?} directory",
env.mountpoint
);
}
Ok(())
}