Nebula 的下载有 .part 续传、上传有分片会话续传,这些"接着传"需要的数据一直都稳稳落在磁盘 / SQLite 里。但有个尴尬的缺口:传输面板那个列表本身没持久化。关掉 App 再打开,面板一片空白——虽然后台的续传数据还在,你却看不到那些没传完的任务,得靠记忆手动重新发起。
这次就补上这块:传输列表持久化。关掉重开,未完成的任务还在面板里,点「继续」接着传。
两层东西,只有一层落了盘
先厘清一件事:传输相关的状态其实分两层。
- 续传数据(一直持久化):下载的
.part临时文件在磁盘上,上传的 multipart 会话在 SQLite 的upload_sessions表里。有了它们,同一个传输就能从断点继续。 - 任务列表(之前只在内存):面板里看到的每一项(在传什么、到哪个账号、进度多少、什么状态),存在前端的 React state 里,关 App 即丢。
所以问题不在"能不能续传"(能),而在"看不看得见"(看不见)。补的就是这层可见的列表。
存下来,重启标为「已中断」
加一张 SQLite transfers 表,存每个未完成任务的身份与状态(id、类型、账号、远端路径、本地路径、进度、状态)。启动时把它们读回来填进面板。
关键的一步:关 App 时还在传的任务(active),重启后并没有真的在跑——进程都没了。所以恢复时把它们的状态从 active 改成 「已中断」,给一个「继续」按钮:
for (const r of rows) {
restored[r.id] = {
...r,
status: r.status === "active" ? "interrupted" : r.status,
};
}
点「继续」,复用的正是前面那层一直都在的续传数据——找到 .part / 上传会话,从断点接着传。跨账号迁移因为缺少源端信息无法这样恢复,就只显示为已中断、不给继续按钮,诚实地告诉你它中断了。
坑:别把 SQLite 写爆
朴素做法是"列表一变就存库"。但传输进度是每收到一个数据块就更新一次,一个大文件下载下来,列表一秒钟能变几十上百次。要是每次都写 SQLite,等于拿进度回调去锤数据库。
所以持久化只认状态变化,不认进度变化。用一个 effect 盯着列表,只在某项的 status 变了(新建 active、变 error / cancelled、或被清除)才落库,纯进度 tick 直接跳过:
useEffect(() => {
const prev = persistedRef.current;
for (const [id, item] of Object.entries(transfers)) {
if (prev[id] === item.status) continue; // 状态没变(含进度 tick)→ 不写库
if (item.status === "done") deleteTransfer(id); // 完成即删
else saveTransfer(item);
}
// 面板里被清除的 → 从库删
// ...
}, [transfers]);
完成的任务落库即删——面板和库里只留"未竟之事"。这样一次下载全程只有"开始"和"结束"两次写库,进度更新一次都不碰盘。
一个刻意的克制:不自动续传
恢复出来的「已中断」任务,我们不在启动时自动接着传,而是显示出来、你点了「继续」才续。
理由很简单:一开 App 就自动跑一堆传输,会在你毫无预期时占满上行 / 下行带宽。让你看到、由你决定,更可控。真要"开机自动续传",也应该是个显式的开关,而不是默认行为。
小结
传输的续传数据早就落盘了,这次只是把可见的列表也持久化,让"关掉重开还在、点一下接着传"成立。三个要点:未完成任务存 SQLite、重启标为已中断、只在状态变化时写库避免锤数据库。传到一半关了 App?再开,它还在那儿等你点继续。