fix: OTA 更新需用户同意、下载完整性与暂存区管理
- 安装同意标记:仅「下次启动安装」写入,main.rs 凭标记才自动安装, 跳过确认对话框不会再被强制升级;对话框新增「暂不更新」(删除暂存包) - 下载完整性:API 提供大小却未下满/超量时报错(read 返回 0 无法 区分干净结束与连接截断);失败路径统一清理 tmp 文件 - 版本比较遵循 semver 预发布语义(1.0.0-beta.2 不比 1.0.0 新); 暂存文件名按尾部锚点解析,预发布后缀不再丢失导致重复下载 - 多版本暂存并存时取最新(readdir 顺序无序);下载完成后清理旧版本残留 - install_staged:spawn 失败(杀毒锁文件)自动恢复旧版本并如实报告回滚结果 - agent 加整体 10 分钟超时;cleanup_old 大小写不敏感、跳过近期 tmp 避免误删并行实例正在写入的文件 - 设置对话框:下载进度条传百分比(原 0~1 小数只显示 1%); 立即安装失败显示具体错误;Sentry release 与更新检查同源版本号
This commit is contained in:
+21
-8
@@ -13,19 +13,32 @@ fn init_sentry() -> sentry::ClientInitGuard {
|
|||||||
sentry::init(
|
sentry::init(
|
||||||
sentry::ClientOptions::new()
|
sentry::ClientOptions::new()
|
||||||
.dsn(SENTRY_DSN)
|
.dsn(SENTRY_DSN)
|
||||||
.release(format!("mqttx@{}", env!("CARGO_PKG_VERSION"))),
|
// 与更新检查用同一版本来源(尊重 MQTTX_VERSION_OVERRIDE),保持一致
|
||||||
|
.release(format!("mqttx@{}", update::current_version())),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// OTA 收尾:清理遗留文件;若暂存区有待安装的新版本(用户上次同意安装),
|
// OTA 收尾:清理遗留文件;若暂存区有待安装的新版本且用户已同意
|
||||||
// 在进入 UI 前完成自替换并重启。
|
// (「下次启动安装」),在进入 UI 前完成自替换并重启。
|
||||||
update::cleanup_old();
|
update::cleanup_old();
|
||||||
if let Some(staged) = update::load_staged()
|
if let Some(staged) = update::load_staged() {
|
||||||
&& update::install_staged(&staged).is_ok()
|
if update::install_consent_matches(&staged.version) {
|
||||||
{
|
match update::install_staged(&staged) {
|
||||||
// 新进程已拉起,当前进程立即退出
|
Ok(()) => {
|
||||||
std::process::exit(0);
|
// 新进程已拉起,当前进程立即退出
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// 安装失败:清除同意标记,继续正常启动,
|
||||||
|
// 由应用内的更新检查重新弹窗询问。
|
||||||
|
update::clear_install_consent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 暂存包存在但用户未同意安装(例如跳过了确认对话框):
|
||||||
|
// 不做任何替换,等待应用内更新流程再次询问。
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _sentry_guard = init_sentry();
|
let _sentry_guard = init_sentry();
|
||||||
|
|||||||
@@ -233,15 +233,15 @@ impl SettingsDialog {
|
|||||||
.primary()
|
.primary()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
if update::install_staged(&staged_now).is_ok() {
|
if let Err(e) = update::install_staged(&staged_now) {
|
||||||
cx.quit();
|
|
||||||
} else {
|
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::error(
|
Notification::error(format!(
|
||||||
"安装失败,请重试",
|
"安装失败:{e}"
|
||||||
),
|
)),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
cx.quit();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -251,6 +251,10 @@ impl SettingsDialog {
|
|||||||
.outline()
|
.outline()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
// 写入同意标记:下次启动据此自动安装
|
||||||
|
if let UpdateUi::ReadyToInstall(staged) = &this.update {
|
||||||
|
update::mark_install_consent(&staged.version);
|
||||||
|
}
|
||||||
this.update = UpdateUi::Idle;
|
this.update = UpdateUi::Idle;
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::info(
|
Notification::info(
|
||||||
@@ -260,6 +264,23 @@ impl SettingsDialog {
|
|||||||
);
|
);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})),
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("upd-discard")
|
||||||
|
.label("放弃此次更新")
|
||||||
|
.outline()
|
||||||
|
.xsmall()
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
if let UpdateUi::ReadyToInstall(staged) = &this.update {
|
||||||
|
update::discard_staged(staged);
|
||||||
|
}
|
||||||
|
this.update = UpdateUi::Idle;
|
||||||
|
window.push_notification(
|
||||||
|
Notification::info("已删除暂存的更新包"),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
cx.notify();
|
||||||
|
})),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -283,7 +304,7 @@ impl SettingsDialog {
|
|||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
gpui_kit::component::progress::Progress::new("update-progress")
|
gpui_kit::component::progress::Progress::new("update-progress")
|
||||||
.value(pct),
|
.value(pct * 100.),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|||||||
+210
-47
@@ -60,6 +60,8 @@ pub fn asset_name_for(version: &str) -> String {
|
|||||||
|
|
||||||
fn agent() -> ureq::Agent {
|
fn agent() -> ureq::Agent {
|
||||||
ureq::AgentBuilder::new()
|
ureq::AgentBuilder::new()
|
||||||
|
// 整体上限:防止慢速滴流的下载无限占用阻塞线程
|
||||||
|
.timeout(std::time::Duration::from_secs(10 * 60))
|
||||||
.timeout_connect(std::time::Duration::from_secs(HTTP_TIMEOUT_SECS))
|
.timeout_connect(std::time::Duration::from_secs(HTTP_TIMEOUT_SECS))
|
||||||
.timeout_read(std::time::Duration::from_secs(HTTP_TIMEOUT_SECS))
|
.timeout_read(std::time::Duration::from_secs(HTTP_TIMEOUT_SECS))
|
||||||
.build()
|
.build()
|
||||||
@@ -74,16 +76,25 @@ fn http_get_json(url: &str) -> Result<serde_json::Value, String> {
|
|||||||
.map_err(|e| format!("解析响应失败: {e}"))
|
.map_err(|e| format!("解析响应失败: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从版本字符串提取数字段(容忍 v 前缀与 -beta 之类后缀)。
|
/// 去掉 v 前缀与预发布后缀(`-` 及之后),返回数字主体。
|
||||||
|
fn release_part(v: &str) -> &str {
|
||||||
|
let v = v.trim().trim_start_matches(['v', 'V']);
|
||||||
|
match v.find('-') {
|
||||||
|
Some(i) => &v[..i],
|
||||||
|
None => v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从版本字符串提取数字段(容忍 v 前缀;预发布后缀(`-` 之后)不参与比较)。
|
||||||
pub fn parse_version(v: &str) -> Vec<u32> {
|
pub fn parse_version(v: &str) -> Vec<u32> {
|
||||||
v.trim()
|
release_part(v)
|
||||||
.trim_start_matches(['v', 'V'])
|
.split('.')
|
||||||
.split(['.', '-'])
|
|
||||||
.filter_map(|s| s.parse::<u32>().ok())
|
.filter_map(|s| s.parse::<u32>().ok())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// a 是否比 b 新(按数字段逐位比较,缺失段视为 0)。
|
/// a 是否比 b 新(按数字段逐位比较,缺失段视为 0)。
|
||||||
|
/// 预发布版本(含 `-` 后缀)不会比同数字段的正式版新(semver 语义)。
|
||||||
pub fn is_newer(a: &str, b: &str) -> bool {
|
pub fn is_newer(a: &str, b: &str) -> bool {
|
||||||
let (a, b) = (parse_version(a), parse_version(b));
|
let (a, b) = (parse_version(a), parse_version(b));
|
||||||
let len = a.len().max(b.len());
|
let len = a.len().max(b.len());
|
||||||
@@ -102,11 +113,16 @@ pub fn pick_assets(rel: &serde_json::Value) -> Option<(String, String, u64, Opti
|
|||||||
let want_ext = if cfg!(windows) { ".exe" } else { "" };
|
let want_ext = if cfg!(windows) { ".exe" } else { "" };
|
||||||
let triple = platform_triple();
|
let triple = platform_triple();
|
||||||
for a in assets {
|
for a in assets {
|
||||||
let name = a.get("name")?.as_str()?;
|
let Some(name) = a.get("name").and_then(|n| n.as_str()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
if !name.contains(triple) || !name.ends_with(want_ext) || name.ends_with(".sha256") {
|
if !name.contains(triple) || !name.ends_with(want_ext) || name.ends_with(".sha256") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let url = a.get("browser_download_url")?.as_str()?.to_string();
|
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);
|
let size = a.get("size").and_then(|s| s.as_u64()).unwrap_or(0);
|
||||||
// 侧车命名:<asset>.sha256
|
// 侧车命名:<asset>.sha256
|
||||||
let sha = assets
|
let sha = assets
|
||||||
@@ -205,16 +221,41 @@ pub fn download_and_stage(
|
|||||||
) -> Result<StagedUpdate, String> {
|
) -> Result<StagedUpdate, String> {
|
||||||
let dir = stage_dir()?;
|
let dir = stage_dir()?;
|
||||||
let final_name = asset_name_for(&info.version);
|
let final_name = asset_name_for(&info.version);
|
||||||
let tmp = dir.join(format!("{}.{}.tmp", final_name, std::process::id()));
|
// tmp 名加入毫秒时间戳:同 pid 并发下载不共用文件
|
||||||
|
let tmp = dir.join(format!(
|
||||||
|
"{}.{}.{}.tmp",
|
||||||
|
final_name,
|
||||||
|
std::process::id(),
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_millis())
|
||||||
|
.unwrap_or(0)
|
||||||
|
));
|
||||||
let dst = dir.join(&final_name);
|
let dst = dir.join(&final_name);
|
||||||
|
|
||||||
|
if let Err(e) = download_to(info, &tmp, on_progress) {
|
||||||
|
// 失败路径统一清理临时文件
|
||||||
|
let _ = std::fs::remove_file(&tmp);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清掉其他版本的残留暂存文件,避免堆积与误装旧版
|
||||||
|
remove_other_staged(&dir, &dst);
|
||||||
|
std::fs::rename(&tmp, &dst).map_err(|e| format!("暂存失败: {e}"))?;
|
||||||
|
Ok(StagedUpdate {
|
||||||
|
version: info.version.clone(),
|
||||||
|
path: dst,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download_to(info: &UpdateInfo, tmp: &std::path::Path, on_progress: &dyn Fn(u64, u64)) -> Result<(), String> {
|
||||||
let resp = agent()
|
let resp = agent()
|
||||||
.get(&info.asset_url)
|
.get(&info.asset_url)
|
||||||
.call()
|
.call()
|
||||||
.map_err(|e| format!("下载失败: {e}"))?;
|
.map_err(|e| format!("下载失败: {e}"))?;
|
||||||
let mut reader = resp.into_reader();
|
let mut reader = resp.into_reader();
|
||||||
let total = if info.size > 0 { info.size } else { 0 };
|
let total = info.size;
|
||||||
let mut file = std::fs::File::create(&tmp).map_err(|e| format!("创建临时文件失败: {e}"))?;
|
let mut file = std::fs::File::create(tmp).map_err(|e| format!("创建临时文件失败: {e}"))?;
|
||||||
let mut buf = [0u8; 64 * 1024];
|
let mut buf = [0u8; 64 * 1024];
|
||||||
let mut downloaded: u64 = 0;
|
let mut downloaded: u64 = 0;
|
||||||
let mut hasher = sha2::Sha256::new();
|
let mut hasher = sha2::Sha256::new();
|
||||||
@@ -232,25 +273,45 @@ pub fn download_and_stage(
|
|||||||
std::io::Write::flush(&mut file).ok();
|
std::io::Write::flush(&mut file).ok();
|
||||||
drop(file);
|
drop(file);
|
||||||
|
|
||||||
|
// 传输完整性:API 提供了大小却没下满/超量,视为损坏(read 返回 0
|
||||||
|
// 无法区分干净结束与连接提前断开,必须与预期大小对账)
|
||||||
|
if total > 0 && downloaded != total {
|
||||||
|
return Err(format!(
|
||||||
|
"下载不完整:期望 {total} 字节,实际 {downloaded} 字节"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// sha256 校验
|
// sha256 校验
|
||||||
let expect = fetch_expected_sha256(info)?;
|
if let Some(expect) = fetch_expected_sha256(info)? {
|
||||||
if let Some(expect) = expect {
|
|
||||||
let actual = format!("{:x}", hasher.finalize());
|
let actual = format!("{:x}", hasher.finalize());
|
||||||
if !actual.eq_ignore_ascii_case(&expect) {
|
if !actual.eq_ignore_ascii_case(&expect) {
|
||||||
let _ = std::fs::remove_file(&tmp);
|
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"校验失败:期望 sha256 {expect},实际 {actual}"
|
"校验失败:期望 sha256 {expect},实际 {actual}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// 覆盖旧暂存(不同版本重名时以新版本为准)
|
/// 删除暂存目录中除 `keep` 外的其他版本暂存文件。
|
||||||
let _ = std::fs::remove_file(&dst);
|
fn remove_other_staged(dir: &std::path::Path, keep: &std::path::Path) {
|
||||||
std::fs::rename(&tmp, &dst).map_err(|e| format!("暂存失败: {e}"))?;
|
if let Ok(entries) = std::fs::read_dir(dir) {
|
||||||
Ok(StagedUpdate {
|
for e in entries.flatten() {
|
||||||
version: info.version.clone(),
|
let p = e.path();
|
||||||
path: dst,
|
if p == *keep {
|
||||||
})
|
continue;
|
||||||
|
}
|
||||||
|
let name = e.file_name();
|
||||||
|
let name = name.to_string_lossy();
|
||||||
|
if name.starts_with("mqttx-v")
|
||||||
|
&& name.contains(platform_triple())
|
||||||
|
&& !name.ends_with(".tmp")
|
||||||
|
&& !name.ends_with(".sha256")
|
||||||
|
{
|
||||||
|
let _ = std::fs::remove_file(&p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 读取 `.sha256` 侧车内容,返回其中的哈希值。没有侧车时返回 None。
|
/// 读取 `.sha256` 侧车内容,返回其中的哈希值。没有侧车时返回 None。
|
||||||
@@ -277,32 +338,44 @@ fn fetch_expected_sha256(info: &UpdateInfo) -> Result<Option<String>, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 从暂存文件名解析版本号(需匹配当前平台的命名约定;`.tmp` 不算)。
|
||||||
|
/// 预发布后缀原样保留。
|
||||||
|
pub fn parse_staged_name(name: &str) -> Option<String> {
|
||||||
|
let ext = if cfg!(windows) { ".exe" } else { "" };
|
||||||
|
let suffix = format!("-{}-mqttx{ext}", platform_triple());
|
||||||
|
if name.contains(".tmp") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
name.strip_prefix("mqttx-v")?
|
||||||
|
.strip_suffix(&suffix)
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// 扫描暂存区,返回待安装的更新(文件存在且比当前版本新才算)。
|
/// 扫描暂存区,返回待安装的更新(文件存在且比当前版本新才算)。
|
||||||
|
/// 多个版本并存时取最新版本(readdir 顺序不保证有序)。
|
||||||
pub fn load_staged() -> Option<StagedUpdate> {
|
pub fn load_staged() -> Option<StagedUpdate> {
|
||||||
let dir = stage_dir().ok()?;
|
let dir = stage_dir().ok()?;
|
||||||
let ext = if cfg!(windows) { ".exe" } else { "" };
|
let mut best: Option<StagedUpdate> = None;
|
||||||
let triple = platform_triple();
|
|
||||||
for e in std::fs::read_dir(&dir).ok()?.flatten() {
|
for e in std::fs::read_dir(&dir).ok()?.flatten() {
|
||||||
let name = e.file_name();
|
if !e.path().is_file() {
|
||||||
let name = name.to_string_lossy().to_string();
|
|
||||||
// 命名:mqttx-v<ver>-<triple>-mqttx[.exe]
|
|
||||||
let Some(rest) = name.strip_prefix("mqttx-v") else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if !(name.ends_with(ext) && name.contains(triple)) || name.contains(".tmp") {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let version = rest
|
let name = e.file_name();
|
||||||
.trim_end_matches(ext)
|
let name = name.to_string_lossy().to_string();
|
||||||
.split('-')
|
let Some(version) = parse_staged_name(&name) else {
|
||||||
.next()?
|
continue;
|
||||||
.to_string();
|
};
|
||||||
if !is_newer(&version, current_version()) {
|
if !is_newer(&version, current_version()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return Some(StagedUpdate { version, path: e.path() });
|
if best.as_ref().is_none_or(|b| is_newer(&version, &b.version)) {
|
||||||
|
best = Some(StagedUpdate {
|
||||||
|
version,
|
||||||
|
path: e.path(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None
|
best
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 安装暂存的更新:替换自身并拉起新进程(旧文件改名 `.old`,
|
/// 安装暂存的更新:替换自身并拉起新进程(旧文件改名 `.old`,
|
||||||
@@ -317,15 +390,71 @@ pub fn install_staged(staged: &StagedUpdate) -> Result<(), String> {
|
|||||||
std::fs::rename(&cur, &old).map_err(|e| format!("替换失败(可能无写入权限): {e}"))?;
|
std::fs::rename(&cur, &old).map_err(|e| format!("替换失败(可能无写入权限): {e}"))?;
|
||||||
if let Err(e) = std::fs::rename(&staged.path, &cur) {
|
if let Err(e) = std::fs::rename(&staged.path, &cur) {
|
||||||
// 回滚,尽量保住当前可执行文件
|
// 回滚,尽量保住当前可执行文件
|
||||||
let _ = std::fs::rename(&old, &cur);
|
let rollback = std::fs::rename(&old, &cur);
|
||||||
return Err(format!("替换失败: {e}"));
|
return Err(format!(
|
||||||
|
"替换失败: {e};{}",
|
||||||
|
rollback_result(rollback, &old)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
std::process::Command::new(&cur)
|
if let Err(e) = std::process::Command::new(&cur).spawn() {
|
||||||
.spawn()
|
// 新文件无法启动(常见:杀毒软件锁定刚落地的文件):恢复旧版本
|
||||||
.map_err(|e| format!("启动新版本失败: {e}"))?;
|
let _ = std::fs::remove_file(&cur);
|
||||||
|
let rollback = std::fs::rename(&old, &cur);
|
||||||
|
return Err(format!(
|
||||||
|
"启动新版本失败: {e};{}",
|
||||||
|
rollback_result(rollback, &old)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
clear_install_consent();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 描述回滚结果;回滚失败时提示 `.old` 备份仍保留在原处。
|
||||||
|
fn rollback_result(rollback: std::io::Result<()>, old: &std::path::Path) -> String {
|
||||||
|
match rollback {
|
||||||
|
Ok(()) => "已恢复原版本".to_string(),
|
||||||
|
Err(re) => format!(
|
||||||
|
"回滚失败({re}),原版本备份保留在 {}",
|
||||||
|
old.display()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 安装同意标记:仅当用户在更新对话框明确选择「下次启动安装」后才写入,
|
||||||
|
/// 启动时据此自动安装——用户未选择的更新不会被强制替换。
|
||||||
|
fn consent_path() -> Result<PathBuf, String> {
|
||||||
|
stage_dir().map(|d| d.join("install-consent.txt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录对指定版本的安装同意。
|
||||||
|
pub fn mark_install_consent(version: &str) {
|
||||||
|
if let Ok(p) = consent_path() {
|
||||||
|
let _ = std::fs::write(p, version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 清除安装同意标记。
|
||||||
|
pub fn clear_install_consent() {
|
||||||
|
if let Ok(p) = consent_path() {
|
||||||
|
let _ = std::fs::remove_file(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 同意标记是否与暂存的版本匹配(版本不匹配视为未同意)。
|
||||||
|
pub fn install_consent_matches(staged_version: &str) -> bool {
|
||||||
|
consent_path()
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| std::fs::read_to_string(p).ok())
|
||||||
|
.map(|v| v.trim() == staged_version)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 用户拒绝本次更新:删除暂存包与同意标记。
|
||||||
|
pub fn discard_staged(staged: &StagedUpdate) {
|
||||||
|
let _ = std::fs::remove_file(&staged.path);
|
||||||
|
clear_install_consent();
|
||||||
|
}
|
||||||
|
|
||||||
/// 启动时清理上次更新遗留的旧文件(改名失败的 `.old`、中断的 `.tmp`)。
|
/// 启动时清理上次更新遗留的旧文件(改名失败的 `.old`、中断的 `.tmp`)。
|
||||||
pub fn cleanup_old() {
|
pub fn cleanup_old() {
|
||||||
if let Ok(cur) = std::env::current_exe()
|
if let Ok(cur) = std::env::current_exe()
|
||||||
@@ -333,24 +462,35 @@ pub fn cleanup_old() {
|
|||||||
&& let Ok(entries) = std::fs::read_dir(dir)
|
&& let Ok(entries) = std::fs::read_dir(dir)
|
||||||
{
|
{
|
||||||
for e in entries.flatten() {
|
for e in entries.flatten() {
|
||||||
|
// Windows 文件系统不区分大小写,统一按小写匹配
|
||||||
let name = e.file_name();
|
let name = e.file_name();
|
||||||
let name = name.to_string_lossy();
|
let lower = name.to_string_lossy().to_lowercase();
|
||||||
if (name.ends_with(".old") && name.starts_with("mqttx"))
|
if (lower.ends_with(".old") && lower.starts_with("mqttx"))
|
||||||
|| name.ends_with(".update.tmp")
|
|| lower.ends_with(".update.tmp")
|
||||||
{
|
{
|
||||||
let _ = std::fs::remove_file(e.path());
|
let _ = std::fs::remove_file(e.path());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 暂存目录里中断的 .tmp
|
// 暂存目录里中断的 .tmp。跳过最近修改的文件,避免误删
|
||||||
|
// 并行实例正在写入的临时文件。
|
||||||
if let Ok(dir) = stage_dir()
|
if let Ok(dir) = stage_dir()
|
||||||
&& let Ok(entries) = std::fs::read_dir(&dir)
|
&& let Ok(entries) = std::fs::read_dir(&dir)
|
||||||
{
|
{
|
||||||
for e in entries.flatten() {
|
for e in entries.flatten() {
|
||||||
let name = e.file_name();
|
let name = e.file_name();
|
||||||
if name.to_string_lossy().ends_with(".tmp") {
|
if !name.to_string_lossy().ends_with(".tmp") {
|
||||||
let _ = std::fs::remove_file(e.path());
|
continue;
|
||||||
}
|
}
|
||||||
|
let recent = std::fs::metadata(e.path())
|
||||||
|
.and_then(|m| m.modified())
|
||||||
|
.ok()
|
||||||
|
.and_then(|t| t.elapsed().ok())
|
||||||
|
.is_some_and(|age| age < std::time::Duration::from_secs(10 * 60));
|
||||||
|
if recent {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_file(e.path());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,11 +506,34 @@ mod tests {
|
|||||||
assert!(is_newer("1.0.1", "1.0.0"));
|
assert!(is_newer("1.0.1", "1.0.0"));
|
||||||
assert!(is_newer("v1.1.0", "1.0.9"));
|
assert!(is_newer("v1.1.0", "1.0.9"));
|
||||||
assert!(is_newer("2.0", "1.9.9"));
|
assert!(is_newer("2.0", "1.9.9"));
|
||||||
// 预发布后缀不做特殊处理(只比较数字段,1.0.0-beta == 1.0.0)
|
// 预发布版本不比同数字段的正式版新(semver 语义)
|
||||||
assert!(!is_newer("1.0.0-beta", "1.0.0"));
|
assert!(!is_newer("1.0.0-beta", "1.0.0"));
|
||||||
|
assert!(!is_newer("1.0.0-beta.2", "1.0.0"));
|
||||||
assert!(!is_newer("1.0.0", "1.0.0"));
|
assert!(!is_newer("1.0.0", "1.0.0"));
|
||||||
assert!(!is_newer("0.9", "1.0.0"));
|
assert!(!is_newer("0.9", "1.0.0"));
|
||||||
|
// 更高版本号的预发布仍是更新(1.0.1-beta > 1.0.0)
|
||||||
|
assert!(is_newer("1.0.1-beta", "1.0.0"));
|
||||||
assert_eq!(parse_version("v1.2.3-beta"), vec![1, 2, 3]);
|
assert_eq!(parse_version("v1.2.3-beta"), vec![1, 2, 3]);
|
||||||
|
assert_eq!(parse_version("v1.2.3"), vec![1, 2, 3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[test]
|
||||||
|
fn staged_name_parsing_keeps_prerelease_suffix() {
|
||||||
|
let triple = platform_triple();
|
||||||
|
assert_eq!(
|
||||||
|
parse_staged_name(&format!("mqttx-v1.0.3-{triple}-mqttx.exe")),
|
||||||
|
Some("1.0.3".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_staged_name(&format!("mqttx-v1.0.3-beta.1-{triple}-mqttx.exe")),
|
||||||
|
Some("1.0.3-beta.1".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_staged_name(&format!("mqttx-v1.0.3-{triple}-mqttx.exe.1234.56.tmp")),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
assert_eq!(parse_staged_name("其他文件.exe"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user