dev-manager-tauri/src-tauri/src/commands/server_software.rs
lanrtop 9660bcbc48 feat: 服务器管理体系 + 云产品扩展 + 源码库 + API密钥管理
新增多个核心功能模块(后端 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 个新模块
2026-04-17 22:25:06 +09:00

454 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(())
}