自动更新功能听起来很美好,但现实网络环境复杂多变——下载中断、安装包损坏、权限不足、磁盘空间不够……任何一个环节出问题,都可能让用户的应用卡在“半更新”状态,甚至无法启动。因此,一个可靠的更新系统必须内置异常处理机制,核心就是两件事:更新失败能回滚,下载中断能续传。
更新失败回滚
回滚的基本思路是:在替换文件之前,先完整保留上一版本的可运行副本。一旦新版本启动失败或校验不通过,自动切回旧版本,保证应用始终可打开。
实现策略(以 Tauri 为例)
- 双目录结构
将应用安装到两个交替使用的目录,例如 app/ 和 app_old/。更新时,新版本下载并解压到非当前使用的目录,确认无误后,通过一个极小的“引导器”或快捷方式切换指向。如果新版本启动后 5 秒内没有心跳(比如进程无响应),引导器自动恢复旧版本入口。
- 版本标记与校验
在更新包中附带一个 signature 或哈希文件,下载完成后必须校验签名和完整性。校验失败则直接丢弃包,不进入安装流程。安装完成后,写入一个 version 标记文件,主进程启动时比对版本号,如果发现不匹配或标记损坏,自动触发回滚。
- 进程守护回滚脚本
在 Windows 上,可借助一个外部 .exe 守护程序:主应用下载更新包后,将安装任务交给守护程序,自己退出。守护程序负责替换文件、重启新版本。如果新版本启动后 10 秒未响应,守护程序再次将旧版本覆盖回来并启动。整个过程对用户透明。
- 用户数据隔离
回滚只替换程序文件,用户配置、缓存、数据库等存储在 appdata 目录中的文件不应被回滚影响。确保升级或回滚后用户的个人数据完好无损。
断点续传
更新包的完整下载是更新的前提,但几百 MB 的包在弱网下下载到一半失败,重头再来既浪费流量又耗时间。断点续传可以大幅提升下载成功率。
实现方式
- HTTP Range 请求
服务端需支持 Accept-Ranges: bytes。下载前先发送 HEAD 请求获取文件总大小,本地已有部分下载的临时文件时,再发送 GET 请求并设置 Range: bytes=已下载字节-,服务端返回 206 状态码并继续传输剩余部分。所有主流 CDN 和对象存储(如阿里云 OSS、AWS S3)都默认支持 Range 请求。
- 分段下载与合并
对于超大包,可以拆成多个 10~20 MB 的分段并行下载,每个分段独立续传。Tauri 的 Rust 后端可使用 reqwest 库轻松实现。下载时记录每个分段的进度到本地日志,中断后重新读取日志,仅下载未完成的分段,最后合并。
- 校验点保存
每下载完一个数据块(比如 1 MB),将当前进度写入一个 .download_progress 文件。下次启动更新时先读取进度文件,若存在且临时文件大小匹配,则从断点继续;若大小不符(说明文件被篡改),则丢弃从头下载。
- 超时与重试策略
设置合理超时(如 30 秒),超时后自动重试 3 次。连续失败则提示用户检查网络,避免无限重试消耗资源。重试时直接从上次保存的进度继续,无需用户干预。
代码思路(Rust 侧)
use reqwest::Client;
use std::fs::{self, OpenOptions};
use std::io::Write;
async fn download_with_resume(url: &str, file_path: &str) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let mut downloaded: u64 = 0;
// 检查已下载的临时文件
if let Ok(metadata) = fs::metadata(file_path) {
downloaded = metadata.len();
}
let mut request = client.get(url);
if downloaded > 0 {
request = request.header("Range", format!("bytes={}-", downloaded));
}
let response = request.send().await?;
let total_size = response.content_length().unwrap_or(0) + downloaded;
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(file_path)?;
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
file.write_all(&chunk)?;
downloaded += chunk.len() as u64;
// 每 1 MB 存一次进度
if downloaded % (1024 * 1024) == 0 {
// 更新进度信息(可发送事件给前端显示)
}
}
Ok(())
}
综合建议
在实际产品中,更新流程应设计成一个有限状态机:
检查更新 → 询问用户 → 下载(可续传) → 校验 → 准备回滚副本 → 安装 → 重启。
每个状态都应有失败处理路径,回滚是“安装”状态的失败出口,续传是“下载”状态的恢复手段。
这样,即使更新过程中断网、断电或进程被杀,下次启动时都能从断点恢复或退回到稳定版本,用户完全无感知。对于需要长期维护的桌面应用,这种容错能力比更新速度更重要。