Skip to content

feat(drivers/s3): support direct multipart upload - #609

Open
Arielfoever wants to merge 1 commit into
OpenListTeam:mainfrom
Arielfoever:feat/s3
Open

feat(drivers/s3): support direct multipart upload#609
Arielfoever wants to merge 1 commit into
OpenListTeam:mainfrom
Arielfoever:feat/s3

Conversation

@Arielfoever

Copy link
Copy Markdown

Summary / 摘要

There is no user-visible behavior changes for uploading.

HTTP Direct for S3 is now upload by multipart unless DirectUploadMaxParts=1 while the default value is 10000. If S3 is standard, there will be nothing change.

  • 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

Testing / 测试

  • go test ./...
    no go test available
  • Manual test / 手动测试:
    test on Aliyun OSS and it uploads in 50 parts as planned. No larger file tested.

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.
    / 我已在适用情况下请求相关维护者或代码所有者审查。
    No right to request review.

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 辅助内容。

@Arielfoever
Arielfoever force-pushed the feat/s3 branch 2 times, most recently from c0ab82b to afb0937 Compare July 26, 2026 15:30
@xrgzs xrgzs changed the title feat(s3): support direct multipart upload feat(drivers/s3): support direct multipart upload Jul 26, 2026
- Upload S3 direct multipart parts using presigned part URLs.

- Complete multipart uploads with collected ETags and abort on failures.

- Treat embedded CompleteMultipartUpload XML errors as upload failures.

- Build completion XML with DOM APIs instead of manual escaping.

@pikachuren pikachuren left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结论:方向正确,但有几处会影响大文件上传成功率的问题需要处理

整体设计合理:通过 upload_urls 是否存在来判断走 multipart 分支,对后端未返回该字段的旧行为完全无影响;失败时调用 abort_url 避免在对象存储侧留下计费中的未完成分片,这个考虑很到位;validateS3CompleteResponse 特意处理了 S3「HTTP 200 但响应体是 <Error>」这一经典陷阱,说明作者对 S3 协议细节是了解的。下面几点建议在合并前处理。

一、分片失败没有重试,大文件成功率会受影响(建议优先处理)

for (let i = 0; i < uploadInfo.upload_urls.length; i++) {
  ...
  const etag = await uploadS3Part(...)

任何一个分片因网络抖动失败,整个上传就直接抛错并 abort,用户只能从头再来。multipart 的主要价值恰恰在于「失败只需重传单个分片」,目前的实现把这个优势丢掉了。一个几十 GB 的文件在传输后期失败一次,代价非常大。建议对单个分片加入有限次数的指数退避重试。

二、ETag 的引号处理需要确认

const etag = xhr.getResponseHeader("ETag")
...
etagNode.textContent = etag

S3 返回的 ETag 通常带双引号(如 "d41d8cd98f00b204e9800998ecf8427e"),而这里原样写入了 CompleteMultipartUpload 的 XML。多数实现能容忍,但部分 S3 兼容存储(MinIO、Ceph RGW 等)对此校验较严,会返回 InvalidPart。建议明确处理,比如去掉首尾引号后再写入,并在注释里说明依据。

三、getResponseHeader("ETag") 依赖 CORS 配置,需要给出明确的错误引导

浏览器只能读取 Access-Control-Expose-Headers 中列出的响应头。如果存储桶的 CORS 没有暴露 ETag,这里会稳定拿到 null,然后抛出 Upload part N did not return an ETag。这个报错对用户来说指向性很差——实际是服务端配置问题,却看起来像上传失败。

建议在这个错误信息里直接提示可能的原因(需在存储桶 CORS 中暴露 ETag 响应头),并在文档中补充说明。否则这会成为一个高频且难以自助排查的支持问题。

四、abort 是 fire-and-forget

void fetch(uploadInfo.abort_url, { method: "DELETE" })

不等待也不处理失败。abort 本身失败(网络断开、URL 过期)时用户和日志都无感知,而未清理的分片会持续产生存储费用。至少建议 catch 后打一条日志;考虑到这里紧接着就要 throw error,也可以 await 后再抛,让清理有确定结果。

五、两处小问题

  • uploadURLmethod 在 multipart 早退分支之前就被计算了,走 multipart 时这两个变量是无用的。建议把 if (uploadInfo.upload_urls) 判断上移到取值之前,逻辑更清晰。
  • 分片是完全串行上传的。既然后端一次性下发了全部 upload_urls,并发上传能显著提升吞吐。仓库里已有 src/utils/async_pool.ts 可以复用。若刻意选择串行(例如为了让进度条单调递增、或控制内存占用),建议加注释说明,否则容易被后来者误认为是遗漏。

另外这个 PR 的 CI 没有运行(分支为 feat/s3,而 PR 构建工作流只在目标为 main 的 PR 上触发),合并前请确认已通过实际的 S3 兼容存储做过端到端验证,最好覆盖一次超过分片阈值的大文件。

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.

2 participants