use super::MhoConfig;
use super::get_config_dir;
use super::patcher::patch_config;
use crate::cli::UpgradeRepo;
use crate::error::{MhoError, Result};
use crate::log_info;
use crate::utils::download::download_text;
use crate::utils::fs::{atomic_write, ensure_dir, file_exists};
use crate::utils::{is_url, strip_suffix};
use easy_install::{InstallConfig, ei};
use github_proxy::{Proxy, Resource};
use std::path::Path;
impl MhoConfig {
pub async fn install(&self, force: bool) -> Result<()> {
log_info!("Installing proxy core and UI (force: {})", force);
self.ensure_default_config()?;
self.install_core(force).await?;
self.install_ui(force).await?;
self.install_geo(force).await?;
log_info!("Installation completed successfully");
Ok(())
}
fn ensure_default_config(&self) -> Result<()> {
let config_path = self.core_config_path();
if config_path.exists() {
return Ok(());
}
log_info!(
"Creating default configuration file: {}",
config_path.display()
);
let default_config = include_str!("../assets/mihomo.yaml");
atomic_write(&config_path, default_config)?;
Ok(())
}
pub async fn install_core(&self, force: bool) -> Result<()> {
let exe_path = self.core.exe_path(&get_config_dir());
if file_exists(&exe_path) && !force {
log_info!("Core already installed at {}", exe_path.display());
return Ok(());
}
log_info!("Installing proxy core: {}", self.core.name());
ensure_dir(&get_config_dir())?;
let resource = self.core.repo(&self.target)?;
let url = self
.proxy
.url(resource)
.ok_or_else(|| MhoError::Download("Failed to get core download URL".to_string()))?;
log_info!("Downloading core from: {}", url);
let result = ei(
&url,
&self.ei_config(get_config_dir(), Some(self.core.name().to_string())),
)
.await;
if result.is_err() {
return Err(MhoError::Download(
"Failed to install core binary".to_string(),
));
}
if !file_exists(&exe_path) {
return Err(MhoError::Download(format!(
"Core binary not found after installation: {}",
exe_path.display()
)));
}
log_info!("Core installed successfully at {}", exe_path.display());
Ok(())
}
pub fn ei_config<T: AsRef<Path>>(&self, dir: T, alias: Option<String>) -> InstallConfig {
easy_install::InstallConfig {
dir: Some(dir.as_ref().to_string_lossy().to_string()),
no_path: true,
proxy: self.proxy,
alias,
target: Some(self.target),
..Default::default()
}
}
pub async fn install_ui(&self, force: bool) -> Result<()> {
let config_dir = get_config_dir();
let ui_dir = self.web.ui_dir(&config_dir);
if ui_dir.exists() && !force {
log_info!("UI already installed at {}", ui_dir.display());
return Ok(());
}
log_info!("Installing web UI: {}", self.web.ui_name());
let url = self.web.ui_url()?;
log_info!("Downloading UI from: {}", url);
let result = ei(
&url,
&self.ei_config(config_dir.join(self.web.ui_name()), None),
)
.await;
if result.is_err() {
return Err(MhoError::Download("Failed to install UI".to_string()));
}
if !ui_dir.exists() {
return Err(MhoError::Download(format!(
"UI directory not found after installation: {}",
ui_dir.display()
)));
}
log_info!("UI installed successfully at {}", ui_dir.display());
Ok(())
}
pub async fn install_geo(&self, force: bool) -> Result<()> {
log_info!("Installing GeoIP databases");
for name in self.core.get_geo_files() {
let Some(url) = Resource::File {
owner: "ahaoboy".to_string(),
repo: "mho-assets".to_string(),
reference: "main".to_string(),
path: name.to_string(),
}
.url(&Proxy::Github) else {
log_info!("Database {} not found.", name);
continue;
};
let db_path = get_config_dir().join(strip_suffix(name));
if file_exists(&db_path) && !force {
log_info!("Database {} already exists", name);
continue;
}
log_info!("Downloading GeoIP database: {}", name);
if ei(&url, &self.ei_config(get_config_dir(), None))
.await
.is_ok()
{
log_info!("Downloaded {} successfully", name);
} else {
log_info!("Downloaded {} error", name);
}
}
log_info!("GeoIP databases installed successfully");
Ok(())
}
pub async fn upgrade(&self, repo: UpgradeRepo) -> Result<()> {
let exe = std::env::current_exe()?;
let dir = exe
.parent()
.ok_or_else(|| MhoError::Download("mho dir not found".to_string()))?;
let url = match repo {
UpgradeRepo::Mho => "ahaoboy/mho",
UpgradeRepo::MhoAssets => "ahaoboy/mho-assets",
};
ei(
url,
&InstallConfig {
name: vec!["mho".to_string()],
upx: repo == UpgradeRepo::Mho,
..self.ei_config(dir, Some("mho".to_string()))
},
)
.await
.map_err(|e| MhoError::Download(e.to_string()))?;
Ok(())
}
pub async fn update_config(&self, force: bool) -> Result<()> {
let dest = &self.core_config_path();
let source = &self.url;
if source.is_empty() {
return Err(MhoError::Config(
"Configuration URL is empty. Please set it first with 'mho config url <url>'"
.to_string(),
));
}
if file_exists(dest) && !force {
log_info!("Configuration file already exists at {}", dest.display());
return Ok(());
}
log_info!("Updating configuration from: {}", source);
let content = if is_url(source) {
log_info!("Downloading configuration from URL: {}", source);
download_text(source).await.map_err(|e| {
MhoError::Config(format!("Failed to download configuration from URL: {}", e))
})?
} else {
let source_path = Path::new(source);
if !source_path.exists() {
return Err(MhoError::Config(format!(
"Configuration source not found: {} (not a valid URL or local file)",
source
)));
}
log_info!("Reading configuration from local file: {}", source);
std::fs::read_to_string(source_path).map_err(|e| {
MhoError::Config(format!("Failed to read local configuration file: {}", e))
})?
};
let patched_content = patch_config(self.core, &self.web, &content);
std::fs::write(dest, patched_content).map_err(|e| {
MhoError::Config(format!(
"Failed to write configuration to {}: {}",
dest.display(),
e
))
})?;
log_info!("Configuration updated successfully");
Ok(())
}
}