feat(offline-download): support torrent uploads for qBittorrent PT - #2676
feat(offline-download): support torrent uploads for qBittorrent PT#2676ctaoist wants to merge 8 commits into
Conversation
- Accept torrent_data in offline download requests and validate uploaded torrent payloads. - Route uploaded torrent data and detected HTTP torrent files through qBittorrent's torrent upload API. - Add delete_after_seeding handling for seeding-capable offline download tools. - Improve qBittorrent add-task response handling for 2xx and non-2xx responses. - Add qBittorrent client tests for torrent uploads, link adds, and error reporting.
- Skip seeding task removal when the delete policy is set to delete_never. - Reuse the delete-policy guard for qBittorrent and Transmission seeding cleanup.
| } | ||
| meta := strings.ToLower(encoded[:comma]) | ||
| if !strings.Contains(meta, ";base64") { | ||
| return nil, "", fmt.Errorf("torrent data URL 必须使用 base64 编码") |
There was a problem hiding this comment.
Pull request overview
This PR extends the offline download pipeline to support uploading .torrent files to qBittorrent (to better support PT scenarios), and adds a new deletion policy that defers cleanup until seeding is complete.
Changes:
- Accept
torrent_data(base64 / data URL) in the offline download request and pass decoded torrent bytes into the offline-download tool layer. - Extend the qBittorrent client/tool to add tasks from uploaded torrent data (and improve add-task error reporting / status progress handling).
- Introduce
delete_after_seedingand propagate adelete_after_timetimestamp through download/transfer tasks to defer temp cleanup until seeding finishes.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| server/handles/offline_download.go | Adds torrent_data request parsing + torrent base64 decoding/validation and task creation using uploaded torrent bytes. |
| pkg/qbittorrent/client.go | Adds AddFromTorrent, refactors “add torrent” request building, and improves non-2xx error reporting. |
| pkg/qbittorrent/client_test.go | Adds tests for link vs torrent upload behavior and non-2xx error handling. |
| internal/offline_download/tool/add.go | Adds DeleteAfterSeeding, threads TorrentData through AddURL, and gates torrent upload to qBittorrent. |
| internal/offline_download/tool/base.go | Extends AddUrlArgs to include TorrentData. |
| internal/offline_download/tool/download.go | Propagates torrent data to tools; adds seeding-time deletion scheduling via DeleteAfterTime. |
| internal/offline_download/tool/transfer.go | Adds DeleteAfterTime and defers temp cleanup when policy is delete_after_seeding. |
| internal/offline_download/qbit/qbit.go | Implements torrent-upload support in qBittorrent tool (upload bytes or fetch+upload http(s) torrent), and fixes progress calculation for size==0. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| addCalled = true | ||
| if err := r.ParseMultipartForm(1024 * 1024); err != nil { | ||
| t.Fatalf("ParseMultipartForm() error = %v", err) | ||
| } |
| addCalled = true | ||
| if err := r.ParseMultipartForm(1024 * 1024); err != nil { | ||
| t.Fatalf("ParseMultipartForm() error = %v", err) | ||
| } |
| default: | ||
| t.Fatalf("unexpected path %s", r.URL.Path) | ||
| } |
| t.Status = "offline download completed, maybe transferring" | ||
| // hack for qBittorrent | ||
| if t.tool.Name() == "qBittorrent" { | ||
| seedTime := setting.GetInt(conf.QbittorrentSeedtime, 0) | ||
| if seedTime >= 0 { | ||
| if seedDuration, ok := t.seedingDuration(); ok { | ||
| t.Status = "offline download completed, waiting for seeding" |
- Replace Chinese torrent data validation errors with English messages. - Remove the accidentally committed qBittorrent client test from Git tracking.
- Map 115 Open error code 430004 to object-not-found. - Create destination directories before enqueueing child transfer tasks. - Use the save path reported by qBittorrent for existing downloads. - Add centralized regression tests for 115 Open and qBittorrent behavior.
Track cleanup state across download, transfer, and seeding phases. Delay temporary-file removal until transfers complete, preserve active paths during startup cleanup, restore pending jobs, and cover failure and retry flows with regression tests.
- Integrate the latest main updates, including 115 Open path recovery and SDK upgrades. - Preserve torrent submissions and the persistent offline cleanup lifecycle. - Combine native ED2K routing and GuangYaPan handling with the custom download flow.
pikachuren
left a comment
There was a problem hiding this comment.
🙏 感谢 @ctaoist 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 Claude Opus 5 模型进行分析。
🎯 结论
🔄 Request Changes — 功能实用且已有基础防护,但从 URL 抓取种子存在 SSRF 风险,需要收紧
📖 概要
feat(offline-download): support torrent uploads for qBittorrent PT · 支持上传种子文件做离线下载(PT 站场景)。
核心改动:AddURLArgs 新增 TorrentData 字段,qBittorrent 走 AddFromTorrent;新增从 URL 自动抓取种子的能力;新增 DeleteAfterSeeding 删除策略。
🧭 整体方案
技术路线是「优先用种子数据、退化到磁力链接」:AddURL 依次尝试 TorrentData → 从 URL 抓取的种子 → 原有 link 方式。针对 PT 站必须用种子文件(含 passkey)而非磁力的场景,这个功能确实有实际价值。实现里已经有几处不错的防护:maxQbittorrentTorrentSize 限制 10MB、用 io.LimitReader 而非无限读取、抓取后用 torrent.Decode 校验确实是合法种子、非 qBittorrent 工具显式报错。主要顾虑是 fetchTorrentDataFromURL 的服务端请求缺乏目标地址限制。
📊 变更统计
7 个文件(+314 / -71 行) | 功能 ⭐⭐⭐⭐ | 最小改动 ⭐⭐⭐ | 前向兼容 ⭐⭐⭐⭐ | 方案设计 ⭐⭐⭐
🚨 关键问题
P0(阻塞合并):
⚠️ internal/offline_download/qbit/qbit.go:fetchTorrentDataFromURL— 该函数会用服务端身份向用户提供的任意 http/https URL 发起 GET 请求,构成 SSRF:用户可以填入http://127.0.0.1:8080/...、http://169.254.169.254/latest/meta-data/(云元数据)或内网地址,借服务端探测内网。虽然响应内容会经torrent.Decode校验、失败即静默返回false,限制了数据回显,但响应时间差异仍可用于端口/存活探测。请问是否考虑加上目标地址校验呢?例如:
// 解析后校验 IP 不落在私有/环回/链路本地网段
ips, err := net.DefaultResolver.LookupIPAddr(args.Ctx, u.Hostname())
if err != nil {
return nil, false
}
for _, ip := range ips {
if ip.IP.IsLoopback() || ip.IP.IsPrivate() || ip.IP.IsLinkLocalUnicast() {
return nil, false
}
}另外建议为该请求设置独立超时,避免慢速目标长期占用连接。
P1(建议修复):
⚠️ 同函数 — 所有失败路径都返回(nil, false)后静默退化为AddFromLink。用户上传了一个 PT 种子 URL,若抓取失败会变成用磁力方式添加(PT 场景下通常会失败或不计流量),而用户看不到任何原因。是否考虑区分「不是种子 URL,正常退化」与「是种子但抓取/解析失败,应当报错」呢?⚠️ AddURL中对非 qBittorrent 工具返回错误,但错误信息里硬编码了工具名"qBittorrent"字符串。若将来支持更多工具需同步修改,是否考虑改为能力查询(例如tool.SupportsTorrentData())?
P2(可选):
- 💡 新增
DeleteAfterSeeding删除策略与 PT 做种需求契合,方向很好。建议在文档中说明它与现有DeleteOnUploadSucceed等策略的区别与优先级~ - 💡
qbit.go中else { s.Progress = info.Progress * 100 }补充了 Size 为 0 时的进度回退,是个好改进。不过这与 #2622、#2581 在同文件有重叠改动,合并时会冲突,建议维护者留意顺序~ - 💡 10MB 的种子大小上限对绝大多数场景够用,但超大型种子(数万文件)可能超限。是否考虑提为配置项?
📂 逐文件分析
internal/offline_download/qbit/qbit.go
改动意图:支持用种子数据添加任务。
代码逻辑:AddURL 三级降级;fetchTorrentDataFromURL 校验 scheme → 发起 GET → 校验状态码 → 限长读取 → torrent.Decode 验证。
问题分析:大小限制、格式校验、状态码检查都做到位了;核心缺口是缺少目标地址校验(P0)与静默降级(P1)。
internal/offline_download/tool/add.go
改动意图:在通用层承载种子数据并做工具能力校验。
问题分析:len(args.TorrentData) == 0 的前置判断避免了种子数据被 SimpleHttp / ed2k 分支误处理,考虑周到;工具名硬编码可优化(P1)。
✅ 待处理清单
- [P0] 为
fetchTorrentDataFromURL增加目标地址校验(禁止环回/私有/链路本地)与独立超时 - [P1] 区分「正常退化」与「种子抓取失败」,后者应明确报错
- [P1] 用能力查询替代硬编码工具名
- [P2] 文档说明
DeleteAfterSeeding语义 - [P2] 与 #2622 / #2581 协调
qbit.go合并顺序
🎯 结论:🔄 Request Changes — 功能有价值、已有大小与格式防护,但服务端代抓 URL 必须先补上 SSRF 防护。
- Restrict remote torrent fetching to public addresses and pin validated DNS results. - Validate redirects, disable proxy use, and reject unsafe schemes without qBittorrent fallback. - Add regression tests for private addresses, DNS rebinding protection, and magnet compatibility.
- Add optional capability declarations with zero-value defaults for offline download tools. - Let qBittorrent advertise torrent data support and remove hardcoded tool-name checks. - Cover default and qBittorrent capabilities with regression tests.
Summary / 摘要
离线下载时,直接传递种子文件到qBittorrent,以支持PT下载。
在上传种子页面添加删除策略:
delete_after_seeding,即等待做种结束再删除。修改删除策略:
从不删除,改完后,后端针对从不删除策略,不再在 qbittorrent 中删除对应的任务,更符合PT场景。This PR has breaking changes.
/ 此 PR 包含破坏性变更。
This PR changes public API, config, storage format, or migration behavior.
/ 此 PR 修改了公开 API、配置、存储格式或迁移行为。
This PR requires corresponding changes in related repositories.
/ 此 PR 需要关联仓库同步修改。
Related repository PRs / 关联仓库 PR:
Testing / 测试
go test ./...从不删除策略的任务Checklist / 检查清单
/ 我已阅读 CONTRIBUTING。
/ 我确认此贡献符合仓库许可证、贡献规范和行为准则。
gofmt,go fmt, orprettierwhere applicable./ 我已按适用情况使用
gofmt、go fmt或prettier格式化变更代码。/ 我已在适用情况下请求相关维护者或代码所有者审查。
AI Disclosure / AI 使用声明
/ 此 PR 包含 AI 辅助内容。
Tools used / 使用工具:
Usage scope / 使用范围:
Code generation / 代码生成
Refactoring / 重构
Documentation / 文档
Tests / 测试
Translation / 翻译
Review assistance / 审查辅助
I have reviewed and validated all AI-assisted content included in this PR.
/ 我已审核并验证此 PR 中的所有 AI 辅助内容。
I have ensured that all AI-assisted commits include
Co-Authored-Byattribution./ 我已确保所有 AI 辅助提交都包含
Co-Authored-By归属信息。I can reproduce all AI-assisted content included in this PR without any AI tools.
/ 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。