use clap::ArgAction;
use clap::Parser;
use home::home_dir;
use rand::Rng;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_json::json;
use std::path::PathBuf;
use crate::errors::GalionError;
use crate::librclone::rclone::Rclone;
use crate::remote::ConfigOrigin;
use crate::remote::RemoteConfiguration;
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct GalionConfig {
pub(crate) remote_configurations: Vec<RemoteConfiguration>,
#[serde(skip)]
pub(crate) config_path: PathBuf,
}
impl GalionConfig {
fn load_config(config_path: Option<PathBuf>) -> Result<GalionConfig, GalionError> {
let config_path = config_path.unwrap_or(GalionConfig::get_default_config_path()?);
if !config_path.exists() {
if let Some(parent) = config_path.parent() {
std::fs::create_dir_all(parent)?;
}
let config_json = serde_json::to_string(&GalionConfig::default())?;
std::fs::write(&config_path, config_json)?;
}
let config_data = std::fs::read_to_string(&config_path)?;
let mut loaded_config = serde_json::from_str::<GalionConfig>(&config_data)?;
loaded_config.config_path = config_path;
Ok(loaded_config)
}
pub fn get_default_config_path() -> Result<PathBuf, GalionError> {
let mut path = home_dir().ok_or("Unable to get home directory")?;
path.push(".config");
path.push(APP_NAME);
path.push("galion.json");
Ok(path)
}
pub fn remotes(&self) -> &[RemoteConfiguration] {
&self.remote_configurations
}
pub fn save_config(&self) -> Result<(), GalionError> {
let remotes_to_save = self
.remote_configurations
.iter()
.filter(|c| c.config_origin == ConfigOrigin::GalionConfig)
.cloned()
.collect::<Vec<RemoteConfiguration>>();
let config = GalionConfig {
remote_configurations: remotes_to_save,
config_path: self.config_path.clone(),
};
std::fs::write(&self.config_path, serde_json::to_string(&config)?)?;
Ok(())
}
}
#[derive(Parser, Debug)]
#[command(name = "galion", version, about = "Galion CLI")]
#[allow(clippy::struct_excessive_bools)]
pub struct GalionArgs {
#[arg(long, value_name = "FILE")]
config: Option<PathBuf>,
#[arg(long, value_name = "FILE")]
rclone_config: Option<PathBuf>,
#[arg(long)]
rclone_ask_password: bool,
#[arg(long, action=ArgAction::SetTrue)]
pub(crate) hide_banner: bool,
#[arg(long, action=ArgAction::SetTrue)]
ignore_duplicate_remote: bool,
}
#[derive(Debug)]
pub struct GalionApp {
pub(crate) galion_args: GalionArgs,
pub(crate) config: GalionConfig,
pub(crate) rclone: Rclone,
}
const APP_NAME: &str = "galion";
impl GalionApp {
const GALION: &str = r" _~
_~ )_)_~
)_))_))_)
_!__!__!_
\______t/";
pub(crate) const WAVES: &str = "~~~~~~~~~~~~";
#[must_use]
pub fn logo() -> String {
format!("{}\n{}", Self::GALION, Self::WAVES)
}
#[must_use]
pub fn logo_random_waves() -> String {
let mut rng = rand::rng();
let roll: u32 = rng.random_range(0..=9);
let mut chars: Vec<char> = Self::WAVES.chars().collect();
let len = chars.len();
if roll > 5 && len >= 3 {
let idx = rng.random_range(2..len - 3);
chars[idx] = '-';
chars[idx + 1] = '=';
chars[idx + 2] = '-';
}
let waves: String = chars.into_iter().collect();
format!("{}\n{}", Self::GALION, waves)
}
#[must_use]
pub fn logo_waves() -> String {
format!("{}\n{}", Self::GALION, Self::WAVES)
}
pub fn try_from_galion_args(galion_args: GalionArgs) -> Result<Self, GalionError> {
let config = GalionConfig::load_config(galion_args.config.clone())?;
let galion = Self {
config,
galion_args,
rclone: Rclone::new(),
};
galion.init()
}
fn init(mut self) -> Result<Self, GalionError> {
if let Some(rclone_config_path) = &self.galion_args.rclone_config {
self.rclone
.set_config_path(&rclone_config_path.to_string_lossy())?;
}
if !self.galion_args.hide_banner {
println!("{}", Self::logo());
}
self.rclone.set_config_options(&json!({
"main": {
"LogLevel": "CRITICAL",
},
}))?;
if !self.galion_args.rclone_ask_password {
self.rclone.set_config_options(&json!({
"main": {
"AskPassword": false,
},
}))?;
}
if let Err(e) = self.rclone.dump_config() {
let err_string = e.to_string();
let err_string = if let Ok(j) = serde_json::from_str::<Value>(&err_string)
&& let Some(Value::String(str)) = j.get("error")
{
str.clone()
} else {
err_string
};
let max_len = 80;
let error_msg = if err_string.len() > max_len {
format!("{}...", &err_string[..max_len.saturating_sub(3)])
} else {
err_string
};
let msg = if self.galion_args.rclone_ask_password {
" and the decryption failed"
} else {
"and you can retry with the --rclone-ask-password flag"
};
return Err(GalionError::new(format!(
"Failed to get the rclone configuration. Most likely the configuration is encrypted {msg}.\nRclone internal error: {error_msg}"
)));
}
let list_remotes = self.rclone.list_remotes()?;
for rclone_remote_name in list_remotes {
if self
.config
.remote_configurations
.iter()
.any(|r| r.remote_name == rclone_remote_name)
&& self.galion_args.ignore_duplicate_remote
{
continue;
}
let remote_conf = self.rclone.get_remote(&rclone_remote_name)?;
let remote_dest = remote_conf
.get("remote")
.and_then(|v| v.as_str())
.map(String::from);
let remote_config = RemoteConfiguration {
remote_name: rclone_remote_name,
remote_src: None,
remote_dest,
config_origin: ConfigOrigin::RcloneConfig,
};
self.config.remote_configurations.push(remote_config);
}
if self.config.remote_configurations.is_empty() {
return Err(GalionError::new(format!(
"No remote found in rclone 'config/listremotes' and in the galion config at {} - please add remote with rclone CLI",
self.config.config_path.display()
)));
}
Ok(self)
}
}