新增多个核心功能模块(后端 Rust 命令 + 前端 Panel 全部接线完成): - 服务器注册表:servers.rs + ServerManagerPanel、ProjectServerPanel(项目关联服务器) - SSH Config 自动同步:ssh_config.rs - 服务器软件管理:server_software.rs + ServerSoftwareBlock - 宿主机应用管理:host_apps.rs + HostAppsPanel - 服务配置管理:service_config.rs + ServiceConfigBlock - 项目部署信息:deploy_info.rs + DeployInfoPanel - API密钥管理:api_keys.rs + ApiKeysPanel - 云产品管理:cloud_products.rs + CloudProductsPanel(含 Docker应用、云数据库实例子模块) - 源码库:source_library.rs + SourceLibraryPage(分类管理 + 项目详情) 导航栏新增「源码库」入口;蓝图 manifest 同步补录 7 个新模块
454 lines
16 KiB
Rust
454 lines
16 KiB
Rust
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<String>,
|
||
}
|
||
|
||
#[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<String>,
|
||
pub last_tested: Option<String>,
|
||
pub test_results: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct ServerSoftware {
|
||
pub id: String,
|
||
pub name: String,
|
||
pub description: Option<String>,
|
||
pub category: Option<String>,
|
||
pub install_type: String,
|
||
pub install_cmd: Option<String>,
|
||
pub github_url: Option<String>,
|
||
pub remote_path: Option<String>,
|
||
pub post_cmd: Option<String>,
|
||
pub is_builtin: bool,
|
||
pub sort_order: i32,
|
||
pub created_at: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
pub struct SaveSoftwareInput {
|
||
pub name: String,
|
||
pub description: Option<String>,
|
||
pub category: Option<String>,
|
||
pub install_type: String,
|
||
pub install_cmd: Option<String>,
|
||
pub github_url: Option<String>,
|
||
pub remote_path: Option<String>,
|
||
pub post_cmd: Option<String>,
|
||
}
|
||
|
||
#[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<u64>,
|
||
pub error: Option<String>,
|
||
}
|
||
|
||
// ── 镜像 CRUD ─────────────────────────────────────────────────────────────────
|
||
|
||
#[tauri::command]
|
||
pub fn get_github_mirrors() -> Result<Vec<GithubMirror>, 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<GithubMirror, String> {
|
||
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<MirrorTestResult, String> {
|
||
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<Option<ServerNetworkConfig>, 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<String>) -> 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<Vec<ServerSoftware>, 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<ServerSoftware, String> {
|
||
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<Vec<String>, 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<String> = 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<String> = 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<Vec<SoftwareCategory>, 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<SoftwareCategory, String> {
|
||
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(())
|
||
}
|