use anyhow::{bail, Result};
use bitslideslib::{enough, slide, Algorithm, CollisionPolicy, GlobalConfig, RootsetConfig};
use chrono::prelude::*;
use config::DEFAULT_KEYWORD;
use std::path::PathBuf;
#[cfg(not(test))]
use anyhow::anyhow;
#[cfg(not(test))]
use log::LevelFilter;
#[cfg(not(test))]
use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
mod cli;
mod config;
fn generate_trace_path(trace_fmt: &str) -> Option<PathBuf> {
let now: DateTime<Local> = Local::now();
let trace = PathBuf::from(
trace_fmt
.replace("%Y", &format!("{:04}", &now.year()))
.replace("%m", &format!("{:02}", &now.month()))
.replace("%d", &format!("{:02}", &now.day()))
.replace("%H", &format!("{:02}", &now.hour()))
.replace("%M", &format!("{:02}", &now.minute()))
.replace("%S", &format!("{:02}", &now.second())),
);
let trace = std::path::absolute(trace).ok()?;
if !trace.exists() {
let trace_parent = trace.parent().unwrap();
if !trace_parent.exists() {
log::error!("{trace:?}: Neither trace or parent folder does exist");
return None;
}
}
Some(trace)
}
fn process_all_configs(
config_paths: Vec<&PathBuf>,
) -> Result<(Vec<RootsetConfig>, Option<PathBuf>)> {
let mut success = false;
let mut rootsets = Vec::new();
let mut trace = None;
for config_path in config_paths {
log::info!("Loading configuration from: {config_path:?}...");
if config_path.exists() {
match config::Config::new(config_path) {
Ok(config) => {
let keyword = config.keyword.unwrap_or(DEFAULT_KEYWORD.to_owned());
let roots = config
.roots
.into_iter()
.map(|x| {
if x.contains("$") {
unimplemented!("Environment variables not supported yet");
}
let x = PathBuf::from(x);
if x.is_absolute() {
x
} else {
PathBuf::from(config_path.parent().unwrap()).join(x)
}
})
.collect::<Vec<PathBuf>>();
rootsets.push(RootsetConfig { keyword, roots });
if let Some(trace_fmt) = config.trace {
trace = generate_trace_path(&trace_fmt);
}
}
Err(e) => {
log::error!("{config_path:?}: Invalid config format: {e}");
continue;
}
};
} else {
log::error!("{config_path:?}: Config not found");
continue;
}
success = true;
}
if !success {
bail!("No valid configuration file found");
}
Ok((rootsets, trace))
}
async fn main_w_args(
args: &[String],
shutdown_signal: tokio::sync::oneshot::Receiver<()>,
) -> Result<()> {
let matches = cli::cli().get_matches_from(args);
let config_files = matches
.get_many::<PathBuf>("config")
.expect("No configuration file found among provided/defaults.");
let dry_run = matches.get_flag("dry-run");
let non_safe = matches.get_flag("non-safe");
let retries = matches.get_one::<u8>("retries").unwrap();
#[cfg(not(test))]
{
let verbosity = *matches.get_one::<u8>("verbose").unwrap_or(&0);
TermLogger::init(
match verbosity {
0 => LevelFilter::Error,
1 => LevelFilter::Warn,
2 => LevelFilter::Info,
3 => LevelFilter::Debug,
_ => LevelFilter::Trace,
},
Config::default(),
TerminalMode::Stderr,
ColorChoice::Auto,
)
.map_err(|_| anyhow!("Unable to initialize log"))?;
if dry_run && verbosity < 2 {
bail!("Dry-run mode is enabled, but the verbosity level is too low to see the output");
}
}
let (rootsets, trace) = process_all_configs(config_files.into_iter().collect())?;
let keep_alive = slide(GlobalConfig {
rootsets,
dry_run,
trace,
check: Some(Algorithm::BLAKE),
collision: CollisionPolicy::Fail,
safe: !non_safe,
retries: *retries,
})
.await?;
shutdown_signal.await?;
enough(keep_alive).await
}
#[tokio::main]
async fn main() -> Result<()> {
let args = std::env::args().collect::<Vec<_>>();
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
let shutdown_tx = std::sync::Mutex::new(Some(shutdown_tx));
ctrlc::set_handler(move || {
log::info!("Received Ctrl+C, shutting down...");
if let Some(tx) = shutdown_tx.lock().unwrap().take() {
let _ = tx.send(());
}
})
.expect("Failed to set Ctrl+C handler");
main_w_args(&args, shutdown_rx).await
}
#[cfg(test)]
mod tests;