use crate::config::core::Core;
use crate::error::{MhoError, Result};
use crate::log_info;
use crate::utils::fs::{atomic_write, ensure_dir};
use crate::utils::get_dir_size;
use github_proxy::Proxy;
use guess_target::{Target, get_local_target};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
pub mod core;
pub mod installer;
pub mod patcher;
pub mod runtime;
pub mod web;
pub use web::WebConfig;
const APP_CONFIG_DIR: &str = "mho_config";
const APP_CONFIG_NAME: &str = "mho_config.json";
const APP_LOG_DIR: &str = "logs";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MhoConfig {
pub version: String,
pub start_time: u64,
pub core: Core,
pub proxy: Proxy,
pub target: Target,
pub web: WebConfig,
pub url: String,
pub max_runtime_hours: u64,
#[serde(default)]
pub stop_force: bool,
#[serde(default)]
pub check_url: Option<String>,
}
impl Default for MhoConfig {
fn default() -> Self {
Self {
version: env!("CARGO_PKG_VERSION").to_string(),
start_time: 0,
core: Core::default(),
proxy: Proxy::default(),
target: *get_local_target().first().unwrap_or(&Target::default()),
web: WebConfig::default(),
url: String::new(),
stop_force: false,
max_runtime_hours: 0,
check_url: None,
}
}
}
impl MhoConfig {
pub fn load() -> Result<Self> {
let config_path = get_config_path();
log_info!("Loading configuration from {}", config_path.display());
let config = if config_path.exists() {
let data = std::fs::read_to_string(&config_path).map_err(|e| {
MhoError::Config(format!(
"Failed to read config file {}: {}",
config_path.display(),
e
))
})?;
let config: MhoConfig = serde_json::from_str(&data)
.map_err(|e| MhoError::Config(format!("Failed to parse config file: {}", e)))?;
config.validate()?;
config
} else {
log_info!("Config file not found, creating default configuration");
let config = Self::default();
config.save()?;
config
};
Ok(config)
}
pub fn save(&self) -> Result<()> {
let config_path = get_config_path();
log_info!("Saving configuration to {}", config_path.display());
ensure_dir(&get_config_dir())?;
let json = serde_json::to_string_pretty(self)
.map_err(|e| MhoError::Config(format!("Failed to serialize config: {}", e)))?;
atomic_write(&config_path, &json)?;
Ok(())
}
pub fn validate(&self) -> Result<()> {
if get_config_dir().to_str().is_none() {
return Err(MhoError::Config(
"Config directory path contains invalid UTF-8".to_string(),
));
}
let host = self.web.host.trim();
if host.is_empty() {
return Err(MhoError::Config("Web host is empty".to_string()));
}
let Some(port_part) = host.rsplit_once(':') else {
return Err(MhoError::Config(format!(
"Invalid web host format (expected `[host]:port`): {}",
self.web.host
)));
};
if port_part.1.parse::<u16>().is_err() {
return Err(MhoError::Config(format!(
"Invalid web host port (expected 0-65535): {}",
self.web.host
)));
}
Ok(())
}
pub fn core_config_path(&self) -> PathBuf {
get_config_dir().join(self.core.config_file_name())
}
pub fn get_size(&self) -> u64 {
get_dir_size(&get_config_dir())
}
}
pub fn get_config_dir() -> PathBuf {
std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|parent| parent.to_path_buf()))
.unwrap_or_else(|| PathBuf::from("."))
.join(APP_CONFIG_DIR)
}
pub fn get_config_path() -> PathBuf {
get_config_dir().join(APP_CONFIG_NAME)
}
pub fn get_log_dir() -> PathBuf {
get_config_dir().join(APP_LOG_DIR)
}