use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; use std::process::Command; use tauri::path::BaseDirectory; use tauri::Manager; #[cfg(windows)] use std::os::windows::process::CommandExt; #[cfg(windows)] const CREATE_NO_WINDOW: u32 = 0x08000000; /// 更新信息结构 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdateInfo { pub version: String, pub notes: Option, pub pub_date: Option, pub download_url: String, pub signature: Option, } /// 自定义更新服务器响应格式 #[derive(Debug, Deserialize)] struct CustomUpdateResponse { version: String, notes: Option, pub_date: Option, download_url: String, signature: Option, platforms: Option, } /// 平台特定的下载链接 #[derive(Debug, Deserialize)] struct PlatformDownloads { #[serde(rename = "windows-x86_64")] windows_x86_64: Option, #[serde(rename = "darwin-x86_64")] darwin_x86_64: Option, #[serde(rename = "darwin-aarch64")] darwin_aarch64: Option, #[serde(rename = "linux-x86_64")] linux_x86_64: Option, } #[derive(Debug, Deserialize)] struct PlatformInfo { url: String, signature: Option, } /// GitHub Release API 响应 #[derive(Debug, Deserialize)] struct GitHubRelease { tag_name: String, name: Option, body: Option, published_at: Option, assets: Vec, } #[derive(Debug, Deserialize)] struct GitHubAsset { name: String, browser_download_url: String, content_type: String, } /// 检查更新 /// /// # 参数 /// - `custom_endpoint`: 自定义更新服务器端点 URL(可选) /// - `github_repo`: GitHub 仓库(格式:owner/repo,可选) /// - `current_version`: 当前应用版本 /// /// # 返回 /// 如果有更新,返回 UpdateInfo;否则返回 None pub async fn check_update( custom_endpoint: Option<&str>, github_repo: Option<&str>, current_version: &str, ) -> Result> { // 首先尝试自定义服务器 if let Some(endpoint) = custom_endpoint { if !endpoint.is_empty() { match check_custom_update(endpoint).await { Ok(Some(info)) => { if compare_versions(&info.version, current_version) > 0 { return Ok(Some(info)); } } Err(e) => { log::warn!("自定义更新服务器检查失败: {}", e); } Ok(None) => {} } } } // Fallback 到 GitHub Release if let Some(repo) = github_repo { match check_github_update(repo).await { Ok(Some(info)) => { if compare_versions(&info.version, current_version) > 0 { return Ok(Some(info)); } } Err(e) => { log::warn!("GitHub Release 检查失败: {}", e); } Ok(None) => {} } } Ok(None) } /// 检查自定义更新服务器 async fn check_custom_update(endpoint: &str) -> Result> { let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(10)) .build()?; let response = client.get(endpoint).send().await?; if !response.status().is_success() { return Err(anyhow::anyhow!("HTTP 状态码: {}", response.status())); } let text = response.text().await?; let update_resp: CustomUpdateResponse = serde_json::from_str(&text) .context("解析自定义更新服务器响应失败")?; // 获取平台特定的下载链接 let download_url = if let Some(platforms) = update_resp.platforms { #[cfg(target_os = "windows")] { #[cfg(target_arch = "x86_64")] { platforms .windows_x86_64 .map(|p| p.url) .unwrap_or(update_resp.download_url) } } #[cfg(target_os = "macos")] { #[cfg(target_arch = "x86_64")] { platforms .darwin_x86_64 .map(|p| p.url) .unwrap_or(update_resp.download_url) } #[cfg(target_arch = "aarch64")] { platforms .darwin_aarch64 .map(|p| p.url) .unwrap_or(update_resp.download_url) } } #[cfg(target_os = "linux")] { #[cfg(target_arch = "x86_64")] { platforms .linux_x86_64 .map(|p| p.url) .unwrap_or(update_resp.download_url) } } } else { update_resp.download_url }; Ok(Some(UpdateInfo { version: update_resp.version, notes: update_resp.notes, pub_date: update_resp.pub_date, download_url, signature: update_resp.signature, })) } /// 检查 GitHub Release async fn check_github_update(repo: &str) -> Result> { let url = format!("https://api.github.com/repos/{}/releases/latest", repo); let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(10)) .user_agent("CS工具箱/1.0") .build()?; let response = client.get(&url).send().await?; if !response.status().is_success() { return Err(anyhow::anyhow!("HTTP 状态码: {}", response.status())); } let release: GitHubRelease = response.json().await?; // 从 tag_name 中提取版本号(移除可能的 'v' 前缀) let version = release.tag_name.trim_start_matches('v').to_string(); // 查找适合当前平台的安装包 let download_url = find_platform_asset(&release.assets)?; Ok(Some(UpdateInfo { version, notes: release.body, pub_date: release.published_at, download_url, signature: None, // GitHub Release 通常不包含签名 })) } /// 查找适合当前平台的资源文件 fn find_platform_asset(assets: &[GitHubAsset]) -> Result { #[cfg(target_os = "windows")] { // 查找 .exe 或 .msi 或 .nsis 安装包 for asset in assets { let name = asset.name.to_lowercase(); if name.ends_with(".exe") || name.ends_with(".msi") || (name.contains("windows") && name.contains("x86_64")) { return Ok(asset.browser_download_url.clone()); } } } #[cfg(target_os = "macos")] { #[cfg(target_arch = "x86_64")] { for asset in assets { let name = asset.name.to_lowercase(); if name.ends_with(".dmg") { if name.contains("x86_64") || name.contains("darwin-x86_64") || (!name.contains("aarch64") && !name.contains("arm64")) { return Ok(asset.browser_download_url.clone()); } } } } #[cfg(target_arch = "aarch64")] { for asset in assets { let name = asset.name.to_lowercase(); if name.ends_with(".dmg") { if name.contains("aarch64") || name.contains("darwin-aarch64") || name.contains("arm64") || (!name.contains("x86_64") && !name.contains("intel")) { return Ok(asset.browser_download_url.clone()); } } } } } #[cfg(target_os = "linux")] { for asset in assets { let name = asset.name.to_lowercase(); if name.ends_with(".deb") || name.ends_with(".rpm") || name.ends_with(".appimage") || (name.contains("linux") && name.contains("x86_64")) { return Ok(asset.browser_download_url.clone()); } } } // 如果找不到特定平台的,返回第一个资源 if let Some(asset) = assets.first() { return Ok(asset.browser_download_url.clone()); } Err(anyhow::anyhow!("未找到适合当前平台的安装包")) } /// 比较版本号 /// 返回: 1 表示 version1 > version2, -1 表示 version1 < version2, 0 表示相等 fn compare_versions(version1: &str, version2: &str) -> i32 { let v1_parts: Vec<&str> = version1 .split(|c: char| c == '.' || c == '-' || c == '_') .collect(); let v2_parts: Vec<&str> = version2 .split(|c: char| c == '.' || c == '-' || c == '_') .collect(); let max_len = v1_parts.len().max(v2_parts.len()); for i in 0..max_len { let v1_part = v1_parts.get(i).and_then(|s| s.parse::().ok()).unwrap_or(0); let v2_part = v2_parts.get(i).and_then(|s| s.parse::().ok()).unwrap_or(0); if v1_part > v2_part { return 1; } else if v1_part < v2_part { return -1; } } 0 } /// 下载更新文件 pub async fn download_update( app: &tauri::AppHandle, download_url: &str, progress_callback: Option>, ) -> Result { let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(300)) .build()?; let response = client.get(download_url).send().await?; if !response.status().is_success() { return Err(anyhow::anyhow!("下载失败,HTTP 状态码: {}", response.status())); } let total_size = response.content_length().unwrap_or(0); // 获取缓存目录 let cache_dir = app .path() .resolve("updates", BaseDirectory::AppCache) .context("无法获取缓存目录")?; fs::create_dir_all(&cache_dir)?; // 从 URL 中提取文件名 let filename = download_url .split('/') .last() .unwrap_or("update") .split('?') .next() .unwrap_or("update"); let file_path = cache_dir.join(filename); // 下载文件 let mut file = fs::File::create(&file_path)?; let mut stream = response.bytes_stream(); let mut downloaded: u64 = 0; use futures_util::StreamExt; use std::io::Write; while let Some(item) = stream.next().await { let chunk = item?; file.write_all(&chunk)?; downloaded += chunk.len() as u64; if let Some(ref callback) = progress_callback { callback(downloaded, total_size); } } file.sync_all()?; Ok(file_path) } /// 安装更新(Windows NSIS) #[cfg(target_os = "windows")] pub fn install_update(installer_path: &str) -> Result<()> { // 使用静默安装参数 let mut cmd = Command::new(installer_path); cmd.args(&["/S", "/D=C:\\Program Files\\CS工具箱"]); cmd.creation_flags(CREATE_NO_WINDOW); cmd.spawn()?; Ok(()) } /// 安装更新(macOS) #[cfg(target_os = "macos")] pub fn install_update(installer_path: &str) -> Result<()> { // macOS 通常需要用户手动安装 DMG // 这里打开安装包 Command::new("open") .arg(installer_path) .spawn()?; Ok(()) } /// 安装更新(Linux) #[cfg(target_os = "linux")] pub fn install_update(installer_path: &str) -> Result<()> { // Linux 根据文件类型选择安装方式 if installer_path.ends_with(".deb") { Command::new("sudo") .args(&["dpkg", "-i", installer_path]) .spawn()?; } else if installer_path.ends_with(".rpm") { Command::new("sudo") .args(&["rpm", "-i", installer_path]) .spawn()?; } else if installer_path.ends_with(".AppImage") { // AppImage 通常只需要设置执行权限 Command::new("chmod") .args(&["+x", installer_path]) .spawn()?; } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_compare_versions() { assert_eq!(compare_versions("1.0.1", "1.0.0"), 1); assert_eq!(compare_versions("1.0.0", "1.0.1"), -1); assert_eq!(compare_versions("1.0.0", "1.0.0"), 0); assert_eq!(compare_versions("0.0.6-beta.4", "0.0.6"), 0); // 简单比较,不考虑 beta } }