use super::mirror::{Mirror, TestResult};
use super::tester::MirrorTester;
use crate::config::Config;
use crate::distro::DistroHandler;
use crate::utils::SMirrorsError;
use anyhow::{Context, Result};
use std::sync::Arc;
use tracing::{error, info, warn};
use url::Url;
pub struct MirrorUpdater {
config: Config,
tester: MirrorTester,
distro_handler: Arc<dyn DistroHandler>,
}
#[derive(Debug, Clone)]
pub struct UpdateOptions {
pub dry_run: bool,
pub force: bool,
pub limit: Option<usize>,
}
impl Default for UpdateOptions {
fn default() -> Self {
Self {
dry_run: false,
force: false,
limit: None,
}
}
}
#[derive(Debug, Clone)]
pub struct UpdateResult {
pub success: bool,
pub mirrors_tested: usize,
pub mirrors_selected: usize,
pub static_mirrors_count: usize,
pub error: Option<String>,
pub dry_run: bool,
}
impl MirrorUpdater {
pub fn new(config: Config, distro_handler: Arc<dyn DistroHandler>) -> Result<Self> {
let tester = MirrorTester::from_config(&config)?;
Ok(Self {
config,
tester,
distro_handler,
})
}
pub async fn update(&self, options: &UpdateOptions) -> Result<UpdateResult> {
info!(
"Starting mirror update for distribution: {}",
self.distro_handler.name()
);
if options.dry_run {
info!("Running in dry-run mode - no changes will be made");
}
let current_mirrors = self
.distro_handler
.get_current_mirrors()
.context("Failed to get current mirrors")?;
info!("Current configuration has {} mirrors", current_mirrors.len());
let static_mirrors = self.load_static_mirrors()?;
info!("Loaded {} static mirrors from config", static_mirrors.len());
let mut available_mirrors = self
.distro_handler
.get_available_mirrors()
.await
.context("Failed to fetch available mirrors")?;
info!("Found {} available mirrors", available_mirrors.len());
if let Some(limit) = options.limit {
available_mirrors.truncate(limit);
info!("Limited to {} mirrors for testing", limit);
}
info!("Testing {} mirrors...", available_mirrors.len());
let test_results = self.tester.test_all(available_mirrors, None).await;
let successful_results = MirrorTester::filter_successful(test_results);
info!(
"{} mirrors tested successfully",
successful_results.len()
);
if successful_results.is_empty() {
error!("No mirrors passed testing!");
return Ok(UpdateResult {
success: false,
mirrors_tested: 0,
mirrors_selected: 0,
static_mirrors_count: static_mirrors.len(),
error: Some("No mirrors passed testing".to_string()),
dry_run: options.dry_run,
});
}
let ranked_mirrors = self.rank_and_select_mirrors(successful_results)?;
info!("Selected {} top-ranked mirrors", ranked_mirrors.len());
let final_mirrors = self.combine_with_static_mirrors(ranked_mirrors, static_mirrors);
info!(
"Final mirror list contains {} mirrors ({} static)",
final_mirrors.len(),
final_mirrors.iter().filter(|m| m.is_static).count()
);
if options.dry_run {
info!("Dry-run complete. Selected mirrors:");
for (i, mirror) in final_mirrors.iter().enumerate() {
info!(
" {}. {} - Score: {} ({})",
i + 1,
mirror.url,
mirror.format_score(),
if mirror.is_static { "static" } else { "dynamic" }
);
}
return Ok(UpdateResult {
success: true,
mirrors_tested: final_mirrors.len(),
mirrors_selected: final_mirrors.len(),
static_mirrors_count: final_mirrors.iter().filter(|m| m.is_static).count(),
error: None,
dry_run: true,
});
}
if self.config.distro.create_backup {
info!("Creating backup of current configuration...");
if let Err(e) = self.distro_handler.backup() {
warn!("Failed to create backup: {}", e);
if !options.force {
return Err(e.context("Backup creation failed"));
}
} else {
info!("Backup created successfully");
}
}
info!("Updating mirror configuration...");
if let Err(e) = self.distro_handler.update_mirrors(&final_mirrors) {
error!("Failed to update mirrors: {}", e);
if self.config.distro.create_backup {
warn!("Attempting to rollback to previous configuration...");
if let Err(rollback_err) = self.distro_handler.restore_backup() {
error!("Rollback failed: {}", rollback_err);
return Err(SMirrorsError::UpdateFailed(format!(
"Update failed and rollback also failed: {} (rollback error: {})",
e, rollback_err
))
.into());
}
info!("Rollback successful");
}
return Err(SMirrorsError::UpdateFailed(e.to_string()).into());
}
info!("Validating new configuration...");
match self.distro_handler.validate() {
Ok(true) => {
info!("Configuration validation successful");
}
Ok(false) => {
warn!("Configuration validation returned false");
if self.config.distro.create_backup {
warn!("Rolling back due to validation failure...");
self.distro_handler.restore_backup()?;
}
return Err(SMirrorsError::ValidationFailed(
"Configuration validation failed".to_string(),
)
.into());
}
Err(e) => {
error!("Configuration validation error: {}", e);
if self.config.distro.create_backup {
warn!("Rolling back due to validation error...");
self.distro_handler.restore_backup()?;
}
return Err(e.context("Configuration validation failed"));
}
}
info!("Mirror update completed successfully");
Ok(UpdateResult {
success: true,
mirrors_tested: final_mirrors.len(),
mirrors_selected: final_mirrors.len(),
static_mirrors_count: final_mirrors.iter().filter(|m| m.is_static).count(),
error: None,
dry_run: false,
})
}
fn load_static_mirrors(&self) -> Result<Vec<Mirror>> {
let mut mirrors = Vec::new();
for (name, url_str) in &self.config.static_mirrors {
match Url::parse(url_str) {
Ok(url) => {
let mut mirror = Mirror::new_static(url);
mirror.metadata.insert("name".to_string(), name.clone());
mirrors.push(mirror);
}
Err(e) => {
warn!(
"Failed to parse static mirror '{}' with URL '{}': {}",
name, url_str, e
);
}
}
}
Ok(mirrors)
}
fn rank_and_select_mirrors(&self, results: Vec<TestResult>) -> Result<Vec<Mirror>> {
let mut sorted_results = MirrorTester::sort_by_score(results);
sorted_results.retain(|r| {
if let Some(score) = r.score {
score >= self.config.testing.min_score
} else {
false
}
});
if sorted_results.is_empty() {
return Err(
SMirrorsError::NoMirrorsAvailable.into()
);
}
if !self.config.testing.country_preference.is_empty() {
sorted_results = self.apply_country_preference(sorted_results);
}
let max_mirrors = self.config.testing.max_mirrors;
sorted_results.truncate(max_mirrors);
let mirrors = MirrorTester::extract_mirrors(sorted_results);
Ok(mirrors)
}
fn apply_country_preference(&self, results: Vec<TestResult>) -> Vec<TestResult> {
let (mut preferred, mut others): (Vec<_>, Vec<_>) = results
.into_iter()
.partition(|r| {
if let Some(ref country) = r.mirror.country {
self.config
.testing
.country_preference
.contains(country)
} else {
false
}
});
preferred.append(&mut others);
preferred
}
fn combine_with_static_mirrors(
&self,
mut ranked_mirrors: Vec<Mirror>,
static_mirrors: Vec<Mirror>,
) -> Vec<Mirror> {
let static_urls: Vec<String> = static_mirrors
.iter()
.map(|m| m.url.to_string())
.collect();
ranked_mirrors.retain(|m| !static_urls.contains(&m.url.to_string()));
let mut final_mirrors = static_mirrors;
final_mirrors.extend(ranked_mirrors);
final_mirrors
}
pub async fn test_mirror(&self, url: Url) -> Result<TestResult> {
let mirror = Mirror::new(url);
Ok(self.tester.test_mirror(&mirror).await)
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn distro_handler(&self) -> &Arc<dyn DistroHandler> {
&self.distro_handler
}
pub fn rollback(&self) -> Result<()> {
info!("Rolling back to previous configuration...");
self.distro_handler.restore_backup()?;
info!("Rollback completed successfully");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_update_options_default() {
let options = UpdateOptions::default();
assert_eq!(options.dry_run, false);
assert_eq!(options.force, false);
assert!(options.limit.is_none());
}
#[test]
fn test_static_mirror_filtering() {
let url1 = Url::parse("https://mirror1.example.com/repo").unwrap();
let url2 = Url::parse("https://mirror2.example.com/repo").unwrap();
let url3 = Url::parse("https://mirror3.example.com/repo").unwrap();
let mut ranked = vec![
Mirror::new(url1.clone()),
Mirror::new(url2),
Mirror::new(url3.clone()),
];
let static_mirrors = vec![Mirror::new_static(url1.clone())];
let config = Config::default();
let updater = MirrorUpdater {
config: config.clone(),
tester: MirrorTester::from_config(&config).unwrap(),
distro_handler: Arc::new(MockDistroHandler {}),
};
let result = updater.combine_with_static_mirrors(ranked, static_mirrors);
assert_eq!(result.len(), 3);
assert_eq!(result[0].is_static, true);
assert_eq!(result[0].url, url1);
}
struct MockDistroHandler;
#[async_trait::async_trait]
impl DistroHandler for MockDistroHandler {
fn name(&self) -> &str {
"Mock"
}
fn detect(&self) -> bool {
true
}
async fn get_available_mirrors(&self) -> Result<Vec<Mirror>> {
Ok(Vec::new())
}
fn get_current_mirrors(&self) -> Result<Vec<Mirror>> {
Ok(Vec::new())
}
fn update_mirrors(&self, _mirrors: &[Mirror]) -> Result<()> {
Ok(())
}
fn backup(&self) -> Result<()> {
Ok(())
}
fn restore_backup(&self) -> Result<()> {
Ok(())
}
fn validate(&self) -> Result<bool> {
Ok(true)
}
}
}