Files
mqttx/gpui-app/src/update/check.rs
T
JoeAllen 68273e8e63 feat(update): OTA 更新源切换到 GitHub Releases
- RELEASES_API 指向 api.github.com/repos/LiJoeAllen/mqttx/releases;
  latest 端点优先,回退列表分页改 per_page=5(GitHub 参数)
- agent 显式设置 User-Agent: mqttx-ota(GitHub API 要求携带 UA)
- api.github.com 未认证配额 60 次/小时/IP:共享出口 IP 打满时检查静默
  降级(错误仅记录),附件下载走 github.com CDN 不受配额限制
- 设置对话框「关于」的更新源展示改为 GitHub(UPDATE_SOURCE 常量)
- v1.0.x 老客户端 OTA 源仍是 Gitea,靠 CI mirror-gitea job 喂养
  (配置 GITEA_URL/GITEA_TOKEN/GITEA_OWNER Secrets 后生效),
  老客户端升级完毕后可移除镜像 job 与 publish-gitea 脚本
2026-09-30 12:41:11 +08:00

126 lines
4.7 KiB
Rust

//! 更新检查:访问 Gitea Release API,比较版本并挑选本平台资产。
use super::*;
pub(super) fn agent() -> ureq::Agent {
ureq::Agent::new_with_config(
ureq::Agent::config_builder()
// GitHub API 要求请求携带 User-Agent(缺省 ureq/x.y 也能过,
// 但标识应用更符合其规范,也便于服务端区分流量)
.user_agent("mqttx-ota")
// 整体上限:防止慢速滴流的下载无限占用阻塞线程
.timeout_global(Some(std::time::Duration::from_secs(10 * 60)))
.timeout_connect(Some(std::time::Duration::from_secs(HTTP_TIMEOUT_SECS)))
.timeout_recv_response(Some(std::time::Duration::from_secs(HTTP_TIMEOUT_SECS)))
.build(),
)
}
pub(super) fn http_get_json(url: &str) -> Result<serde_json::Value, String> {
let mut body = agent()
.get(url)
.call()
.map_err(|e| format!("请求失败: {e}"))?
.into_body();
body.read_json::<serde_json::Value>()
.map_err(|e| format!("解析响应失败: {e}"))
}
pub fn pick_assets(rel: &serde_json::Value) -> Option<(String, String, u64, Option<String>)> {
let assets = rel.get("assets")?.as_array()?;
let triple = platform_triple();
// 产物命名约定:`mqttx-v<ver>-<triple>-mqttx[.exe]`(压缩包再加 `.7z`)。
// 必须按固定后缀精确匹配:非 Windows 下扩展名为空串时 `ends_with("")` 恒真,
// 任何含三元组的附件(.tar.gz/.deb/.txt)都会被当成可执行文件下载并替换自身。
let bin_name = if cfg!(windows) { "mqttx.exe" } else { "mqttx" };
// 第一轮 7z 压缩包,第二轮裸二进制
for suffix in [".7z", ""] {
for a in assets {
let Some(name) = a.get("name").and_then(|n| n.as_str()) else {
continue;
};
if !name.contains(triple.as_str())
|| !name.ends_with(&format!("{bin_name}{suffix}"))
|| name.ends_with(".sha256")
{
continue;
}
let Some(url) = a.get("browser_download_url").and_then(|u| u.as_str()) else {
continue;
};
let url = url.to_string();
let size = a.get("size").and_then(|s| s.as_u64()).unwrap_or(0);
// 侧车命名:<asset>.sha256
let sha = assets
.iter()
.find(|b| {
b.get("name").and_then(|n| n.as_str()) == Some(&format!("{name}.sha256"))
})
.and_then(|b| b.get("browser_download_url"))
.and_then(|u| u.as_str())
.map(|s| s.to_string());
return Some((name.to_string(), url, size, sha));
}
}
None
}
/// 检查更新。返回 Ok(None) 表示已是最新。
pub fn check_latest() -> Result<Option<UpdateInfo>, String> {
let base = RELEASES_API;
// 优先 latest 端点(GitHub 恒可用);回退到列表取第一个非草稿/预发布,作容错
let rel = match http_get_json(&format!("{base}/latest")) {
Ok(v) => v,
Err(_) => {
let list = http_get_json(&format!("{base}?per_page=5"))?;
let first = list
.as_array()
.and_then(|l| l.iter().find(|r| {
r.get("draft").and_then(|d| d.as_bool()) == Some(false)
&& r.get("prerelease").and_then(|d| d.as_bool()) == Some(false)
}))
.ok_or_else(|| "没有可用 Release".to_string())?;
first.clone()
}
};
let tag = rel
.get("tag_name")
.and_then(|t| t.as_str())
.ok_or_else(|| "Release 缺少 tag_name".to_string())?;
if !is_newer(tag, current_version()) {
return Ok(None);
}
let Some((name, url, size, sha)) = pick_assets(&rel) else {
return Err(format!("Release {tag} 没有当前平台({})的产物", platform_triple()));
};
Ok(Some(UpdateInfo {
version: tag.trim_start_matches('v').to_string(),
notes: rel
.get("body")
.and_then(|b| b.as_str())
.unwrap_or_default()
.to_string(),
asset_name: name,
asset_url: url,
sha256_url: sha,
size,
}))
}
/// 返回 Ok(None) 表示已是最新;错误仅记录、不打扰用户。
pub fn check_and_download() -> Result<Option<StagedUpdate>, String> {
let Some(info) = check_latest()? else {
return Ok(None);
};
// 已暂存同版本则不重复下载
if let Some(staged) = load_staged()
&& staged.version == info.version
{
return Ok(Some(staged));
}
download_and_stage(&info, &|_, _| {}).map(Some)
}