Skip to content

feat(fs): add pipelined multipart upload#2723

Open
thetapilla wants to merge 4 commits into
OpenListTeam:mainfrom
thetapilla:feat/multipart-upload
Open

feat(fs): add pipelined multipart upload#2723
thetapilla wants to merge 4 commits into
OpenListTeam:mainfrom
thetapilla:feat/multipart-upload

Conversation

@thetapilla

@thetapilla thetapilla commented Jul 5, 2026

Copy link
Copy Markdown

Summary / 摘要

实现网页端分块上传(multipart upload),解决经 CDN 反代(如 Cloudflare 免费计划 100MB 请求体上限)时无法上传大文件的问题,并为上传链路补上断点续传与失败重试能力。

用户可感知的变化:

  • 新增第四种上传方式 Multipart :客户端按分片并发上传,单个请求体可控制在 CDN 限制之内(分片大小可配,默认 10MB)
  • 断点续传:上传中断后(网络断开、刷新页面)重新发起同一文件的上传会自动跳过已接收的分片继续
  • 失败重试:整体上传失败后会话元数据保留 30 分钟,客户端从 chunk 0 重灌即可重试,无需重新走完整流程
  • 秒传:勾选"尝试秒传"时哈希随会话初始化先行到达驱动,秒传命中则后续分片全部免传(对应 feat(fs): implement client-side multipart upload with File-Path header #1877 评审意见中的期望)
  • 管理端新增两个设置:multipart_enabled(功能开关)、multipart_chunk_size(分片大小 MB,保存时校验 1–90)

实现要点:

  • 流水线而非收齐再传:会话初始化即调用 op.Put 启动驱动上传,客户端→服务端与服务端→存储两段并行,分别占用服务器下行/上行带宽
  • 环形窗口重组:乱序并发到达的分片经环形暂存文件重组为顺序流,每会话磁盘占用封顶为 8×分片大小(默认约 80MB),分片被驱动消费后即释放;不做任何全量落盘
  • 零驱动改动:驱动侧拿到的仍是标准的顺序 FileStreamer,与现有 /fs/put 直传对驱动完全同构,87 个驱动无需任何适配(已在本地/S3/WebDAV/百度/123 五种驱动形态上实测)
  • 浏览器友好的流控:窗口满时分片请求在服务端有界停靠(上限 10s,远低于 CDN 请求时限)等待槽位而非立即拒绝;所有响应路径先排干请求体再回包——否则浏览器会把提前响应视为连接错误
  • 数据安全:断点续传要求哈希证明文件身份(防止同路径同大小不同内容的静默混合);重灌时逐片 CRC32 校验拦截"两次重试间文件内容变化"
  • 回收完整:成功/中止/失败/被取代/过期/重启孤儿六类终止路径均验证无分片数据残留,孤儿暂存文件在服务启动时清扫

关于与 As-Task 的关系:本 PR 中 multipart 有意不支持"添加为任务"。As-Task 的语义是"先收完、入队、稍后传",必然要求文件全量落盘,与流水线形态(收片即传、磁盘占用窗口封顶、秒传免传后续分片)在根本上互斥;且 multipart 会话自身已具备任务队列的核心价值(进度可查、失败可断点重试)。若社区确有"multipart + As-Task"的需求(全量落盘后再推远端,形态接近 #1877 的 store-and-forward 方案),可以作为后续独立 PR 在本 PR 的会话框架上补充,实现难度低于本 PR。

兼容性说明:不改动任何既有端点与驱动接口;新增 /api/fs/multipart/{init,chunk,complete,status,abort} 五个端点(header 风格与 /fs/put 对齐);新增两个 PUBLIC 设置项;会话仅存内存(与 Upload 任务不持久化的既有决策对齐),重启后未完成会话失效、暂存自动清扫。

  • 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:

Related Issues / 关联 Issue

Relates to #460

说明:#460 同时提出了网页端与 WebDAV 两个方向的分块上传,本 PR 覆盖网页端;WebDAV 分块(如 Nextcloud 兼容协议)不在本 PR 范围内,但服务端重组器与协议无关,后续可将 WebDAV/Nextcloud 入口作为它的另一个前端接入。

致谢:感谢 #1877@dnslin)对该方向的探索,以及 @KirCute 在该 PR 评审中提出的两点要求——"开始上传时即构建 FileStream 调用驱动(两段带宽并行)"与"首个请求携带文件哈希触发秒传、命中后免传后续分片"。本 PR 为独立的从零实现,上述两点均为本设计的出发点并已实现(哈希放在 init 请求而非首个分片,语义等价且更早:秒传判定可在任何分片到达前完成)。

Testing / 测试

测试平台:macOS arm64,Go 1.26.4。

  • go test ./...
    本机环境因缺少 macFUSE 头文件无法构建 internal/fuse(存量平台问题,与本 PR 无关),故未跑全仓测试;实际执行为下述包级测试与主二进制构建。
  • Manual test / 手动测试:

自动化测试:

  • go test -race -count=1 ./internal/multipart/:32 个用例全部通过,覆盖乱序/并发写入、背压停靠与唤醒、幂等重发、秒传与在途分片的竞态(含确定性竞态构造与变异验证)、窗口关闭与完成裁决之间的间隙、CRC 重灌一致性、会话状态机全迁移、GC 过期与阻塞读者唤醒
  • gofmt -l 对全部改动文件零违规;go build . 主二进制构建通过
  • 本地 e2e 脚本(local 驱动,59 项断言):100MB 乱序并发上传后哈希比对、断点续传、只读目录故障注入→修复→重灌→成功、abort 清理(含目标端无部分文件)、429 背压语义、设置门禁与非法值拒绝、无哈希重灌的数据混合防护
  • 资源回收审计:成功/中止/可恢复失败/重灌/被新上传取代/服务重启孤儿清扫,全部验证无暂存残留,并含全局空目录断言
  • 浏览器行为模拟压测(全局 6 连接上限、发送异常视为响应不可读):3×90MB 并发上传至限速 3MB/s 的存储,零错误、零误报,总耗时 90s(等于限速理论下限)

跨驱动实测:

  • MinIO S3(分片会话型驱动):200MB 上传+下载回读哈希一致;另测每片间隔 3s 的慢速客户端,会话空闲容忍正常
  • WebDAV 回环(单请求流式驱动):30MB 上传,落盘字节一致
  • 百度网盘真实账号(先落盘算哈希的 spool 型驱动):20MB 正常传输链路通过;秒传接口被百度侧拒绝(errno 31500,第三方应用限制)时回退为正常上传,数据完整
  • 123 云盘真实账号:秒传命中实录——同内容带 MD5 重传 0.8s 完成(对照全量传输 10.4s),服务端到存储零流量

浏览器手动验证(配合前端配套分支联调):覆盖已有文件 × 尝试秒传四种组合、刷新页面后重拖续传、3×3 大文件并发上传、失败行重试按钮断点续传、管理端分片大小非法值拒绝提示。

Checklist / 检查清单

  • I have read CONTRIBUTING.
    / 我已阅读 CONTRIBUTING
  • I confirm this contribution follows the repository license, contribution policy, and code of conduct.
    / 我确认此贡献符合仓库许可证、贡献规范和行为准则。
  • I have formatted the changed code with gofmt, go fmt, or prettier where applicable.
    / 我已按适用情况使用 gofmtgo fmtprettier 格式化变更代码。
  • I have requested review from relevant maintainers or code owners where applicable.
    / 我已在适用情况下请求相关维护者或代码所有者审查。

AI Disclosure / AI 使用声明

  • This PR includes AI-assisted content.
    / 此 PR 包含 AI 辅助内容。

Tools used / 使用工具:

  • ChatGPT
  • Codex
  • GitHub Copilot
  • Claude
  • Gemini
  • Other (please specify) / 其他(请注明):

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-By attribution.
    / 我已确保所有 AI 辅助提交都包含 Co-Authored-By 归属信息。

  • I can reproduce all AI-assisted content included in this PR without any AI tools.
    / 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。

- Reassemble concurrently uploaded chunks into a sequential stream through a ring file, bounding disk usage to slots*chunkSize per session
- Park writers up to a deadline when their slot is busy instead of rejecting instantly, so flow control does not surface as connection errors in browsers
- Record a per-chunk CRC32 table for re-fill verification and keep it readable after close
- Propagate cancellation to blocked readers via CloseWithError so drivers treat aborts like canceled requests
- Cover ordering, backpressure, idempotent resends and close/abort wake-ups with race-enabled tests
- Start the driver upload at session init over a sequential stream backed by the window, so client-to-server and server-to-storage transfers run concurrently
- Attach client-provided hashes to the stream so drivers can attempt rapid upload before any chunk arrives, and absorb chunks racing pipeline completion idempotently
- Keep only metadata and chunk CRCs after a failed attempt: re-sending chunk 0 re-fills a fresh window, and content changes between attempts are rejected
- Resume receiving sessions only when client hashes prove the same file; failed_retriable sessions resume unconditionally
- Reclaim sessions with a sliding-TTL GC and sweep orphaned ring files at startup
- Cover the state machine with race-enabled tests over a stubbed storage layer
- Add multipart_enabled and multipart_chunk_size (MB) as public traffic settings
- Validate the chunk size on save (integer within 1-90) via the setting item hook
- Add /api/fs/multipart init/chunk/complete/status/abort endpoints with headers aligned with /fs/put
- Gate init behind the FsUp permission checks and reuse the client upload rate limiter for chunk uploads
- Drain the request body before answering chunk requests on every path, so browsers do not see early responses as network errors
- Start the multipart session GC when the router is initialized to reclaim ring files orphaned by a previous run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant