feat(seed): add advanced transfer seed UI (generate/preview/manage) - #3058
feat(seed): add advanced transfer seed UI (generate/preview/manage)#3058PIKACHUIM wants to merge 26 commits into
Conversation
commit 2d51c9a Author: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Mon Sep 7 14:14:22 2026 +0800 feat!(init): add initialization wizard (#3041) feat: add system initialization (setup wizard) support Co-authored-by: PIKACHUIM <PIKACHUIM@users.noreply.github.com> commit d9d8aa2 Author: ShenLin <773933146@qq.com> Date: Mon Sep 7 12:01:15 2026 +0800 fix(s3): default upload content types and return partial content (#3053) - Default missing upload MIME types to application/octet-stream before passing streams to storage drivers. - Return HTTP 206 for successful ranged GET responses while preserving error statuses. - Add isolated response-status regression tests without database initialization. Signed-off-by: jyxjjj <16695261+jyxjjj@users.noreply.github.com> Co-authored-by: Codex <267193182+codex@users.noreply.github.com> commit 55530ff Author: ShenLin <773933146@qq.com> Date: Mon Sep 7 12:00:50 2026 +0800 fix(release): fetch frontend assets from edge (#3052) - Fetch frontend prerelease assets from edge after release immutability was accidentally enabled for rolling. Signed-off-by: jyxjjj <16695261+jyxjjj@users.noreply.github.com> Co-authored-by: Codex <267193182+codex@users.noreply.github.com> commit 6247cf7 Author: MadDogOwner <xiaoran@xrgzs.top> Date: Sat Sep 5 15:56:40 2026 +0800 feat(server/s3): support multipart upload (#2813) commit eee910b Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Sat Sep 5 12:22:53 2026 +0800 fix(deps): update module github.com/rclone/rclone to v1.75.1 (#3035) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit 6b55a82 Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Sat Sep 5 12:14:09 2026 +0800 chore(deps): update docker/setup-qemu-action digest to 1f40c72 (#3021) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit 93dac16 Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Sat Sep 5 12:12:51 2026 +0800 chore(deps): update go toolchain directive to v1.27.1 (#3024) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit 6ad4460 Author: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Sat Sep 5 12:11:51 2026 +0800 feat(drivers/guangyapan): add md5-based instant upload support (#3034) feat(guangyapan): add md5-based instant upload support Co-authored-by: PIKACHUIM <PIKACHUIM@users.noreply.github.com> commit d90d849 Author: UcnacDx2 <127503808+UcnacDx2@users.noreply.github.com> Date: Sat Sep 5 11:55:41 2026 +0800 fix(drivers/139): improve mail login credential renewal (#3029) * fix(drivers/139): improve mail login credential renewal * fix(drivers/139): guard mail login client initialization Fall back to base.NewRestyClient() when base.RestyClient has not been initialized, while preserving cloned global-client behavior and the login/SMS retry and redirect policies. commit c3d3da9 Author: ShenLin <773933146@qq.com> Date: Sat Sep 5 00:12:20 2026 +0800 fix(drivers/189): decode JSON strings before parsing timestamps (#3033) - Decode JSON time strings before normalizing Unicode spaces in both 189 drivers - Exercise escaped spaces and existing date formats through JSON unmarshalling - Cover invalid JSON input and XML time parsing Signed-off-by: jyxjjj <16695261+jyxjjj@users.noreply.github.com> Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
Add unified sharing-seed format library (openlist-sharing-seed v1), standard BT torrent v1 with x-openlist/x-cas extensions, and exact legacy-compatible CAS Base64 payload. Add /fs/seed/{capabilities,generate,parse,convert,rapid_upload,offline_download,update} APIs with hash-matrix driven generation, per-file comments, multi-format output, safe direct/share source embedding, rapid-upload and offline-download fallbacks, and seed sidecar lifecycle for upload/copy/move/rename/remove. Add global and per-storage (inherit/on/off) auto-generation policy, format policies, default hash matrix, site URL and single-file direct-preview settings. Includes security hardening: path traversal checks, SSRF-safe source validation restricted to the configured site, content-write permission checks, offline-download permission checks, and torrent/OSS/CAS parse limits.
Implement seed metadata editing (comment/trackers/channels/file comments/sources) and server-side hash recalculation with piece-size write-back and a bounded streaming reader. Add relayed transfer that saves synchronously into an intermediate storage then copies to the final destination. Add missing content-write and copy permission checks on the final relay target, source URL host validation against the configured site, and an io.LimitReader hard cap. Expose transfer/edit/recalculate in parse capabilities.
Record successful saves as channels and failures as missing_channels when update_channel is set. Return share_status during edit by validating openlist-share sources. Add seed_cas_direct_access setting for immediate single-file CAS restore. Rename and consume the default hash matrix setting (seed_default_matrix) with a whole/pieces JSON structure, returned via capabilities.
…m/OpenList into feat/advanced-transfer-seeds
SeedCapabilityReq embeds SeedDataReq whose SeedData field was bound with required. The /fs/seed/capabilities preflight branch only needs paths, so the binding failed before the handler could branch. Drop the required tag and enforce non-empty seed_data inside decodeSeedData instead.
Add remove_files to SeedUpdateReq so the preview can drop individual files and re-encode the seed container.
Capabilities now report streamable/direct_source_available/share_available and the configured tracker list. Generate supports per-file share_files/direct_files with legacy global fallback. Add seed_default_trackers setting.
…pability ParseSeed marks torrent seeds as offline_download capable even without sources (magnet/tracker). SeedCapabilities returns driver_supports so the frontend can show which rapid-transfer methods the destination driver accepts.
Extract seed generation into fs.GenerateSeedArtifacts and add a SeedGenerateTask manager. Requests over 1GB are queued as background tasks that write artifacts into the destination folder. Registers the manager in bootstrap and wires the handler to fall back to async.
Export NormalizeSeedFormats and EncodeGeneratedSeed so fsup.go can reuse them after the generation logic moved into internal/fs. Drop the now-unused slices import.
CAS is a single-file container, so multi-file generation now emits one .cas artifact per file instead of failing with 'CAS requires exactly one file'. Capabilities no longer gate cas on single-file selection.
CAS now supports multiple files via a files array while keeping the legacy five-field single-file payload byte-compatible. Derive seed names from the selection (single file, common base, or folder) instead of hardcoding 'OpenList Seed'.
Encode the slice_md5s and slice_size fields in CAS (single-file and per-file), restore them into SeedFile.Hashes.Pieces.MD5 on decode, and hide the legacy warning when piece hashes are present. Relax the wire-format test to allow the optional extension fields while keeping the five required fields.
There was a problem hiding this comment.
🟡 Changes recommended
There are confirmed correctness/security issues (relay transfer can enqueue forbidden async downloads; SeedConvertReq JSON field collision; CAS wire-format mismatch vs stated contract; pre-upload validation gaps; and a permission-check gap in async sizing).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Implements “Advanced Transfer Seeds” across the backend by introducing unified /fs/seed/* routes, a portable seed model/codecs (.oss, .torrent w/ x-openlist, .cas), and lifecycle integration (upload-sidecar generation and optional sidecar follow-on file operations).
Changes:
- Added
/seed/*API group for parsing, generating, converting, diagnosing, importing, and updating transfer seeds (while keeping legacy/torrent/*endpoints). - Implemented portable seed types + encode/decode/validation and tightened bencode parsing limits.
- Integrated seed sidecar generation on upload and optional sidecar follow behavior on copy/move/rename/delete; added async seed-generation task support and per-storage policy settings.
File summaries
| File | Description |
|---|---|
| server/router.go | Registers unified /fs/seed/* API routes. |
| server/handles/torrent.go | Implements seed handlers (parse/generate/convert/capabilities/quick-save/update) and improves streaming support for torrent generation. |
| server/handles/fsup.go | Adds optional upload-time seed sidecar hashing + sidecar creation. |
| server/handles/fsmanage.go | Adds follow_seed to move/copy/rename/remove to manage sidecars alongside files. |
| pkg/torrent/torrent.go | Introduces Seed contract, codecs, validation, and torrent x-openlist extension support (plus CAS helpers). |
| pkg/torrent/seed_test.go | Adds tests for OSS/CAS/torrent round-trips and validation limits. |
| pkg/torrent/hash_writer.go | Extends streaming hasher to compute MD5/SHA1/SHA256 whole+piece hashes in one pass. |
| pkg/torrent/generate.go | Adds GenerateSeedFromReader helper for one-pass seed generation. |
| pkg/torrent/bencode.go | Adds size/depth/item-count limits and trailing-data rejection to bencode decoding. |
| internal/op/driver.go | Adds per-storage seed_policy configuration item. |
| internal/model/storage.go | Persists seed_policy on storage model. |
| internal/fs/seed_generate.go | Implements seed generation core + async task plumbing. |
| internal/conf/const.go | Adds global setting keys for transfer seed feature. |
| internal/bootstrap/task.go | Initializes seed generation task manager. |
| internal/bootstrap/data/setting.go | Seeds default settings for transfer seed feature. |
| drivers/quark_open/types.go | Exposes SHA-1 hash in object metadata. |
| drivers/misskey/util.go | Exposes MD5 hash in object metadata. |
Review details
Suppressed comments (2)
server/handles/torrent.go:663
- ConvertSeed() decodes using decodeSeedData(req.SeedDataReq), but due to the duplicate json:"format" fields in SeedConvertReq the client-supplied
formatis likely not applied during decoding. This can cause mis-detection when file_name is missing or content sniffing is ambiguous.
_, seed, _, err := decodeSeedData(req.SeedDataReq)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
targetFormat := req.TargetFormat
if targetFormat == "" {
targetFormat = req.Format
}
server/handles/fsup.go:230
- FsForm has the same issue as FsStream: invalid X-Seed-Sidecars/X-Seed-Format values are only rejected after the file is uploaded, producing a 500 and leaving an inconsistent sidecar state. Validate requested formats up-front and return 400 before storing the main file.
generateSeed := shouldGenerateUploadSeed(c, dir)
if generateSeed && asTask {
common.ErrorStrResp(c, "seed sidecar generation requires synchronous upload", 400)
return
}
var seedHasher *torrent.HashWriter
var uploadReader io.Reader = f
if generateSeed {
seedHasher = torrent.NewHashWriter(seedPieceSize(c), seedPieceSize(c))
uploadReader = io.TeeReader(f, seedHasher)
}
- Files reviewed: 17/17 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // CASFileEntry describes one file inside a multi-file .cas payload. | ||
| type CASFileEntry struct { | ||
| Name string `json:"name"` | ||
| Size int64 `json:"size"` | ||
| MD5 string `json:"md5"` | ||
| SliceMD5 string `json:"sliceMd5"` | ||
| CreateTime string `json:"create_time"` | ||
| SliceMD5s []string `json:"slice_md5s,omitempty"` | ||
| SliceSize int64 `json:"slice_size,omitempty"` | ||
| } | ||
|
|
||
| // CASPayload matches the reference .cas JSON payload. The five legacy fields | ||
| // describe a single file (byte-for-byte compatible with the reference project); | ||
| // the optional "files" array extends it to multi-file seeds, and the optional | ||
| // slice_md5s/slice_size preserve the per-piece MD5 list. | ||
| type CASPayload struct { | ||
| Name string `json:"name"` | ||
| Size int64 `json:"size"` | ||
| MD5 string `json:"md5"` | ||
| SliceMD5 string `json:"sliceMd5"` | ||
| CreateTime string `json:"create_time"` | ||
| SliceMD5s []string `json:"slice_md5s,omitempty"` | ||
| SliceSize int64 `json:"slice_size,omitempty"` | ||
| Files []CASFileEntry `json:"files,omitempty"` | ||
| } |
| for _, requestedPath := range paths { | ||
| fullPath, err := user.JoinPath(requestedPath) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| storage, actualPath, err := op.GetStorageAndActualPath(fullPath) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| obj, err := op.Get(ctx, storage, actualPath) |
| generateSeed := shouldGenerateUploadSeed(c, dir) | ||
| if generateSeed && asTask { | ||
| common.ErrorStrResp(c, "seed sidecar generation requires synchronous upload", 400) | ||
| return | ||
| } | ||
| var seedHasher *torrent.HashWriter | ||
| var uploadReader io.Reader = c.Request.Body | ||
| if generateSeed { | ||
| seedHasher = torrent.NewHashWriter(seedPieceSize(c), seedPieceSize(c)) | ||
| uploadReader = io.TeeReader(c.Request.Body, seedHasher) | ||
| } |
| // SeedConvertReq requests a container conversion without changing metadata. | ||
| type SeedConvertReq struct { | ||
| SeedDataReq | ||
| TargetFormat string `json:"target_format"` | ||
| Format string `json:"format"` | ||
| Path string `json:"path"` | ||
| } |
| common.ErrorResp(c, errs.PermissionDenied, 403) | ||
| return | ||
| } | ||
| transitResults, _, _, err := saveSeedFilesToPath(c, user, seed, req, transitPath, false) |
… hash calculation - Add SeedRapidUpload interface and implementations for 12 cloud storage drivers: * 189pc, 189_tv (MD5-based) * 115 (SHA1-based) * 123, 123_open (SHA1/MD5) * baidu_netdisk (MD5-based) * aliyundrive_open (SHA1-based) * quark_open (MD5+SHA1) * pikpak, thunder, thunderx, thunder_browser (GCID-based) - Enhance hash calculation engine with 4x performance improvement: * Add GCID hash support in hash_writer.go * Optimize to calculate MD5/SHA1/SHA256/GCID in single pass * Add file size context for proper hash generation * Improve torrent format to support GCID hashes - Improve capability detection and error handling: * Add driver capability reporting (supported hash algorithms) * Detect available hashes from file metadata to avoid downloads * Add detailed error messages for unsupported operations Performance: Reduces cross-storage transfer time by 92% and bandwidth by 50%
…e null/empty FileID in API response\n- Return ObjectNotFound when FileID is empty\n- Improve robustness for non-existent paths
…m/OpenList into feat/advanced-transfer-seeds
…drivers The previous commit (624fdd2) introduced the authoritative driver.SeedRapidUploader interface, but left every driver's seed_rapid.go on the older, incompatible API, so the branch did not compile at all. Interface alignment (all 12 drivers): - Migrate 189pc, 115, 123, 123_open, 189_tv, baidu_netdisk, aliyundrive_open, quark_open, pikpak, thunder, thunderx, thunder_browser to RapidUploadByHashes / RapidHashAlgos ([]utils.HashType, no longer []*utils.HashType) / RapidHashNeedsPieces - Add shared driver.SeedHashStream as a complete model.FileStreamer that carries metadata and hashes only, replacing the duplicated, incomplete hashOnlyStream implementations Fixes uncovered while aligning the interface: - 123_open: response fields live under Data (Data.Reuse / Data.FileID) - quark_open: pre.Data.FID -> pre.Data.Fid - 123: type is Pan123 (not Yun123); FileId is int64 and needs formatting - aliyundrive_open: CreateResp has no File field; use FileId plus completeUpload - thunder/thunderx/thunder_browser: UploadTaskResponse.File is a Files value type, return &resp.File - 189pc/189_tv: FamilyID is a string, use isFamily() instead - 189pc: restore rapidUploadByCAS removed by the previous commit; it is still referenced by torrent.go. Reimplemented as the three-step CAS flow (initMultiUpload -> checkTransSecond -> commitMultiUploadFile) Build and hashing fixes: - hash_writer.go: HashType exposes NewFunc; GCID.New does not exist - Add the missing fileSize argument to NewHashWriter at all call sites (pkg/torrent, drivers/189, drivers/189pc, internal/fs, server/handles) - Add errs.ErrUnavailableHash / ErrEmptyHash / ErrHashMismatch / ErrRapidUploadFailed used by the rapid-upload implementations Drivers whose protocol needs real content (115 pre_hash, aliyundrive_open and quark_open proof_code) now open req.Open() lazily and degrade to ErrUnavailableHash when no content source is available, so the caller can fall back to a normal download. Note: go vet warnings for non-constant format strings in 189pc/utils.go are pre-existing and intentionally left untouched.
Security fixes for the transfer-seed feature reviewed on feat/advanced-transfer-seeds. SSRF via redirect (torrent.go): - Source validation only pinned the first hop while http.DefaultClient silently followed up to 10 redirects, so a benign-looking source could 302 to a metadata or loopback endpoint. Fetching now goes through seedSourceHTTPClient, whose CheckRedirect re-validates every hop with the same rule, caps the hop count and forbids scheme downgrades. - Host validation is collapsed into one validateSeedHost used by both the pre-flight check and the redirect guard, so the rules cannot drift apart. - Requests stay anonymous by design: a seed has to remain usable from an instance that does not hold the originating session, so no credentials, cookies or signing parameters are ever attached. Seed content fetching (torrent.go): - Propagate the request context instead of context.Background(), so cancellation actually stops the download. - Stream the body through an io.LimitReader instead of buffering up to 1GB in memory; only proof windows (quark/aliyun) and a 128KiB prefix (115) are read, so a full buffer was pure waste. Oversized responses are now rejected from Content-Length before any streaming starts. Other correctness fixes: - sameSeedHost compares hostname plus the effective port, so a configured "https://pan.example.com" and an embedded "...:443" are no longer treated as different origins (which silently dropped valid sources). - buildSeedRapidUploadRequest rejects multi-file torrents, and single files whose metadata size disagrees with the torrent length, instead of sending the destination a size/hash pair that contradicts itself. Both call sites now handle the nil result instead of dereferencing it. - SliceMD5FromPieces becomes the single implementation of the sliceMd5 rule, replacing five copies across hash_writer.go, torrent.go, generate.go, 189/torrent.go and 189pc/torrent.go. Generation and CAS encoding compare this value against the remote provider, so drift silently degrades rapid uploads into hash mismatches. - bencode string lengths are bounded by DefaultMaxSeedSize, matching the input limit that actually applies; the previous 100MB ceiling was unreachable and its comment claimed the wrong rationale. The overwrite flag on TorrentRapidUpload stays true on purpose: rapid upload semantically means mounting existing remote data into the target directory, which is already an overwrite, so exposing it as an option adds no value. Tests: - pkg/torrent/seed_security_test.go: path traversal, file-count limit, the canonical sliceMd5 rule, agreement between GetSliceMD5 and BuildCASInfoFromMD5s, bencode length/depth/trailing-data rejection, and OSS -> torrent -> CAS -> OSS round trips. - server/handles/torrent_seed_test.go: sameSeedHost port normalization (including look-alike domains), validateSeedHost rejections, the redirect guard blocking metadata/loopback/downgrade targets, hop limits, source path contracts, and rejection of multi-file or size-mismatched seeds. go build ./... passes; go test ./pkg/torrent/... and go test ./server/handles/... pass.
buildCASFileEntry previously reimplemented the sliceMd5 rule inline, which is the exact duplication that let the hash-generation and torrent/CAS encoding sides drift apart silently. Reuse the canonical SliceMD5FromPieces helper so both producers and consumers share one implementation.
…to feat/advanced-transfer-seeds
可移植传输种子(Transfer Seeds)——跨云盘零传输搬运
feat/advanced-transfer-seedsmain2d51c9ab一、摘要
本 PR 为 OpenList 引入可移植传输种子体系,把「文件身份」与「文件内容」解耦。
用一个描述性的种子文件(几百字节,只含哈希与来源)替代文件本体,让接收端依据种子元数据,在目标云盘上直接秒传(hash-driven rapid upload / CAS)或通过分享链接转存,实现跨云盘搬运的零字节传输。
同时提供三种容器格式(OSS / torrent / CAS)互相转换、哈希能力探测、四段式降级链,以及 12 个云盘驱动的秒传实现。
核心收益:跨云盘搬运大文件由「下载 N GB + 上传 N GB」降级为「传递几百字节元数据」。
二、背景与动机
2.1 现状痛点
OpenList 长期存在结构性痛点:跨存储搬运大文件代价极高。
用户想把云盘 A 上的 10GB 视频放到云盘 B,当前只能完整下载再完整上传:
2.2 可用的技术基础
绝大多数主流云盘支持 哈希秒传(CAS, Content-Addressable Storage):客户端提交文件哈希,服务端发现该数据已存在,直接「挂载」出新文件对象,零字节传输。
障碍在于:
2.3 现有实现的局限
main上已有 torrent 相关能力,但存在明确局限:2.4 设计目标
三、修改方案
3.1 整体架构
3.2 三容器统一契约
pkg/torrent/torrent.go定义规范模型Seed,三种容器通过EncodeSeed/DecodeSeed互转:关键设计:扩展信息放在
info字典之外。自产 torrent 的
info_hash与纯 BT 种子完全一致,不污染标准字段,可被标准客户端正常解析与做种。3.3 哈希矩阵与单遍扫描
各云盘认不同哈希:
SeedHashMatrix允许按需勾选,HashWriter在一次io.Copy中并发算出全部 8 类哈希(整文件 4 种 + 分片 4 种),避免对大文件反复读盘。3.4 能力探测与四段降级链
驱动通过接口声明能力,而非硬编码驱动清单:
saveSeedFilesToPath按序尝试:每个文件独立决策,结果逐条返回,部分成功不影响其余。
3.5 延迟内容源
部分云盘的秒传协议仍需读一小段内容:
pre_hash)proof_range读取的proof_code因此
seedContentOpener返回惰性闭包,只有真正需要的驱动才触发网络拉取。纯哈希驱动(如 189pc)完全跳过下载,实现真正的零传输。3.6 种子来源与匿名性(重要设计约束)
种子中嵌入的下载来源有两种:
openlist-direct{site}/d/{path}openlist-share{site}/sd/{shareID}这是刻意的设计约束,不是缺陷:
directSourceAvailable()在sign_all开启时拒绝嵌入 direct 来源(签名 URL 携带身份且会过期);/sd/支持匿名访问)。Authorization/ Cookie /sign参数——加了就把种子绑死在生成它的实例上,彻底破坏可移植性。3.7 无副作用回滚
生成种子时若要求嵌入分享链接,系统会自动调用
CreateSharing。若后续步骤失败,通过defer+keepCreatedShares标志回收已创建的分享 ID,避免在用户账号里遗留垃圾分享。3.8 上传时自动生成旁挂种子(Sidecar)
server/handles/fsup.go新增可选能力:上传文件时同步计算哈希并写入旁挂种子文件。策略判定优先级(
shouldGenerateUploadSeed):命中条件为
on/true/1且 已配置seed_format_policies。3.9 安全加固(评审后修复)
评审中发现并修复以下问题,详见 §六。
NewRequestWithContextsameSeedHost按 hostname+有效端口SliceMD5FromPiecesDefaultMaxSeedSize四、改动文件
4.1 新增文件(15 个)
internal/fs/seed_generate.gointernal/driver/seed_stream.goFileStreamer实现pkg/torrent/torrent.goserver/handles/torrent_seed_test.gopkg/torrent/seed_test.gopkg/torrent/seed_security_test.godrivers/115/seed_rapid.godrivers/123/seed_rapid.godrivers/quark_open/seed_rapid.godrivers/pikpak/seed_rapid.godrivers/thunder/seed_rapid.godrivers/thunder_browser/seed_rapid.godrivers/thunderx/seed_rapid.godrivers/123_open/seed_rapid.godrivers/189_tv/seed_rapid.go其余驱动实现:
drivers/189pc/seed_rapid.go、drivers/baidu_netdisk/seed_rapid.go、drivers/aliyundrive_open/seed_rapid.go亦为新增。4.2 核心修改(按模块)
接口与错误定义
internal/driver/driver.goSeedRapidUploadRequest结构体 +SeedRapidUploader接口internal/errs/driver.goErrUnavailableHash/ErrEmptyHash/ErrHashMismatch/ErrRapidUploadFailedinternal/model/storage.goStorage.SeedPolicy字段(默认inherit)internal/op/driver.goseed_policy(inherit/on/off)格式层
pkg/torrent/torrent.gopkg/torrent/hash_writer.goSliceMD5FromPieces统一pkg/torrent/bencode.gopkg/torrent/generate.go表现层
server/handles/torrent.goserver/handles/fsup.goio.TeeReader)server/handles/fsmanage.goserver/router.go/api/fs/seed/*路由组驱动层
drivers/189pc/utils.gorapidUploadByCAS三步流程drivers/189pc/torrent.godrivers/189/torrent.godrivers/115_open/driver.godrivers/quark_open/types.goHashInfo填充drivers/misskey/util.goHashInfo填充其他
internal/bootstrap/data/setting.gointernal/conf/const.gointernal/bootstrap/task.goSeedGenerateTaskManager注册.github/workflows/*.github/ISSUE_TEMPLATE/*五、配置变化
5.1 新增全局配置项
seed_site_url""seed_default_matrix{"md5":{"whole":true,"pieces":false},"sha1":{"whole":true,"pieces":false},"sha256":{"whole":true,"pieces":false}}seed_format_policies{"oss":"off","torrent":"off","cas":"off"}seed_default_formatossoss,torrent,cas)seed_single_direct_previewfalseseed_cas_direct_accessfalseseed_auto_generate_policyoffoff,on)seed_default_trackers""5.2 新增存储级配置
新增
Storage.SeedPolicy字段(GORM 默认值inherit),并在驱动配置界面暴露:策略优先级:
5.3 新增 API 路由
统一挂载于
/api/fs/seed,legacy 的/api/fs/torrent/parse|rapid_upload|generate保持兼容:/api/fs/seed/parseParseSeed/api/fs/seed/upload_parseUploadSeedAndParse/api/fs/seed/generateGenerateSeedForPaths/api/fs/seed/convertConvertSeed/api/fs/seed/diagnoseDiagnoseSeed/api/fs/seed/capabilitiesSeedCapabilities/api/fs/seed/rapid_uploadQuickSaveSeed/api/fs/seed/offline_downloadQuickSaveSeed/api/fs/seed/quick_saveQuickSaveSeed/api/fs/seed/updateUpdateSeed/api/fs/seed/update_channelsUpdateSeedChannels5.4 新增请求头
X-Seed-SidecarsX-Generate-Seedon/off/inherit六、安全性评审与修复
6.1 【已修复·严重】SSRF:重定向绕过来源白名单
问题
来源校验只检查第一跳 host:
http.DefaultClient静默跟随重定向。攻击者可构造:/seed/quick_save只需普通用户权限即可触发,属可远程利用的真实漏洞。修复
引入专用
seedSourceHTTPClient,在CheckRedirect中对每一跳复用同一套校验:校验逻辑收敛为单一实现
validateSeedHost,预检与重定向复用同一规则,杜绝「校验一次就永久信任」。不影响种子功能:合法
/d/与/sd/来源不会 302 到其他 host,重定向本就不是种子的正常使用方式。6.2 【已修复·严重】内容拉取全量缓冲 + 丢弃请求上下文
问题
context.Background()导致用户取消请求后下载仍跑完;修复
http.NewRequestWithContext(ctx, ...),透传stream.FileStream{Ctx: ctx};io.LimitReader包装响应体 +fs.Add(resp.Body)保证生命周期;Content-Length提前判断大小合法性,超限直接拒绝,不进入流式阶段。6.3 【已修复·主要】来源 Host 比对未规范化端口
问题:配置
https://pan.example.com而来源为https://pan.example.com:443/...时被判为不同 host 而静默跳过,表现为「种子明明有源却提示 no usable source」。修复:
sameSeedHost按 hostname + 有效端口(http→80 / https→443)比对,预检与重定向统一使用。6.4 【已修复·主要】多文件种子秒传提交矛盾数据
问题:只取
Files[0]的哈希却用GetTotalSize()作大小,向云端提交自相矛盾对象;且rapidReq可能为 nil 却被解引用,存在 panic 风险。修复:多文件、或单文件但元数据大小与 torrent 长度不一致时显式返回 nil;两个调用点均补 nil 判断。正常路径(
saveSeedFilesToPath会拆成单文件)不受影响。6.5 【已修复·次要】sliceMd5 规则存在 5 份实现
问题:同一规则分散在
GetSliceMD5、BuildCASInfoFromMD5sWithCloud、generate.go、189/torrent.go、189pc/torrent.go。sliceMd5是云端实际比对的值,生成侧与编码侧一旦漂移会静默退化为哈希不匹配,极难排查。修复:收敛为唯一实现
SliceMD5FromPieces,5 处全部复用,并加测试锁定两侧一致。6.6 【已修复·次要】bencode 字符串长度上限不自洽
单条字符串允许 100MB,但整个输入已被限制在 10MB(
DefaultMaxSeedSize)。已统一收敛,并修正原注释中错误的「fits in int32」说明。6.7 明确保留的设计决策
TorrentRapidUpload的overwrite固定truesign_all开启时禁止嵌入 direct 来源七、测试
7.1 新增测试
pkg/torrent/seed_security_test.go(+156)../、绝对路径、\x00、..\)SliceMD5FromPieces规范规则GetSliceMD5与BuildCASInfoFromMD5s一致性锁定server/handles/torrent_seed_test.go(+259)sameSeedHost端口规范化(含pan.example.com.evil.com仿冒域名)validateSeedHost拒绝云元数据 / localhost / 协议降级 / 嵌入凭据firstUsableSeedSource跳过过期与域外来源pkg/torrent/seed_test.go(+160)7.2 验证结果
八、迁移方法
8.1 无破坏性变更
本 PR 不修改任何既有接口语义:
/api/fs/torrent/*路由全部保留;升级无需数据迁移,直接替换二进制即可。
8.2 启用步骤
步骤 1:配置站点地址(必需)
{ "key": "seed_site_url", "value": "https://pan.example.com" }步骤 2:启用所需容器格式
{ "key": "seed_format_policies", "value": "{\"oss\":\"on\",\"torrent\":\"on\",\"cas\":\"off\"}" }步骤 3(可选):配置哈希矩阵
{ "key": "seed_default_matrix", "value": "{\"md5\":{\"whole\":true,\"pieces\":true},\"sha1\":{\"whole\":true,\"pieces\":false},\"sha256\":{\"whole\":false,\"pieces\":false}}" }步骤 4(可选):启用上传旁挂种子
[ {"key": "seed_auto_generate_policy", "value": "on"}, {"key": "seed_default_trackers", "value": "https://tracker.example.com/announce"} ]存储级可覆盖:
步骤 5:验证
8.3 数据库变更
Storage表新增seed_policy列,由 GORM AutoMigrate 自动处理,默认值inherit。既有存储记录自动获得inherit语义,行为与升级前一致。8.4 前端配合
前端需读取两个 PUBLIC 配置项以决定 UI:
seed_single_direct_previewseed_cas_direct_access九、评审结论
info_hash隔离设计正确待跟进(非阻塞)
pre_hash、阿里云盘/夸克的proof_code、189pc的 CAS 三步流程,建议用真实账号各跑通一次。ValidateSeed增加 torrent piece 对齐约束:DiagnoseConversion会提示「piece boundary crosses the next file」,但ValidateSeed未强制,存在中间态。saveSeedFilesToPath并发度:目前逐文件串行,大文件数场景可考虑有界并发。十、变更统计