Zhupi222
首页关于项目技术文章联系方式

桌面设备的素材同步:首次全量、增量阈值与失败回退

2026-07-18T00:00:00.000Z约9分钟
回顾桌面应用素材同步从完整 ZIP 下载发展到增量更新和断网回退的过程,以及一次黑屏风险如何改变文件替换顺序。
Electron资源同步离线运行项目复盘

远程 Recipe 刚接入时,JSON 已经能下发,里面引用的图片和视频却仍在服务器上。配置先到、文件后到,页面就会拿到一批本地不存在的 URL。素材同步由此进入项目。

第一版只解决启动时把完整资源包放到设备上。后来设备需要在网络不稳定时继续运行,代码才逐步增加增量同步、失败回退和初始化进度。现在的实现保留了这段演进过程,也留下了一些尚未补齐的地方。

起因:远程配置开始引用本地文件

组件拿到的 token 只保存文件名。asset resolver 把它转换成 static://recipe-assets/{projectId}/{filename},Electron 再从用户目录读取。这样组件无需知道 Windows 路径,视频也能通过 Range 请求流式读取。

问题随之变成:在远程 tokens 启用前,projectId 对应的目录必须已经准备好。最初的同步提交跨越主进程、渲染进程和加载界面,一次增加了五百多行代码。功能重点很明确——下载 ZIP、显示进度、解压到本地,然后通知 Provider 可以开始使用远程素材。

第一版先把整包素材下载到设备

设备没有同步记录时,会请求完整 ZIP 的下载地址、MD5 和更新时间。缺少其中任何一项,初始化直接失败。全新设备没有旧文件可以参照,所以这里没有尝试改用增量接口拼出一套资源。

ZIP 先下载到系统临时目录。文件流结束后计算 MD5,校验通过再解压。进度会依次报告 download、verify 和 extract。成功后,更新时间写进 recipe-sync-state.json,并按 projectId 保存。

这套流程能让新设备开始运行,代价是每次更新完整包都比较重。随着素材数量增加,后续提交加入了增量接口。

后来改成只下载有变化的文件

本地存在 updatedAt 时,客户端只请求这个时间之后变化的文件。清单中的每一项包含 URL、MD5 和新的更新时间。下载器从 URL 取出文件名,先写到目标目录下的 .tmp 文件,校验后 rename。

const destPath = path.join(targetDir, filename); const tmpPath = destPath + '.tmp'; await downloadFile(asset.url, tmpPath, signal); const actualMd5 = await computeFileMd5(tmpPath); if (actualMd5 !== asset.md5) { fs.unlinkSync(tmpPath); throw new Error('MD5 mismatch'); } fs.renameSync(tmpPath, destPath);

.tmp 避免组件读到半个文件。MD5 负责发现传输损坏,校验失败时临时文件会被删除。这里没有使用版本目录,增量文件校验成功后就进入当前目录。

代码还保留了一个很具体的判断:变更文件超过 20 个时,放弃逐文件更新,重新下载完整 ZIP。这个阈值来自当前项目对请求次数和压缩包成本的取舍。同步成功后才保存新的 updatedAt,失败的批次下次仍会重新请求。

断网后继续启动,是后来补上的能力

半离线场景加入后,素材同步被放进统一的 app-init 流程。编排器最终会返回 synced、fallback-local、aborted 或 failed。

当网络或接口出错时,只要 projectId 对应的本地目录已经存在,结果就是 fallback-local。初始化把这次同步标记为跳过,应用继续启动。全新设备没有目录时,同样的错误会返回 failed。

这个判断解决了“昨天能用,今天断网”的设备。它没有检查本地目录中的每个文件,也没有保留一份独立的上一版本。fallback-local 当前表达的是本地目录可供继续尝试使用。

渲染层还有一层保护。initDone 之前,Provider 使用打包在 public 下的默认素材。即使项目目录同步失败,基础页面仍然可以用默认 Recipe 挂载。

一次黑屏风险改变了文件替换顺序

项目里另一条静态资源同步链路曾经先清空正式目录,再把 ZIP 解压进去。解压中途失败,设备只剩半套文件,页面可能直接黑屏。后续修复先在正式目录旁边创建临时解压目录,全部完成后再替换。临时目录放在同一文件系统,rename 才能保持稳定。

Recipe 的完整 ZIP 同步现在也先下载、校验和解压,再删除旧 targetDir 并 rename 新目录。它已经避开了边解压边覆盖的问题,不过删除和 rename 之间仍有短暂空档,也没有留下上一版本。这一点在源码注释中的“atomic swap”之外仍需单独留意。

这套同步最后解决了哪些问题

当前链路满足了设备运行中最常见的几种情况:首次安装使用完整包,日常只取增量,变化较多时回到全量,文件损坏不会覆盖目标,已经同步过的设备断网后可以继续启动。同步状态和日志也能看出本次走了哪条路径。

仍未解决的问题同样明确。增量清单没有处理远端删除,本地 bundle 启动时不会做完整校验,MD5 也只能检查完整性。若继续提高可靠性,下一步应当增加版本目录、切换后的延迟清理和启动校验。

这段代码从一个 ZIP 下载器走到现在,变化都由设备现场会遇到的问题推动。它还算不上完整的资源发布系统,但每条回退路径都能在提交记录中找到对应原因。

上一篇LeetCode 567 解题优化记录:从暴力到滑动窗口下一篇为什么要把多套部署差异做成 Recipe

相关

为什么要把多套部署差异做成 Recipe
2026-07-21T00:00:00.000Z约8分钟
Electron前端架构配置系统