下载 / 上传一个大文件,总得能中途停下来。这次给传输面板加了取消:每个进行中的任务旁边一个 ✕,点一下就停。实现里有两个值得说的点:怎么"优雅地"停,以及为什么停下来其实等于暂停

协作式取消,而不是"一刀砍断"

停一个正在跑的异步任务,最粗暴的做法是直接 abort 掉它对应的 task。但那样会在任意一个 await 点被打断——可能正写到文件一半、或分片传到一半,留下不一致的中间状态。

我们用协作式取消:每个传输登记一个共享的取消标志(Arc<AtomicBool>),传输循环在每个数据块 / 分片之间主动检查它:

while let Some(chunk) = stream.next().await {
    if cancel.load(Ordering::Relaxed) {   // 在块的边界上检查
        return Err("已取消".into());
    }
    file.write_all(&chunk).await?;
    // ...
}

这样停下来的位置永远是"干净的块边界"——当前块要么完整写完,要么还没开始,不会有半块的烂摊子。前端点 ✕ 就把对应任务的标志置位,后端下一个块检查到就停。一个小小的登记表(id → 标志)把两端连起来,任务结束时用一个 RAII guard 自动注销,任何返回路径都不漏。

取消完,状态别丢——于是它变成了暂停

关键的设计选择在这里:取消时不清理已经传好的部分

  • 下载:已经下的字节留在 .part 临时文件里,不删。
  • 上传:分片上传的会话(upload_id + 已传分片)留在库里,不 abort。

这正是我们做断点续传时铺好的地基。于是取消一个传输后,它并没有真的"消失"——已完成的进度都还在。传输面板里,被取消的任务会显示一个"继续"按钮,点一下重新发起,它查到 .part / 上传会话,从断点接着传

所以在 Nebula 里,“取消"实际上是"暂停”:停下来不丢进度,想继续随时继续。这不是特意多做的功能,而是取消 + 续传两块拼在一起自然长出来的——先把状态持久化做对了,取消就顺理成章地免费获得了"可恢复"。

一致的收尾

前端怎么区分"用户取消"和"真的失败"?后端统一用 已取消 这个信号:取消路径返回它,前端识别后把任务标成"已取消"(灰掉、给个继续按钮),而不是弹一个红色的错误横幅。失败才是失败,取消是取消,两者在 UI 上清清楚楚。

一句话:协作式取消保证停在干净的边界,不丢已传进度让取消等于暂停,统一的信号让取消和失败各归各位。