Nebula 的传输面板一路加了不少能力:并发、全局限速、取消即暂停、断点续传、重启后恢复、失败重试。但重试这块一直有个洞——跨云迁移不能重试。取消或失败的迁移任务,面板上根本不显示那个「重试」按钮。这次把它补上。
为什么迁移是个例外
其他传输重试起来都好办,因为重跑需要的信息任务本身就带着:
- 下载:账号 + 远端路径 + 本地目录 —— 都在任务里。
- 上传:本地文件 + 目标路径 —— 都在任务里,而且还能断点续传。
- 文件夹下载:账号 + 远端目录 + 本地目录 —— 都在任务里。
迁移不一样。它是从一个账号搬到另一个账号,所以重跑既要知道目标(账号 + 路径),也要知道源端(账号 + 路径)。而任务里只记了目标——account / remote 存的是目的地,源端只在发起时的那次调用里出现过,发完就没了。没有源端,重试无从谈起,按钮自然也不显示。
修法:把迁移逻辑抽成可复用的 runner
原来的迁移逻辑内联在 doMigrate 里,分单对象和文件夹两支。要让「重试」也能发起同样的迁移,就得让这段逻辑可被复用——不能只从那个选目标的对话框里调用。于是把两支各抽成一个函数:
// 之前:逻辑埋在 doMigrate 内,只有对话框能触发
const doMigrate = async (dstAccount, dstPath) => {
const entry = migrateTarget;
if (entry.kind === "directory") { /* 一大段文件夹迁移 */ }
else { /* 一大段单对象迁移 */ }
};
// 之后:抽成 runner,doMigrate 和 retry 都能调
const runMigrateSingle = async (srcAccount, srcPath, size, dstAccount, dstPath, name) => { … };
const runMigrateFolder = async (srcAccount, srcRoot, dstAccount, dstDir, name) => { … };
关键是 runner 的第一个参数就是源端,并且把它记进任务对象:
setTransfers((prev) => ({ ...prev, [id]: {
…, account: dstAccount, remote: dstPath,
srcAccount, srcPath, // ← 新增:把源端记下来
status: "active",
} }));
于是 doMigrate 瘦成一个分发,重试则直接拿任务里记下的源端再调一次:
} else if (t.kind === "迁移" && t.srcAccount && t.srcPath) {
void runMigrateSingle(t.srcAccount, t.srcPath, t.total, t.account, t.remote, t.name);
}
面板那边,原先用一串 kind !== "迁移" && kind !== … 把迁移排除在重试之外;现在换成一个正向判断——带了源端的迁移才可重试:
const retryable =
i.kind === "上传" || i.kind === "下载" || i.kind === "下载文件夹" ||
((i.kind === "迁移" || i.kind === "迁移文件夹") && !!i.srcAccount);
一个诚实的边界:源端只存在内存里
传输列表是持久化的——存进本地 SQLite,重启后未完成的任务还在,能接着重试。那源端要不要也一起存?
这次故意不存。源端字段(srcAccount / srcPath)是任务对象上的可选字段,发到后端持久化时会被直接忽略(后端的记录结构里没有这两格,serde 自然丢弃),所以不需要改数据库表结构。代价是:重启之后,恢复出来的迁移任务没有源端,面板不给它重试按钮。
这是个清醒的取舍。迁移重试是「重跑」而非「断点续传」——它会从头再拷一遍,本就不是那种攒了半程等着接续的场景;真正常见的是同一会话里取消了想立刻重来,而这个内存方案完整覆盖。跨重启的迁移续跑要动表结构,留给以后按需再做,而不是现在为一个次要场景先背上一次数据库迁移。判断的准绳始终是:别为边角场景引入结构性负担。
小结
迁移是传输面板里最后一个不能重试的类型,原因是重跑要源端而它没被记下。把迁移抽成带源端参数的 runner、让重试复用同一段逻辑,洞就补上了。源端只留在内存,换来零表结构改动,代价是重启后不可重试——对「取消了想立刻重来」这个真实场景,够用且诚实。