Files
soft_quay/docs/tasks/T-301.md

82 lines
6.1 KiB
Markdown

---
id: T-301
title: 可恢复下载队列
phase: 3
deps: [T-202]
status: DONE
created: 2026-07-16
issue: null
context_ref: a52acbf926041550ebc30049ff87f08faaa5ba0c
claim_branch: null
work_branch: agent/codex/T-301
write_paths:
- docs/tasks/T-301.md
- core/downloader/
- core/application/
- core/storage/
- schemas/download-task.schema.json
- testdata/download/
- testdata/README.md
- docs/api.md
- docs/04-architecture.md
- docs/current-state.md
---
## 问题 / 背景
Phase 2 已能识别软件状态并展示列表/详情,但点击下载后还没有并发调度、暂停/取消/重试、HTTP Range 续传或重启恢复。若下载状态只保存在 goroutine 内存中,盒子退出后任务丢失;若 Range 响应、临时文件和元数据不一致,可能把拼接错误的字节交给 T-302 校验/安装链路。
T-301 是 T-302/T-303 的前置安全边界。它只负责把远端 ZIP 稳定下载为隔离的 `.part`/完成文件并发布进度事件,不声称包已可信可执行。
## 方案
1. 在 `core/downloader` 定义任务模型、队列命令与状态机;默认最多 2 个 active transfer,其余保持 queued。
2. 支持 enqueue、pause、resume、cancel、retry;每个命令按稳定 `request_id` / `app_id` 定位,重复命令返回确定结果而非产生第二份任务。
3. HTTP transport 使用注入接口和标准库实现:
- 已有 `.part` 时发送 `Range: bytes=<offset>-`。
- `206` 必须校验 Content-Range 起点;不匹配则拒绝。
- 服务端返回 `200` 时安全截断并从 0 重下,不得追加到旧字节。
- 网络中断保留已落盘 `.part`;取消按策略删除任务临时文件;完成前 flush/sync/close。
4. 在 `core/storage` 持久化下载任务元数据,采用同目录临时文件 + backup 原子替换;Schema 落到 `schemas/download-task.schema.json`。
5. 启动恢复时:
- queued / paused / failed 保持可恢复语义。
- 进程退出时遗留的 downloading 降级为 queued,不假装仍在下载。
- 元数据与 `.part` 实际长度不一致时以磁盘为准或返回稳定损坏错误,不得静默跳过字节。
6. application 事件沿用 `DownloadStarted` / `DownloadProgress` / `DownloadPaused` / `DownloadCompleted` / `DownloadFailed`;负载使用具体类型,UI 只消费事件,不轮询文件。
7. 进度包含 done/total/speed;total 未知时明确表示 unknown,不伪造百分比。事件节流与 clock 注入保证测试确定性。
## 验收要点
- 并发默认值为 2;表驱动/阻塞 transport 测试证明任意时刻 active transfer 不超过 2,完成/暂停后下一个 queued 自动开始。
- pause 保留 `.part` 和元数据;resume 使用正确 Range;cancel 不再发布进度且按协议清理;failed 可 retry。
- httptest 覆盖:正常 200、正确 206、错误 Content-Range、服务器忽略 Range 返回 200、连接中断后续传、非成功状态。
- 任务元数据严格读写;未知字段、非法状态/ID/URL/路径/计数被拒绝;主文件缺失时可读取中断遗留 backup。
- 新队列实例能从磁盘恢复;遗留 downloading → queued,paused 保持 paused,已完成任务不重复下载。
- `.part` 长度、done 与下一次 Range offset 一致;不把未完成文件暴露为完成文件。
- application 具体事件负载与 request_id/app_id 匹配;进度 done 单调不回退,暂停/取消后不出现迟到 progress。
- `cd core && go vet ./... && go test -count=1 ./...`、完整双目标闸门与治理校验通过。
## 边界(不改什么)
- 不做 ZIP SHA-256 / package 签名、app.json 比对、解压、staging 切换或健康检查(T-302)。
- 不做磁盘空间预检查、程序占用、hash_mismatch/zip_corrupt 等完整失败矩阵(T-303)。
- 不实现 Gio DownloadPanel;本任务只交付 core 队列、持久化与事件,后续 UI 通过事件接入。
- 不实现多源下载、P2P、带宽限速、代理设置或动态并发 UI。
- 不引入第三方下载器、数据库或 Go 1.21+ API。
## 协作约束
- 唯一写入 Agent 独占本任务全部 `write_paths`,负责实现但不创建额外任务提交。
- 测试设计 Agent 只读输出并发、Range、崩溃恢复和事件时序测试矩阵。
- 安全/架构 Agent 只读审查路径隔离、响应拼接、持久化原子性、迟到事件和 T-302 信任边界。
- coordinator 汇总意见、要求必要修正、执行最终验证、更新本执行记录并创建唯一 T-301 Git 提交。
- T-302/T-303 在本任务 DONE 前不得领取;若后续决定并行编码,先另行落成并提交带后缀子任务及共享接口冻结任务。
## 执行记录
- 2026-07-16:正式落成多 Agent 协作方式;采用一个写入 Agent + 测试设计/安全架构两个只读 Agent,保持 T-301 → T-302 → T-303 依赖串行。
- 2026-07-16:实现 `core/downloader.Queue`、严格 HTTPS/Range transport、任务状态/路径协议、pause/resume/cancel/retry、默认并发 2、进度节流与 application 具体事件负载;新增 `core/storage.DownloadTaskStore`、`download-task.schema.json` 和测试数据说明。
- 2026-07-16:根据多轮只读审查修复安全边界:服务器忽略 Range 时先截断并 sync `.part` 再持久化新 attempt/validator;unknown total 全程受硬上限约束且续传必须证明完整 total;写入句柄身份贯穿 rename 前后;pause 不吞 body close/sync 错误,cancel 则记录非致命关闭错误后仍完成固定清理;Close 不覆盖用户 cancel;known-total 完整 part 在同进程 resume/retry 直接 finalize;observer 失败不反向改变 durable 结果并通过 `OnObserverError` 报告。
- 2026-07-16:补齐并发补位、断连续传、restart 写失败窗口、未知 total、HTTP 404/416/500 与畸形响应、进度单调/节流/速度、迟到 generation、事件字段、observer 失败对账、崩溃恢复和文件身份替换回归测试;最终只读测试/安全审查均无 blocker/high。
- 2026-07-16:验证通过:`go -C core vet ./...`;`go -C core test -count=1 ./...`;`go -C core test -count=10 ./downloader`;`./scripts/verify_phase0.ps1`。`go test -race` 已尝试,当前环境 `CGO_ENABLED=0` 且未安装 gcc,因此 race detector 无法启动;非 race 闸门全部通过。