use crate::db::pool; use rusqlite::params; use serde::{Deserialize, Serialize}; use std::process::Command; // ── 数据结构 ────────────────────────────────────────────────────────────────── #[derive(Debug, Serialize, Deserialize, Clone)] pub struct SoftwareCategory { pub id: String, pub name: String, pub color: String, pub sort_order: i32, } #[derive(Debug, Deserialize)] pub struct SaveCategoryInput { pub name: String, pub color: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct GithubMirror { pub id: String, pub name: String, pub url_prefix: String, pub is_builtin: bool, pub sort_order: i32, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ServerNetworkConfig { pub server_id: String, pub github_mirror_id: Option, pub last_tested: Option, pub test_results: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ServerSoftware { pub id: String, pub name: String, pub description: Option, pub category: Option, pub install_type: String, pub install_cmd: Option, pub github_url: Option, pub remote_path: Option, pub post_cmd: Option, pub is_builtin: bool, pub sort_order: i32, pub created_at: Option, } #[derive(Debug, Deserialize)] pub struct SaveSoftwareInput { pub name: String, pub description: Option, pub category: Option, pub install_type: String, pub install_cmd: Option, pub github_url: Option, pub remote_path: Option, pub post_cmd: Option, } #[derive(Debug, Deserialize)] pub struct SaveMirrorInput { pub name: String, pub url_prefix: String, } #[derive(Debug, Serialize)] pub struct MirrorTestResult { pub mirror_id: String, pub reachable: bool, pub latency_ms: Option, pub error: Option, } // ── 镜像 CRUD ───────────────────────────────────────────────────────────────── #[tauri::command] pub fn get_github_mirrors() -> Result, String> { let conn = pool().get().map_err(|e| e.to_string())?; let mut stmt = conn .prepare("SELECT id, name, url_prefix, is_builtin, sort_order FROM github_mirrors ORDER BY sort_order") .map_err(|e| e.to_string())?; let mirrors = stmt .query_map([], |row| { Ok(GithubMirror { id: row.get(0)?, name: row.get(1)?, url_prefix: row.get(2)?, is_builtin: row.get::<_, i64>(3)? != 0, sort_order: row.get(4)?, }) }) .map_err(|e| e.to_string())? .filter_map(|r| r.ok()) .collect(); Ok(mirrors) } #[tauri::command] pub fn add_github_mirror(input: SaveMirrorInput) -> Result { let conn = pool().get().map_err(|e| e.to_string())?; let id = format!("mirror-{}", uuid::Uuid::new_v4()); conn.execute( "INSERT INTO github_mirrors (id, name, url_prefix) VALUES (?1, ?2, ?3)", params![id, input.name, input.url_prefix], ) .map_err(|e| e.to_string())?; Ok(GithubMirror { id, name: input.name, url_prefix: input.url_prefix, is_builtin: false, sort_order: 0, }) } #[tauri::command] pub fn delete_github_mirror(id: String) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; // 不允许删除内置镜像 let builtin: bool = conn .query_row( "SELECT is_builtin FROM github_mirrors WHERE id = ?1", params![id], |row| row.get::<_, i64>(0).map(|v| v != 0), ) .unwrap_or(false); if builtin { return Err("内置镜像不可删除".into()); } conn.execute("DELETE FROM github_mirrors WHERE id = ?1", params![id]) .map_err(|e| e.to_string())?; Ok(()) } /// 本地测试镜像可用性(curl) #[tauri::command] pub fn test_mirror_local(mirror_id: String) -> Result { let conn = pool().get().map_err(|e| e.to_string())?; let (url_prefix,): (String,) = conn .query_row( "SELECT url_prefix FROM github_mirrors WHERE id = ?1", params![mirror_id], |row| Ok((row.get(0)?,)), ) .map_err(|_| "镜像不存在".to_string())?; let test_url = if url_prefix.contains("github.com") { // 直连模式 "https://github.com/robots.txt".to_string() } else { format!("{}/https://github.com/robots.txt", url_prefix.trim_end_matches('/')) }; let start = std::time::Instant::now(); let output = Command::new("curl") .args(["-sI", "--max-time", "8", &test_url]) .output(); match output { Ok(out) => { let elapsed = start.elapsed().as_millis() as u64; let stdout = String::from_utf8_lossy(&out.stdout); let first_line = stdout.lines().next().unwrap_or(""); let reachable = first_line.contains("200") || first_line.contains("301") || first_line.contains("302"); Ok(MirrorTestResult { mirror_id, reachable, latency_ms: Some(elapsed), error: if reachable { None } else { Some(first_line.to_string()) }, }) } Err(e) => Ok(MirrorTestResult { mirror_id, reachable: false, latency_ms: None, error: Some(e.to_string()), }), } } // ── 服务器网络配置 ──────────────────────────────────────────────────────────── #[tauri::command] pub fn get_server_network_config(server_id: String) -> Result, String> { let conn = pool().get().map_err(|e| e.to_string())?; conn.query_row( "SELECT server_id, github_mirror_id, last_tested, test_results FROM server_network_config WHERE server_id = ?1", params![server_id], |row| { Ok(ServerNetworkConfig { server_id: row.get(0)?, github_mirror_id: row.get(1)?, last_tested: row.get(2)?, test_results: row.get(3)?, }) }, ) .ok() .map(|c| Ok(Some(c))) .unwrap_or(Ok(None)) } #[tauri::command] pub fn set_server_mirror(server_id: String, mirror_id: Option) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; conn.execute( "INSERT INTO server_network_config (server_id, github_mirror_id) VALUES (?1, ?2) ON CONFLICT(server_id) DO UPDATE SET github_mirror_id = ?2", params![server_id, mirror_id], ) .map_err(|e| e.to_string())?; Ok(()) } // ── 软件 CRUD ───────────────────────────────────────────────────────────────── #[tauri::command] pub fn get_server_software_list() -> Result, String> { let conn = pool().get().map_err(|e| e.to_string())?; let mut stmt = conn .prepare( "SELECT id, name, description, category, install_type, install_cmd, github_url, remote_path, post_cmd, is_builtin, sort_order, created_at FROM server_software ORDER BY sort_order, created_at", ) .map_err(|e| e.to_string())?; let list = stmt .query_map([], |row| { Ok(ServerSoftware { id: row.get(0)?, name: row.get(1)?, description: row.get(2)?, category: row.get(3)?, install_type: row.get(4)?, install_cmd: row.get(5)?, github_url: row.get(6)?, remote_path: row.get(7)?, post_cmd: row.get(8)?, is_builtin: row.get::<_, i64>(9)? != 0, sort_order: row.get(10)?, created_at: row.get(11)?, }) }) .map_err(|e| e.to_string())? .filter_map(|r| r.ok()) .collect(); Ok(list) } #[tauri::command] pub fn add_server_software(input: SaveSoftwareInput) -> Result { let conn = pool().get().map_err(|e| e.to_string())?; let id = format!("sw-{}", uuid::Uuid::new_v4()); conn.execute( "INSERT INTO server_software (id, name, description, category, install_type, install_cmd, github_url, remote_path, post_cmd) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)", params![ id, input.name, input.description, input.category, input.install_type, input.install_cmd, input.github_url, input.remote_path, input.post_cmd, ], ) .map_err(|e| e.to_string())?; Ok(ServerSoftware { id, name: input.name, description: input.description, category: input.category, install_type: input.install_type, install_cmd: input.install_cmd, github_url: input.github_url, remote_path: input.remote_path, post_cmd: input.post_cmd, is_builtin: false, sort_order: 0, created_at: None, }) } #[tauri::command] pub fn update_server_software(id: String, input: SaveSoftwareInput) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; conn.execute( "UPDATE server_software SET name = ?1, description = ?2, category = ?3, install_type = ?4, install_cmd = ?5, github_url = ?6, remote_path = ?7, post_cmd = ?8 WHERE id = ?9", params![ input.name, input.description, input.category, input.install_type, input.install_cmd, input.github_url, input.remote_path, input.post_cmd, id, ], ) .map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub fn delete_server_software(id: String) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; conn.execute("DELETE FROM server_software WHERE id = ?1", params![id]) .map_err(|e| e.to_string())?; Ok(()) } /// 为指定服务器生成某软件的安装命令(自动套镜像) #[tauri::command] pub fn generate_install_command(software_id: String, server_id: String) -> Result, String> { let conn = pool().get().map_err(|e| e.to_string())?; let sw: ServerSoftware = conn .query_row( "SELECT id, name, description, category, install_type, install_cmd, github_url, remote_path, post_cmd, is_builtin, sort_order, created_at FROM server_software WHERE id = ?1", params![software_id], |row| { Ok(ServerSoftware { id: row.get(0)?, name: row.get(1)?, description: row.get(2)?, category: row.get(3)?, install_type: row.get(4)?, install_cmd: row.get(5)?, github_url: row.get(6)?, remote_path: row.get(7)?, post_cmd: row.get(8)?, is_builtin: row.get::<_, i64>(9)? != 0, sort_order: row.get(10)?, created_at: row.get(11)?, }) }, ) .map_err(|_| "软件不存在".to_string())?; let mut commands: Vec = Vec::new(); match sw.install_type.as_str() { "package" => { if let Some(cmd) = &sw.install_cmd { commands.push(cmd.clone()); } } "github" => { if let Some(github_url) = &sw.github_url { // 查该服务器的镜像配置 let mirror_prefix: Option = conn .query_row( "SELECT m.url_prefix FROM server_network_config c JOIN github_mirrors m ON m.id = c.github_mirror_id WHERE c.server_id = ?1", params![server_id], |row| row.get(0), ) .ok(); let download_url = if let Some(prefix) = mirror_prefix { if prefix.contains("github.com") { // 直连模式 github_url.clone() } else { format!("{}/{}", prefix.trim_end_matches('/'), github_url) } } else { github_url.clone() }; let target = sw.remote_path.as_deref().unwrap_or("/tmp/download"); commands.push(format!("wget \"{}\" -O {}", download_url, target)); } } _ => {} } if let Some(post) = &sw.post_cmd { if !post.is_empty() { commands.push(post.clone()); } } Ok(commands) } // ── 软件分类标签 CRUD ───────────────────────────────────────────────────────── #[tauri::command] pub fn get_software_categories() -> Result, String> { let conn = pool().get().map_err(|e| e.to_string())?; let mut stmt = conn .prepare("SELECT id, name, color, sort_order FROM software_categories ORDER BY sort_order") .map_err(|e| e.to_string())?; let list = stmt .query_map([], |row| { Ok(SoftwareCategory { id: row.get(0)?, name: row.get(1)?, color: row.get(2)?, sort_order: row.get(3)?, }) }) .map_err(|e| e.to_string())? .filter_map(|r| r.ok()) .collect(); Ok(list) } #[tauri::command] pub fn add_software_category(input: SaveCategoryInput) -> Result { let conn = pool().get().map_err(|e| e.to_string())?; let id = format!("cat-{}", uuid::Uuid::new_v4()); let color = input.color.unwrap_or_else(|| "#6b7280".to_string()); let max_order: i32 = conn .query_row("SELECT COALESCE(MAX(sort_order), -1) FROM software_categories", [], |r| r.get(0)) .unwrap_or(-1); conn.execute( "INSERT INTO software_categories (id, name, color, sort_order) VALUES (?1, ?2, ?3, ?4)", params![id, input.name, color, max_order + 1], ) .map_err(|e| e.to_string())?; Ok(SoftwareCategory { id, name: input.name, color, sort_order: max_order + 1 }) } #[tauri::command] pub fn update_software_category(id: String, input: SaveCategoryInput) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; let color = input.color.unwrap_or_else(|| "#6b7280".to_string()); conn.execute( "UPDATE software_categories SET name = ?1, color = ?2 WHERE id = ?3", params![input.name, color, id], ) .map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub fn delete_software_category(id: String) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; // 将该分类下的软件改为 "custom" conn.execute( "UPDATE server_software SET category = 'cat-custom' WHERE category = ?1", params![id], ) .map_err(|e| e.to_string())?; conn.execute("DELETE FROM software_categories WHERE id = ?1", params![id]) .map_err(|e| e.to_string())?; Ok(()) }