From ae442113681f578bf4d7ea53cbe13424da3f5c3f Mon Sep 17 00:00:00 2001 From: cysamurai Date: Wed, 20 May 2026 14:48:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- call-client/src-tauri/Cargo.lock | 1 + call-client/src-tauri/Cargo.toml | 1 + call-client/src-tauri/src/commands/update.rs | 246 ++++++++++++++++++- 3 files changed, 247 insertions(+), 1 deletion(-) diff --git a/call-client/src-tauri/Cargo.lock b/call-client/src-tauri/Cargo.lock index a920f76..8c590b5 100644 --- a/call-client/src-tauri/Cargo.lock +++ b/call-client/src-tauri/Cargo.lock @@ -395,6 +395,7 @@ name = "call-client" version = "0.1.5" dependencies = [ "chrono", + "flate2", "fs2", "reqwest 0.12.28", "serde", diff --git a/call-client/src-tauri/Cargo.toml b/call-client/src-tauri/Cargo.toml index 1698757..5ee2d09 100644 --- a/call-client/src-tauri/Cargo.toml +++ b/call-client/src-tauri/Cargo.toml @@ -28,4 +28,5 @@ serde_json = "1" fs2 = "0.4" reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } chrono = { version = "0.4", default-features = false, features = ["clock"] } +flate2 = "1" diff --git a/call-client/src-tauri/src/commands/update.rs b/call-client/src-tauri/src/commands/update.rs index f2e299a..a9f7062 100644 --- a/call-client/src-tauri/src/commands/update.rs +++ b/call-client/src-tauri/src/commands/update.rs @@ -180,6 +180,239 @@ pub struct AptUpdateCheckResult { update_command: String, } +#[cfg(target_os = "linux")] +const ZYYUN_APT_LIST_FILE: &str = "/etc/apt/sources.list.d/zyyun.list"; +#[cfg(target_os = "linux")] +const DEFAULT_ZYYUN_REPO_BASE: &str = "http://80.12.140.29:80/apt"; +#[cfg(target_os = "linux")] +const DEFAULT_ZYYUN_SUITE: &str = "v10"; +#[cfg(target_os = "linux")] +const DEFAULT_ZYYUN_COMPONENT: &str = "main"; + +/// 读取紫云 apt 源配置;缺失时使用与 deb postinst 一致的默认值。 +#[cfg(target_os = "linux")] +fn read_zyyun_apt_repo_config() -> (String, String, String) { + let fallback = || { + ( + DEFAULT_ZYYUN_REPO_BASE.to_string(), + DEFAULT_ZYYUN_SUITE.to_string(), + DEFAULT_ZYYUN_COMPONENT.to_string(), + ) + }; + let Ok(raw) = std::fs::read_to_string(ZYYUN_APT_LIST_FILE) else { + return fallback(); + }; + for line in raw.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + if let Some((url, suite, component)) = parse_deb_source_line(line) { + return (url, suite, component); + } + } + fallback() +} + +#[cfg(target_os = "linux")] +fn parse_deb_source_line(line: &str) -> Option<(String, String, String)> { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.first().copied() != Some("deb") { + return None; + } + let mut idx = 1usize; + if idx < parts.len() && parts[idx].starts_with('[') { + idx += 1; + } + if idx + 2 >= parts.len() { + return None; + } + Some(( + parts[idx].to_string(), + parts[idx + 1].to_string(), + parts[idx + 2].to_string(), + )) +} + +#[cfg(target_os = "linux")] +fn detect_dpkg_architecture() -> String { + std::process::Command::new("dpkg") + .args(["--print-architecture"]) + .output() + .ok() + .filter(|o| o.status.success()) + .and_then(|o| String::from_utf8(o.stdout).ok()) + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| "amd64".to_string()) +} + +#[cfg(target_os = "linux")] +fn dpkg_version_gt(a: &str, b: &str) -> bool { + std::process::Command::new("dpkg") + .args(["--compare-versions", a, "gt", b]) + .status() + .map(|s| s.success()) + .unwrap_or(false) +} + +#[cfg(target_os = "linux")] +fn max_dpkg_version(versions: Vec) -> Option { + versions.into_iter().reduce(|acc, v| { + if dpkg_version_gt(&v, &acc) { + v + } else { + acc + } + }) +} + +#[cfg(target_os = "linux")] +fn parse_package_versions_from_packages(text: &str, package_name: &str) -> Vec { + let mut versions = Vec::new(); + for block in text.split("\n\n") { + let mut name: Option<&str> = None; + let mut ver: Option<&str> = None; + for line in block.lines() { + if let Some(rest) = line.strip_prefix("Package: ") { + name = Some(rest.trim()); + } else if let Some(rest) = line.strip_prefix("Version: ") { + ver = Some(rest.trim()); + } + } + if name == Some(package_name) { + if let Some(v) = ver { + if !v.is_empty() { + versions.push(v.to_string()); + } + } + } + } + versions +} + +/// 直接从仓库 HTTP 拉取 Packages.gz,避免本机未 `apt update` 时候选版本滞后。 +#[cfg(target_os = "linux")] +fn fetch_remote_highest_package_version(package_name: &str) -> Result, String> { + use flate2::read::GzDecoder; + use std::io::Read; + use std::time::Duration; + + let (base_url, suite, component) = read_zyyun_apt_repo_config(); + let arch = detect_dpkg_architecture(); + let base = base_url.trim_end_matches('/'); + let url = format!( + "{base}/dists/{suite}/{component}/binary-{arch}/Packages.gz" + ); + let _ = app_log( + "info".to_string(), + format!( + "检查更新: 在线拉取 Packages.gz package={} arch={} url={}", + package_name, arch, url + ), + ); + + let client = reqwest::blocking::Client::builder() + .timeout(Duration::from_secs(20)) + .build() + .map_err(|e| format!("创建 HTTP 客户端失败: {e}"))?; + let response = client + .get(&url) + .send() + .map_err(|e| format!("请求 Packages.gz 失败: {e}"))?; + if !response.status().is_success() { + return Err(format!( + "Packages.gz HTTP {}: {}", + response.status().as_u16(), + url + )); + } + let bytes = response + .bytes() + .map_err(|e| format!("读取 Packages.gz 响应失败: {e}"))?; + let mut decoder = GzDecoder::new(bytes.as_ref()); + let mut text = String::new(); + decoder + .read_to_string(&mut text) + .map_err(|e| format!("解压 Packages.gz 失败: {e}"))?; + + let versions = parse_package_versions_from_packages(&text, package_name); + if versions.is_empty() { + let _ = app_log( + "warn".to_string(), + format!( + "检查更新: Packages.gz 中未找到 package={}(arch={})", + package_name, arch + ), + ); + return Ok(None); + } + let highest = max_dpkg_version(versions); + let _ = app_log( + "info".to_string(), + format!( + "检查更新: 在线解析最高版本 package={} remoteCandidate={}", + package_name, + highest.as_deref().unwrap_or("(none)") + ), + ); + Ok(highest) +} + +#[cfg(target_os = "linux")] +fn merge_candidate_with_remote( + package_name: &str, + policy_candidate: &str, +) -> (String, bool) { + let policy_ok = + policy_candidate != "(none)" && policy_candidate != "(unknown)" && !policy_candidate.is_empty(); + match fetch_remote_highest_package_version(package_name) { + Ok(Some(remote)) => { + if !policy_ok { + let _ = app_log( + "info".to_string(), + format!( + "检查更新: apt 本地候选不可用,使用在线候选 {remote}(提示:可执行 apt update 同步本地索引)" + ), + ); + return (remote, true); + } + if dpkg_version_gt(&remote, policy_candidate) { + let _ = app_log( + "info".to_string(), + format!( + "检查更新: 在线候选 {remote} 高于 apt-cache 本地候选 {policy_candidate}(仓库已发布新版本但本机可能未 apt update)" + ), + ); + return (remote, true); + } + (policy_candidate.to_string(), true) + } + Ok(None) => ( + if policy_ok { + policy_candidate.to_string() + } else { + "(unknown)".to_string() + }, + policy_ok, + ), + Err(err) => { + let _ = app_log( + "warn".to_string(), + format!("检查更新: 在线 Packages.gz 拉取失败,仍使用 apt-cache 本地结果: {err}"), + ); + ( + if policy_ok { + policy_candidate.to_string() + } else { + "(unknown)".to_string() + }, + policy_ok, + ) + } + } +} + #[cfg(target_os = "linux")] fn extract_policy_value(output: &str, key: &str) -> String { let label = key.trim_end_matches(':').trim(); @@ -376,12 +609,23 @@ pub(crate) fn check_apt_update_internal( ); } + let policy_candidate = candidate_version.clone(); + let (merged_candidate, remote_source_ok) = + merge_candidate_with_remote(&package_name, &policy_candidate); + candidate_version = merged_candidate; + let baseline_version = if installed_version == "(none)" || installed_version == "(unknown)" { current_version.clone() } else { installed_version.clone() }; - let source_available = candidate_version != "(none)" && candidate_version != "(unknown)"; + let policy_source_ok = + policy_candidate != "(none)" && policy_candidate != "(unknown)" && !policy_candidate.is_empty(); + let source_available = policy_source_ok + || remote_source_ok + || (candidate_version != "(none)" + && candidate_version != "(unknown)" + && !candidate_version.trim().is_empty()); let has_update = source_available && candidate_version != baseline_version && !candidate_version.trim().is_empty();