[feat] better installer + changelog test version switch + better video config view
This commit is contained in:
429
src-tauri/src/tool/updater.rs
Normal file
429
src-tauri/src/tool/updater.rs
Normal file
@@ -0,0 +1,429 @@
|
||||
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<String>,
|
||||
pub pub_date: Option<String>,
|
||||
pub download_url: String,
|
||||
pub signature: Option<String>,
|
||||
}
|
||||
|
||||
/// 自定义更新服务器响应格式
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CustomUpdateResponse {
|
||||
version: String,
|
||||
notes: Option<String>,
|
||||
pub_date: Option<String>,
|
||||
download_url: String,
|
||||
signature: Option<String>,
|
||||
platforms: Option<PlatformDownloads>,
|
||||
}
|
||||
|
||||
/// 平台特定的下载链接
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PlatformDownloads {
|
||||
#[serde(rename = "windows-x86_64")]
|
||||
windows_x86_64: Option<PlatformInfo>,
|
||||
#[serde(rename = "darwin-x86_64")]
|
||||
darwin_x86_64: Option<PlatformInfo>,
|
||||
#[serde(rename = "darwin-aarch64")]
|
||||
darwin_aarch64: Option<PlatformInfo>,
|
||||
#[serde(rename = "linux-x86_64")]
|
||||
linux_x86_64: Option<PlatformInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PlatformInfo {
|
||||
url: String,
|
||||
signature: Option<String>,
|
||||
}
|
||||
|
||||
/// GitHub Release API 响应
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GitHubRelease {
|
||||
tag_name: String,
|
||||
name: Option<String>,
|
||||
body: Option<String>,
|
||||
published_at: Option<String>,
|
||||
assets: Vec<GitHubAsset>,
|
||||
}
|
||||
|
||||
#[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<Option<UpdateInfo>> {
|
||||
// 首先尝试自定义服务器
|
||||
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<Option<UpdateInfo>> {
|
||||
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<Option<UpdateInfo>> {
|
||||
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<String> {
|
||||
#[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::<u32>().ok()).unwrap_or(0);
|
||||
let v2_part = v2_parts.get(i).and_then(|s| s.parse::<u32>().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<Box<dyn Fn(u64, u64) + Send + Sync>>,
|
||||
) -> Result<PathBuf> {
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user