use super::args::{Cli, Commands, ConfigAction, OutputFormat, ServiceAction, SortBy};
use crate::config::Config;
use crate::core::{Mirror, MirrorTester, MirrorUpdater, TestResult, UpdateOptions};
use crate::distro::detect_handler;
use crate::storage::{BackupManager, Database, MirrorFilter, UpdateRecord};
use crate::utils::SMirrorsError;
use anyhow::{Context, Result};
use std::io::{self, Write};
use std::process::Command as ProcessCommand;
use std::sync::Arc;
use tracing::{debug, info};
use url::Url;
pub async fn handle_command(cli: Cli) -> Result<()> {
crate::utils::init_cli_logger(cli.log_level())?;
debug!("Executing command: {:?}", cli.command);
match cli.command {
Commands::Test {
count,
format,
success_only,
sort,
} => test_command(cli.config, count, format, success_only, sort).await,
Commands::Update {
dry_run,
force,
limit,
yes,
} => update_command(cli.config, dry_run, force, limit, yes).await,
Commands::List {
static_only,
with_tests,
format,
} => list_command(cli.config, static_only, with_tests, format).await,
Commands::Add {
repo,
url,
skip_validation,
} => add_command(cli.config, repo, url, skip_validation).await,
Commands::Remove { mirror, yes } => remove_command(cli.config, mirror, yes).await,
Commands::Tui => tui_command().await,
Commands::Status { detailed, format } => status_command(detailed, format).await,
Commands::History {
count,
format,
success_only,
failed_only,
} => history_command(count, format, success_only, failed_only).await,
Commands::Rollback {
backup_id,
yes,
list,
} => rollback_command(cli.config, backup_id, yes, list).await,
Commands::Enable { now } => enable_command(now).await,
Commands::Disable { stop } => disable_command(stop).await,
Commands::Config { action } => config_command(cli.config, action).await,
Commands::Init { force, skip_service } => init_command(force, skip_service).await,
Commands::Service { action } => service_command(cli.config, action).await,
}
}
async fn test_command(
config_path: Option<std::path::PathBuf>,
count: Option<usize>,
format: OutputFormat,
success_only: bool,
sort: SortBy,
) -> Result<()> {
info!("Starting mirror test");
let config = load_config(config_path)?;
let handler = detect_handler()?;
info!("Detected distribution: {}", handler.name());
println!("Fetching available mirrors...");
let mut mirrors = handler
.get_available_mirrors()
.await
.context("Failed to fetch available mirrors")?;
if let Some(limit) = count {
mirrors.truncate(limit);
}
println!("Testing {} mirrors...", mirrors.len());
let tester = MirrorTester::from_config(&config)?;
let progress_callback = Arc::new(move |current: usize, total: usize, url: &str| {
eprint!(
"\rTesting mirrors: {}/{} - {}",
current, total, url
);
let _ = io::stderr().flush();
});
let results = tester.test_all(mirrors, Some(progress_callback)).await;
eprintln!();
let filtered_results: Vec<TestResult> = if success_only {
results.into_iter().filter(|r| r.success).collect()
} else {
results
};
let sorted_results = sort_test_results(filtered_results, sort);
println!("\nTest Results:");
println!("{}", format_test_results(&sorted_results, format));
let successful = sorted_results.iter().filter(|r| r.success).count();
let failed = sorted_results.len() - successful;
println!("\nSummary:");
println!(" Total: {}", sorted_results.len());
println!(" Successful: {}", successful);
println!(" Failed: {}", failed);
Ok(())
}
async fn update_command(
config_path: Option<std::path::PathBuf>,
dry_run: bool,
force: bool,
limit: Option<usize>,
yes: bool,
) -> Result<()> {
if !dry_run && !nix::unistd::geteuid().is_root() {
return Err(SMirrorsError::PermissionDenied(
"Updating mirror configuration requires root privileges. Try running with sudo"
.to_string(),
)
.into());
}
info!("Starting mirror update");
let config = load_config(config_path)?;
let handler = detect_handler()?;
info!("Detected distribution: {}", handler.name());
let handler_arc: Arc<dyn crate::distro::DistroHandler> = handler.into();
let updater = MirrorUpdater::new(config.clone(), handler_arc)?;
let options = UpdateOptions {
dry_run,
force,
limit,
};
if !dry_run && !yes {
println!("\nWARNING: This will modify your system's package manager configuration.");
println!("A backup will be created before making changes.");
println!();
if !prompt_confirmation("Do you want to continue?")? {
println!("Update cancelled.");
return Ok(());
}
}
println!("Updating mirrors...");
let result = updater.update(&options).await?;
if result.dry_run {
println!("\n{} DRY RUN RESULTS {}", "=".repeat(25), "=".repeat(25));
} else {
println!("\n{} UPDATE RESULTS {}", "=".repeat(25), "=".repeat(25));
}
println!("Status: {}", if result.success { "Success" } else { "Failed" });
println!("Mirrors tested: {}", result.mirrors_tested);
println!("Mirrors selected: {}", result.mirrors_selected);
println!("Static mirrors: {}", result.static_mirrors_count);
if let Some(ref error) = result.error {
println!("Error: {}", error);
}
if result.success && !result.dry_run {
println!("\n{} Mirror configuration updated successfully!", "✓");
println!("Run 'sudo apt update' (or equivalent) to use the new mirrors.");
if let Ok(db) = get_database() {
let _ = db.save_update_record(
result.mirrors_selected as i64,
true,
None,
);
}
} else if result.success && result.dry_run {
println!("\n{} Dry run completed. Use --no-dry-run to apply changes.", "ℹ");
}
Ok(())
}
async fn list_command(
config_path: Option<std::path::PathBuf>,
static_only: bool,
with_tests: bool,
format: OutputFormat,
) -> Result<()> {
info!("Listing current mirrors");
let _config = load_config(config_path)?;
let mirrors = if with_tests {
let db = get_database()?;
let filter = MirrorFilter {
static_only,
tested_only: false,
country: None,
min_score: None,
};
db.get_mirrors(&filter)?
} else {
let handler = detect_handler()?;
let mut mirrors = handler.get_current_mirrors()?;
if static_only {
mirrors.retain(|m| m.is_static);
}
mirrors
};
if mirrors.is_empty() {
println!("No mirrors found.");
return Ok(());
}
println!("Current Mirrors ({} total):", mirrors.len());
println!("{}", format_mirrors(&mirrors, format, with_tests));
Ok(())
}
async fn add_command(
config_path: Option<std::path::PathBuf>,
repo: Option<String>,
url: String,
skip_validation: bool,
) -> Result<()> {
info!("Adding static mirror: {}", url);
let parsed_url = Url::parse(&url).context("Invalid URL format")?;
if !skip_validation {
print!("Validating mirror URL...");
io::stdout().flush()?;
if let Err(e) = crate::utils::check_url_reachable(&parsed_url).await {
eprintln!(" Failed!");
return Err(anyhow::anyhow!("Mirror URL is not reachable: {}", e));
}
println!(" OK");
}
let mut config = load_config(config_path.clone())?;
let repo_name = repo.unwrap_or_else(|| {
parsed_url
.host_str()
.unwrap_or("mirror")
.to_string()
});
if config.static_mirrors.contains_key(&repo_name) {
return Err(anyhow::anyhow!(
"Static mirror '{}' already exists. Use remove first or choose a different name.",
repo_name
));
}
config.static_mirrors.insert(repo_name.clone(), url.clone());
config.save().context("Failed to save configuration")?;
println!("✓ Static mirror '{}' added successfully: {}", repo_name, url);
Ok(())
}
async fn remove_command(
config_path: Option<std::path::PathBuf>,
mirror: String,
yes: bool,
) -> Result<()> {
info!("Removing mirror: {}", mirror);
let mut config = load_config(config_path.clone())?;
let mirror_key = if config.static_mirrors.contains_key(&mirror) {
mirror.clone()
} else {
config
.static_mirrors
.iter()
.find(|(_, url)| url.as_str() == mirror)
.map(|(name, _)| name.clone())
.ok_or_else(|| anyhow::anyhow!("Mirror '{}' not found in static mirrors", mirror))?
};
let mirror_url = config.static_mirrors.get(&mirror_key).unwrap().clone();
if !yes {
println!("Mirror to remove:");
println!(" Name: {}", mirror_key);
println!(" URL: {}", mirror_url);
println!();
if !prompt_confirmation("Are you sure you want to remove this mirror?")? {
println!("Removal cancelled.");
return Ok(());
}
}
config.static_mirrors.remove(&mirror_key);
config.save().context("Failed to save configuration")?;
println!("✓ Static mirror '{}' removed successfully", mirror_key);
Ok(())
}
async fn tui_command() -> Result<()> {
println!("Interactive TUI is not yet implemented.");
println!();
println!("In the meantime, you can use these commands:");
println!(" smirrors test - Test available mirrors");
println!(" smirrors update - Update mirror configuration");
println!(" smirrors list - List current mirrors");
println!(" smirrors status - Show service status");
println!(" smirrors history - View update history");
println!();
println!("Run 'smirrors --help' for more information.");
Ok(())
}
async fn status_command(detailed: bool, format: OutputFormat) -> Result<()> {
info!("Checking service status");
let service_status = get_systemd_service_status("smirrors.service")?;
let timer_status = get_systemd_service_status("smirrors.timer")?;
match format {
OutputFormat::Json => {
let json = serde_json::json!({
"service": service_status,
"timer": timer_status,
});
println!("{}", serde_json::to_string_pretty(&json)?);
}
_ => {
println!("SMirrors Service Status:");
println!();
println!("Service: {}", service_status.active);
println!("Timer: {}", timer_status.active);
println!();
if detailed {
println!("Detailed Service Status:");
println!("{}", service_status.status_output);
println!();
println!("Detailed Timer Status:");
println!("{}", timer_status.status_output);
}
if let Ok(db) = get_database() {
if let Ok(Some(last_update)) = db.get_latest_update() {
println!();
println!("Last Update:");
println!(" Time: {}", last_update.updated_at.format("%Y-%m-%d %H:%M:%S UTC"));
println!(" Mirrors changed: {}", last_update.mirrors_changed);
println!(" Status: {}", if last_update.success { "Success" } else { "Failed" });
if let Some(ref error) = last_update.error {
println!(" Error: {}", error);
}
}
}
}
}
Ok(())
}
async fn history_command(
count: usize,
format: OutputFormat,
success_only: bool,
failed_only: bool,
) -> Result<()> {
info!("Retrieving update history");
let db = get_database()?;
let mut history = db.get_history(count)?;
if success_only {
history.retain(|h| h.success);
} else if failed_only {
history.retain(|h| !h.success);
}
if history.is_empty() {
println!("No update history found.");
return Ok(());
}
println!("Update History ({} entries):", history.len());
println!("{}", format_history(&history, format));
Ok(())
}
async fn rollback_command(
config_path: Option<std::path::PathBuf>,
backup_id: Option<String>,
yes: bool,
list: bool,
) -> Result<()> {
if !nix::unistd::geteuid().is_root() {
return Err(SMirrorsError::PermissionDenied(
"Rollback requires root privileges. Try running with sudo".to_string(),
)
.into());
}
let config = load_config(config_path)?;
let backup_dir = Config::data_dir()?.join("backups");
let backup_manager = BackupManager::new(&backup_dir, config.distro.backup_count)?;
if list {
let backups = backup_manager.list_backups()?;
if backups.is_empty() {
println!("No backups found.");
return Ok(());
}
println!("Available Backups:");
for backup in &backups {
println!();
println!(" ID: {}", backup.id);
println!(" Created: {}", backup.created_at.format("%Y-%m-%d %H:%M:%S UTC"));
println!(" Files: {}", backup.file_count);
println!(" Size: {}", crate::utils::format_size(backup.total_size as usize));
if let Some(ref desc) = backup.description {
println!(" Description: {}", desc);
}
}
return Ok(());
}
let target_backup = if let Some(id) = backup_id {
id
} else {
backup_manager
.get_latest_backup_id()?
.ok_or_else(|| anyhow::anyhow!("No backups available to restore"))?
};
let backups = backup_manager.list_backups()?;
let backup_info = backups
.iter()
.find(|b| b.id == target_backup)
.ok_or_else(|| anyhow::anyhow!("Backup '{}' not found", target_backup))?;
if !yes {
println!("Backup to restore:");
println!(" ID: {}", backup_info.id);
println!(" Created: {}", backup_info.created_at.format("%Y-%m-%d %H:%M:%S UTC"));
println!(" Files: {}", backup_info.file_count);
println!();
println!("WARNING: This will overwrite your current mirror configuration.");
println!();
if !prompt_confirmation("Do you want to continue?")? {
println!("Rollback cancelled.");
return Ok(());
}
}
println!("Restoring backup...");
let restored_count = backup_manager.restore_backup(&target_backup, true)?;
println!("✓ Successfully restored {} files from backup '{}'", restored_count, target_backup);
println!("Run 'sudo apt update' (or equivalent) to use the restored mirrors.");
if let Ok(db) = get_database() {
let _ = db.save_update_record(
restored_count as i64,
true,
Some(format!("Rollback to backup {}", target_backup)),
);
}
Ok(())
}
async fn enable_command(now: bool) -> Result<()> {
if !nix::unistd::geteuid().is_root() {
return Err(SMirrorsError::PermissionDenied(
"Enabling service requires root privileges. Try running with sudo".to_string(),
)
.into());
}
info!("Enabling SMirrors service");
println!("Enabling SMirrors timer...");
systemctl_command("enable", "smirrors.timer")?;
println!("Starting SMirrors timer...");
systemctl_command("start", "smirrors.timer")?;
println!("✓ SMirrors automatic updates enabled");
if now {
println!("Starting SMirrors service...");
systemctl_command("start", "smirrors.service")?;
println!("✓ SMirrors service started");
}
println!();
let timer_status = get_systemd_service_status("smirrors.timer")?;
println!("Timer status: {}", timer_status.active);
Ok(())
}
async fn disable_command(stop: bool) -> Result<()> {
if !nix::unistd::geteuid().is_root() {
return Err(SMirrorsError::PermissionDenied(
"Disabling service requires root privileges. Try running with sudo".to_string(),
)
.into());
}
info!("Disabling SMirrors service");
println!("Stopping SMirrors timer...");
systemctl_command("stop", "smirrors.timer")?;
println!("Disabling SMirrors timer...");
systemctl_command("disable", "smirrors.timer")?;
println!("✓ SMirrors automatic updates disabled");
if stop {
println!("Stopping SMirrors service...");
systemctl_command("stop", "smirrors.service")?;
println!("✓ SMirrors service stopped");
}
Ok(())
}
async fn config_command(
config_path: Option<std::path::PathBuf>,
action: Option<ConfigAction>,
) -> Result<()> {
match action {
None | Some(ConfigAction::Show { .. }) => {
let raw = matches!(action, Some(ConfigAction::Show { raw: true, .. }));
let section = match action {
Some(ConfigAction::Show { section, .. }) => section,
_ => None,
};
config_show(config_path, raw, section).await
}
Some(ConfigAction::Set { key, value }) => config_set(config_path, key, value).await,
Some(ConfigAction::Get { key }) => config_get(config_path, key).await,
Some(ConfigAction::Edit { validate }) => config_edit(config_path, validate).await,
Some(ConfigAction::Validate { verbose }) => config_validate(config_path, verbose).await,
Some(ConfigAction::Reset { section, yes }) => config_reset(config_path, section, yes).await,
}
}
async fn config_show(
config_path: Option<std::path::PathBuf>,
raw: bool,
section: Option<String>,
) -> Result<()> {
let config = load_config(config_path)?;
if raw {
let toml = toml::to_string_pretty(&config)?;
println!("{}", toml);
} else if let Some(section_name) = section {
match section_name.as_str() {
"general" => println!("{:#?}", config.general),
"testing" => println!("{:#?}", config.testing),
"distro" => println!("{:#?}", config.distro),
"logging" => println!("{:#?}", config.logging),
"notifications" => println!("{:#?}", config.notifications),
"static_mirrors" => println!("{:#?}", config.static_mirrors),
_ => return Err(anyhow::anyhow!("Unknown section: {}", section_name)),
}
} else {
println!("{:#?}", config);
}
Ok(())
}
async fn config_set(
config_path: Option<std::path::PathBuf>,
key: String,
value: String,
) -> Result<()> {
let mut config = load_config(config_path.clone())?;
config.set(&key, &value)?;
config.save()?;
println!("✓ Configuration updated: {} = {}", key, value);
Ok(())
}
async fn config_get(config_path: Option<std::path::PathBuf>, key: String) -> Result<()> {
let config = load_config(config_path)?;
if let Some(value) = config.get(&key) {
println!("{}", value);
} else {
return Err(anyhow::anyhow!("Configuration key '{}' not found", key));
}
Ok(())
}
async fn config_edit(config_path: Option<std::path::PathBuf>, validate: bool) -> Result<()> {
let config_file = config_path.unwrap_or_else(|| Config::config_path().unwrap());
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
let status = ProcessCommand::new(&editor)
.arg(&config_file)
.status()
.with_context(|| format!("Failed to open editor '{}'", editor))?;
if !status.success() {
return Err(anyhow::anyhow!("Editor exited with non-zero status"));
}
if validate {
println!("Validating configuration...");
match Config::load_from(&config_file) {
Ok(_) => println!("✓ Configuration is valid"),
Err(e) => {
eprintln!("✗ Configuration validation failed: {}", e);
return Err(e);
}
}
}
Ok(())
}
async fn config_validate(config_path: Option<std::path::PathBuf>, verbose: bool) -> Result<()> {
let config_file = config_path.unwrap_or_else(|| Config::config_path().unwrap());
println!("Validating configuration at: {:?}", config_file);
match Config::load_from(&config_file) {
Ok(config) => {
println!("✓ Configuration is valid");
if verbose {
println!();
println!("Configuration details:");
println!("{:#?}", config);
}
Ok(())
}
Err(e) => {
eprintln!("✗ Configuration validation failed:");
eprintln!("{}", e);
Err(e)
}
}
}
async fn config_reset(
config_path: Option<std::path::PathBuf>,
section: Option<String>,
yes: bool,
) -> Result<()> {
let mut config = load_config(config_path.clone())?;
if !yes {
let message = if let Some(ref s) = section {
format!("Are you sure you want to reset the '{}' section to defaults?", s)
} else {
"Are you sure you want to reset ALL configuration to defaults?".to_string()
};
if !prompt_confirmation(&message)? {
println!("Reset cancelled.");
return Ok(());
}
}
if let Some(section_name) = section {
match section_name.as_str() {
"general" => config.general = Default::default(),
"testing" => config.testing = Default::default(),
"distro" => config.distro = Default::default(),
"logging" => config.logging = Default::default(),
"notifications" => config.notifications = Default::default(),
"static_mirrors" => config.static_mirrors.clear(),
_ => return Err(anyhow::anyhow!("Unknown section: {}", section_name)),
}
println!("✓ Section '{}' reset to defaults", section_name);
} else {
config = Config::default();
println!("✓ All configuration reset to defaults");
}
config.save()?;
Ok(())
}
async fn init_command(force: bool, skip_service: bool) -> Result<()> {
info!("Initializing SMirrors");
let is_root = nix::unistd::geteuid().is_root();
let config_path = Config::config_path()?;
if config_path.exists() && !force {
println!("Configuration already exists at: {:?}", config_path);
println!("Use --force to reinitialize.");
return Ok(());
}
println!("Creating configuration...");
let config = Config::default();
config.save()?;
println!("✓ Configuration created at: {:?}", config_path);
println!("Creating data directories...");
let data_dir = Config::data_dir()?;
std::fs::create_dir_all(&data_dir)?;
println!("✓ Data directory created at: {:?}", data_dir);
let cache_dir = Config::cache_dir()?;
std::fs::create_dir_all(&cache_dir)?;
println!("✓ Cache directory created at: {:?}", cache_dir);
println!("Initializing database...");
let db_path = data_dir.join("smirrors.db");
let _db = Database::new(&db_path)?;
println!("✓ Database initialized at: {:?}", db_path);
if is_root && !skip_service {
println!("Installing systemd service...");
println!("ℹ Systemd service installation not implemented yet.");
println!(" Service files should be installed to /etc/systemd/system/");
}
println!();
println!("✓ SMirrors initialization complete!");
println!();
println!("Next steps:");
println!(" 1. Review configuration: smirrors config show");
println!(" 2. Test mirrors: smirrors test");
println!(" 3. Update mirrors: sudo smirrors update");
if is_root && !skip_service {
println!(" 4. Enable automatic updates: sudo smirrors enable");
}
Ok(())
}
async fn service_command(
config_path: Option<std::path::PathBuf>,
action: ServiceAction,
) -> Result<()> {
match action {
ServiceAction::Run => {
println!("Service mode not yet implemented");
Ok(())
}
ServiceAction::Update => {
update_command(config_path, false, false, None, true).await
}
}
}
fn load_config(config_path: Option<std::path::PathBuf>) -> Result<Config> {
if let Some(path) = config_path {
Config::load_from(&path)
} else {
Config::load()
}
}
fn get_database() -> Result<Database> {
let db_path = Config::data_dir()?.join("smirrors.db");
Database::new(&db_path)
}
fn prompt_confirmation(message: &str) -> Result<bool> {
print!("{} [y/N] ", message);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes"))
}
fn systemctl_command(action: &str, unit: &str) -> Result<()> {
let output = ProcessCommand::new("systemctl")
.arg(action)
.arg(unit)
.output()
.context("Failed to execute systemctl")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow::anyhow!("systemctl {} {} failed: {}", action, unit, stderr));
}
Ok(())
}
#[derive(Debug, Clone, serde::Serialize)]
struct ServiceStatus {
active: String,
status_output: String,
}
fn get_systemd_service_status(unit: &str) -> Result<ServiceStatus> {
let output = ProcessCommand::new("systemctl")
.arg("is-active")
.arg(unit)
.output()
.context("Failed to check service status")?;
let active = String::from_utf8_lossy(&output.stdout).trim().to_string();
let status_output = ProcessCommand::new("systemctl")
.arg("status")
.arg(unit)
.arg("--no-pager")
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_else(|_| "Unable to get status".to_string());
Ok(ServiceStatus {
active,
status_output,
})
}
fn sort_test_results(mut results: Vec<TestResult>, sort_by: SortBy) -> Vec<TestResult> {
match sort_by {
SortBy::Score => {
results.sort_by(|a, b| {
let score_a = a.score.unwrap_or(0.0);
let score_b = b.score.unwrap_or(0.0);
score_b.partial_cmp(&score_a).unwrap_or(std::cmp::Ordering::Equal)
});
}
SortBy::Speed => {
results.sort_by(|a, b| {
let speed_a = a.speed.unwrap_or(0.0);
let speed_b = b.speed.unwrap_or(0.0);
speed_b.partial_cmp(&speed_a).unwrap_or(std::cmp::Ordering::Equal)
});
}
SortBy::Latency => {
results.sort_by(|a, b| {
let lat_a = a.latency.map(|d| d.as_millis()).unwrap_or(u128::MAX);
let lat_b = b.latency.map(|d| d.as_millis()).unwrap_or(u128::MAX);
lat_a.cmp(&lat_b)
});
}
SortBy::Url => {
results.sort_by(|a, b| a.mirror.url.as_str().cmp(b.mirror.url.as_str()));
}
}
results
}
fn format_test_results(results: &[TestResult], format: OutputFormat) -> String {
match format {
OutputFormat::Json => {
serde_json::to_string_pretty(results).unwrap_or_else(|_| "[]".to_string())
}
OutputFormat::Table | OutputFormat::Pretty => {
if results.is_empty() {
return "No results".to_string();
}
let mut output = String::new();
output.push_str(&format!(
"{:<50} {:<12} {:<12} {:<10} {:<10}\n",
"URL", "Speed", "Latency", "Score", "Status"
));
output.push_str(&"-".repeat(100));
output.push('\n');
for result in results {
let url = truncate_string(&result.mirror.url_string(), 48);
let speed = result.speed.map(|s| format!("{:.2} MB/s", s)).unwrap_or_else(|| "N/A".to_string());
let latency = result.latency.map(|l| format!("{} ms", l.as_millis())).unwrap_or_else(|| "N/A".to_string());
let score = result.score.map(|s| format!("{:.1}%", s * 100.0)).unwrap_or_else(|| "N/A".to_string());
let status = if result.success { "✓" } else { "✗" };
output.push_str(&format!(
"{:<50} {:<12} {:<12} {:<10} {:<10}\n",
url, speed, latency, score, status
));
}
output
}
OutputFormat::Compact => {
results
.iter()
.map(|r| {
format!(
"{} | {} | {} | {}",
r.mirror.url,
r.mirror.format_speed(),
r.mirror.format_latency(),
r.mirror.format_score()
)
})
.collect::<Vec<_>>()
.join("\n")
}
}
}
fn format_mirrors(mirrors: &[Mirror], format: OutputFormat, with_tests: bool) -> String {
match format {
OutputFormat::Json => {
serde_json::to_string_pretty(mirrors).unwrap_or_else(|_| "[]".to_string())
}
OutputFormat::Table | OutputFormat::Pretty => {
if mirrors.is_empty() {
return "No mirrors".to_string();
}
let mut output = String::new();
if with_tests {
output.push_str(&format!(
"{:<50} {:<12} {:<12} {:<10} {:<8}\n",
"URL", "Speed", "Latency", "Score", "Type"
));
} else {
output.push_str(&format!("{:<70} {:<8}\n", "URL", "Type"));
}
output.push_str(&"-".repeat(if with_tests { 100 } else { 80 }));
output.push('\n');
for mirror in mirrors {
if with_tests {
let url = truncate_string(&mirror.url_string(), 48);
let mirror_type = if mirror.is_static { "Static" } else { "Dynamic" };
output.push_str(&format!(
"{:<50} {:<12} {:<12} {:<10} {:<8}\n",
url,
mirror.format_speed(),
mirror.format_latency(),
mirror.format_score(),
mirror_type
));
} else {
let url = truncate_string(&mirror.url_string(), 68);
let mirror_type = if mirror.is_static { "Static" } else { "Dynamic" };
output.push_str(&format!("{:<70} {:<8}\n", url, mirror_type));
}
}
output
}
OutputFormat::Compact => {
mirrors
.iter()
.map(|m| {
let type_marker = if m.is_static { "[S]" } else { "[D]" };
format!("{} {}", type_marker, m.url)
})
.collect::<Vec<_>>()
.join("\n")
}
}
}
fn format_history(history: &[UpdateRecord], format: OutputFormat) -> String {
match format {
OutputFormat::Json => {
let records: Vec<_> = history
.iter()
.map(|h| {
serde_json::json!({
"id": h.id,
"mirrors_changed": h.mirrors_changed,
"success": h.success,
"error": h.error,
"updated_at": h.updated_at.to_rfc3339(),
})
})
.collect();
serde_json::to_string_pretty(&records).unwrap_or_else(|_| "[]".to_string())
}
OutputFormat::Table | OutputFormat::Pretty => {
if history.is_empty() {
return "No history".to_string();
}
let mut output = String::new();
output.push_str(&format!(
"{:<5} {:<22} {:<10} {:<10} {:<30}\n",
"ID", "Timestamp", "Mirrors", "Status", "Error"
));
output.push_str(&"-".repeat(80));
output.push('\n');
for record in history {
let timestamp = record.updated_at.format("%Y-%m-%d %H:%M:%S").to_string();
let status = if record.success { "✓ Success" } else { "✗ Failed" };
let error = record
.error
.as_ref()
.map(|e| truncate_string(e, 28))
.unwrap_or_else(|| "-".to_string());
output.push_str(&format!(
"{:<5} {:<22} {:<10} {:<10} {:<30}\n",
record.id, timestamp, record.mirrors_changed, status, error
));
}
output
}
OutputFormat::Compact => {
history
.iter()
.map(|h| {
format!(
"{} | {} | {} | {}",
h.updated_at.format("%Y-%m-%d %H:%M"),
h.mirrors_changed,
if h.success { "OK" } else { "FAIL" },
h.error.as_deref().unwrap_or("-")
)
})
.collect::<Vec<_>>()
.join("\n")
}
}
}
fn truncate_string(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len.saturating_sub(3)])
}
}