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:
Generated
+12
-1
@@ -1679,7 +1679,7 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
"toml 1.1.6+spec-1.1.0",
|
"toml 1.1.6+spec-1.1.0",
|
||||||
"vswhom",
|
"vswhom",
|
||||||
"winreg",
|
"winreg 0.55.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7890,6 +7890,7 @@ dependencies = [
|
|||||||
"ureq-proto",
|
"ureq-proto",
|
||||||
"utf8-zero",
|
"utf8-zero",
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
|
"winreg 0.56.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -9182,6 +9183,16 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winreg"
|
||||||
|
version = "0.56.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wio"
|
name = "wio"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
|||||||
+3
-2
@@ -41,8 +41,9 @@ rustls-native-certs = "0.8"
|
|||||||
# 原生文件对话框(连接导入/导出);Windows 走内建 COM 后端,Linux 用默认 xdg-portal
|
# 原生文件对话框(连接导入/导出);Windows 走内建 COM 后端,Linux 用默认 xdg-portal
|
||||||
rfd = "0.17"
|
rfd = "0.17"
|
||||||
|
|
||||||
# OTA 自更新:Gitea Release 检查/下载(ureq 用 rustls,与 rumqttc 依赖同源)
|
# OTA 自更新:GitHub Release 检查/下载(ureq 用 rustls,与 rumqttc 依赖同源;
|
||||||
ureq = { version = "3", features = ["json"] }
|
# win-system-proxy 使 Windows 系统代理注册表设置自动生效,环境变量代理本就默认支持)
|
||||||
|
ureq = { version = "3", features = ["json", "win-system-proxy"] }
|
||||||
sha2 = "0.11"
|
sha2 = "0.11"
|
||||||
# Release 附件为 7z 压缩包,纯 Rust 解压(客户端免装 7-Zip)
|
# Release 附件为 7z 压缩包,纯 Rust 解压(客户端免装 7-Zip)
|
||||||
sevenz-rust = "0.6"
|
sevenz-rust = "0.6"
|
||||||
|
|||||||
@@ -69,6 +69,19 @@ pub fn pick_assets(rel: &serde_json::Value) -> Option<(String, String, u64, Opti
|
|||||||
|
|
||||||
/// 检查更新。返回 Ok(None) 表示已是最新。
|
/// 检查更新。返回 Ok(None) 表示已是最新。
|
||||||
pub fn check_latest() -> Result<Option<UpdateInfo>, String> {
|
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;
|
let base = RELEASES_API;
|
||||||
// 优先 latest 端点(GitHub 恒可用);回退到列表取第一个非草稿/预发布,作容错
|
// 优先 latest 端点(GitHub 恒可用);回退到列表取第一个非草稿/预发布,作容错
|
||||||
let rel = match http_get_json(&format!("{base}/latest")) {
|
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) 表示已是最新;错误仅记录、不打扰用户。
|
/// 返回 Ok(None) 表示已是最新;错误仅记录、不打扰用户。
|
||||||
pub fn check_and_download() -> Result<Option<StagedUpdate>, String> {
|
pub fn check_and_download() -> Result<Option<StagedUpdate>, String> {
|
||||||
let Some(info) = check_latest()? else {
|
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)
|
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 会把每次
|
/// - v1.0.x 老客户端的 OTA 源仍是自建 Gitea,CI 的 mirror-gitea job 会把每次
|
||||||
/// 发布镜像回 Gitea,老客户端升级完毕后可移除该 job。
|
/// 发布镜像回 Gitea,老客户端升级完毕后可移除该 job。
|
||||||
pub const RELEASES_API: &str = "https://api.github.com/repos/LiJoeAllen/mqttx/releases";
|
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";
|
pub const UPDATE_SOURCE: &str = "GitHub";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user