feat(update): OTA 兜底发现与系统代理支持
- check_latest 加免 API 配额的兜底路径:api.github.com 被限流(60 次/ 小时/IP,共享出口 IP 实测已打满)时,跟踪 github.com/releases/latest 的 302 从最终 URL 提取 tag,下载与 .sha256 按命名约定构造;notes 置空、 大小置 0(进度未知、对账跳过),sha256 校验仍强制执行 - ureq 启用 win-system-proxy:Windows 系统代理注册表设置自动生效; HTTP(S)_PROXY 环境变量代理 ureq 默认就读取,无需配置 - 实测:github.com 直连与代理均可达(302 正常),api.github.com 直连 与代理出口 IP 的未认证配额均耗尽 —— 兜底路径为必需品而非锦上添花
This commit is contained in:
+3
-2
@@ -41,8 +41,9 @@ rustls-native-certs = "0.8"
|
||||
# 原生文件对话框(连接导入/导出);Windows 走内建 COM 后端,Linux 用默认 xdg-portal
|
||||
rfd = "0.17"
|
||||
|
||||
# OTA 自更新:Gitea Release 检查/下载(ureq 用 rustls,与 rumqttc 依赖同源)
|
||||
ureq = { version = "3", features = ["json"] }
|
||||
# OTA 自更新:GitHub Release 检查/下载(ureq 用 rustls,与 rumqttc 依赖同源;
|
||||
# win-system-proxy 使 Windows 系统代理注册表设置自动生效,环境变量代理本就默认支持)
|
||||
ureq = { version = "3", features = ["json", "win-system-proxy"] }
|
||||
sha2 = "0.11"
|
||||
# Release 附件为 7z 压缩包,纯 Rust 解压(客户端免装 7-Zip)
|
||||
sevenz-rust = "0.6"
|
||||
|
||||
@@ -69,6 +69,19 @@ pub fn pick_assets(rel: &serde_json::Value) -> Option<(String, String, u64, Opti
|
||||
|
||||
/// 检查更新。返回 Ok(None) 表示已是最新。
|
||||
pub fn check_latest() -> Result<Option<UpdateInfo>, String> {
|
||||
// API 主路径信息最全(notes/资产清单/大小),但 api.github.com 未认证配额
|
||||
// 仅 60 次/小时/IP,共享出口 IP(VPN/办公 NAT)经常打满 —— 打满或不可达时
|
||||
// 退到 releases 页面重定向发现(无配额),完整性校验不受影响。
|
||||
match check_latest_via_api() {
|
||||
Ok(v) => Ok(v),
|
||||
Err(api_err) => match check_latest_via_redirect() {
|
||||
Ok(v) => Ok(v),
|
||||
Err(_) => Err(api_err),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn check_latest_via_api() -> Result<Option<UpdateInfo>, String> {
|
||||
let base = RELEASES_API;
|
||||
// 优先 latest 端点(GitHub 恒可用);回退到列表取第一个非草稿/预发布,作容错
|
||||
let rel = match http_get_json(&format!("{base}/latest")) {
|
||||
@@ -110,6 +123,48 @@ pub fn check_latest() -> Result<Option<UpdateInfo>, String> {
|
||||
}))
|
||||
}
|
||||
|
||||
/// 兜底:跟踪 `releases/latest` 的 302,从最终 URL 提取最新 tag。
|
||||
///
|
||||
/// github.com 页面不受 API 配额限制。代价是拿不到 Release notes 与资产清单:
|
||||
/// notes 置空、大小置 0(进度条显示未知大小,大小对账自动跳过),
|
||||
/// 下载 URL 与 `.sha256` 侧车按命名约定构造,sha256 校验仍然强制执行。
|
||||
fn check_latest_via_redirect() -> Result<Option<UpdateInfo>, String> {
|
||||
use ureq::ResponseExt as _;
|
||||
let resp = agent()
|
||||
.get(&format!("{RELEASES_PAGE}/latest"))
|
||||
.call()
|
||||
.map_err(|e| format!("页面请求失败: {e}"))?;
|
||||
let uri = resp.get_uri().to_string();
|
||||
let Some(tag) = tag_from_releases_url(&uri) else {
|
||||
return Err(format!("页面 URL 异常: {uri}"));
|
||||
};
|
||||
if !is_newer(tag, current_version()) {
|
||||
return Ok(None);
|
||||
}
|
||||
let version = tag.trim_start_matches('v');
|
||||
let triple = platform_triple();
|
||||
let bin = if cfg!(windows) { "mqttx.exe" } else { "mqttx" };
|
||||
// 与 publish 脚本/工作流的产物命名约定一致(pick_assets 同款匹配规则)
|
||||
let asset_base = format!("mqttx-v{version}-{triple}-{bin}");
|
||||
let asset = format!("{asset_base}.7z");
|
||||
Ok(Some(UpdateInfo {
|
||||
version: version.to_string(),
|
||||
notes: String::new(),
|
||||
asset_name: asset.clone(),
|
||||
asset_url: format!("{RELEASES_PAGE}/download/{tag}/{asset}"),
|
||||
sha256_url: Some(format!("{RELEASES_PAGE}/download/{tag}/{asset}.sha256")),
|
||||
size: 0,
|
||||
}))
|
||||
}
|
||||
|
||||
/// 从 Releases 页面 URL 提取 tag(如 `.../releases/tag/v1.0.1` → `v1.0.1`)。
|
||||
fn tag_from_releases_url(uri: &str) -> Option<&str> {
|
||||
let tag = uri.split("/releases/tag/").nth(1)?;
|
||||
let tag = tag.trim_end_matches('/');
|
||||
// 防御:tag 不含路径段,且必须以 v 开头(约定)
|
||||
(tag.starts_with('v') && !tag.contains('/')).then_some(tag)
|
||||
}
|
||||
|
||||
/// 返回 Ok(None) 表示已是最新;错误仅记录、不打扰用户。
|
||||
pub fn check_and_download() -> Result<Option<StagedUpdate>, String> {
|
||||
let Some(info) = check_latest()? else {
|
||||
@@ -123,3 +178,34 @@ pub fn check_and_download() -> Result<Option<StagedUpdate>, String> {
|
||||
}
|
||||
download_and_stage(&info, &|_, _| {}).map(Some)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tag_extraction_from_releases_url() {
|
||||
// /releases/latest 302 后的最终 URL 形态
|
||||
assert_eq!(
|
||||
tag_from_releases_url("https://github.com/LiJoeAllen/mqttx/releases/tag/v1.0.1"),
|
||||
Some("v1.0.1")
|
||||
);
|
||||
assert_eq!(
|
||||
tag_from_releases_url("https://github.com/LiJoeAllen/mqttx/releases/tag/v1.2.3-rc.1/"),
|
||||
Some("v1.2.3-rc.1")
|
||||
);
|
||||
// 非 tag 页面 / 异常形态一律拒绝
|
||||
assert_eq!(
|
||||
tag_from_releases_url("https://github.com/LiJoeAllen/mqttx/releases"),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
tag_from_releases_url("https://github.com/LiJoeAllen/mqttx/releases/tag/1.0.0"),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
tag_from_releases_url("https://github.com/LiJoeAllen/mqttx/releases/tag/v1.0.0/extra"),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ pub use install::{
|
||||
/// - v1.0.x 老客户端的 OTA 源仍是自建 Gitea,CI 的 mirror-gitea job 会把每次
|
||||
/// 发布镜像回 Gitea,老客户端升级完毕后可移除该 job。
|
||||
pub const RELEASES_API: &str = "https://api.github.com/repos/LiJoeAllen/mqttx/releases";
|
||||
/// Releases 页面(github.com,非 API、无配额限制):api.github.com 被限流时
|
||||
/// 兜底发现最新 tag —— 跟踪 `{RELEASES_PAGE}/latest` 的 302,从最终 URL 取 tag,
|
||||
/// 下载与校验按命名约定构造 URL,完整性(sha256)不受影响。
|
||||
pub const RELEASES_PAGE: &str = "https://github.com/LiJoeAllen/mqttx/releases";
|
||||
/// 更新源展示名(设置对话框「关于」)。
|
||||
pub const UPDATE_SOURCE: &str = "GitHub";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user