- 前端
- UI组件
- 后端
【免费下载链接】uppy
The next open source file uploader for web browsers :dog:
本指南以
@uppy/aws-s3插件的 CHANGELOG.md 为核心骨架,结合仓库源码(index.ts、S3Uploader.ts、s3-client)深入讲解:Uppy S3 插件在 v6 中的一次性重写如何将配置收敛为三种互斥签名模式,signRequest新增的key覆盖与headers返回能力解决了哪些真实问题,以及这些能力在源码层面是如何实现的。读完本文,你将掌握@uppy/aws-s3从接入、选型、配置到故障排查的完整链路,并能在自己的项目中直接复刻三种签名模式的最小可用配置。
一、为什么 v6 值得一次从零重写:从 changelog 看架构演进
@uppy/aws-s3的 CHANGELOG.md 中,最核心的里程碑是6.0.0:插件被从零重写(对应上游 PR #6345),其核心变化可以概括为三句话:
- 插件构建在一个独立的 S3 客户端之上,不再依赖散落的数十个回调选项;
- 配置被收敛为三种互斥的签名模式:
getCredentials、signRequest、companionEndpoint; - 任何 S3 兼容服务(如 R2、MinIO、DigitalOcean Spaces)在每种签名模式下都能工作——包括客户端签名,此前客户端签名硬编码了
*.amazonaws.com,只能面向 AWS 本体。
这一重写带来了清晰的选项迁移清单,对升级用户至关重要:
移除的选项:endpoint、headers、cookiesRule、getTemporarySecurityCredentials、getUploadParameters、signPart、createMultipartUpload、listParts、abortMultipartUpload、completeMultipartUpload、uploadPartBytes、retryDelays。
新增的选项:s3Endpoint、region、getCredentials、signRequest、companionEndpoint、generateObjectKey。
不变的选项:shouldUseMultipart、getChunkSize、allowedMetaFields、limit。
从源码看,这种"少即是多"的设计直接体现在插件初始化逻辑中。在 index.ts 的#initS3Client()里,插件按照companionEndpoint→getCredentials→signRequest的优先级分支,分别实例化两种底层客户端:
companionEndpoint模式 →S3Companion(CompanionS3.ts),通过与 Companion 服务端交互完成签名与上传;getCredentials与signRequest模式 →S3mini(S3mini.ts),一个浏览器端可用的 S3 兼容客户端(源自 good-lly/s3mini,MIT 许可,Uppy 做了适配改造)。
如果三种选项一个都没传,插件会直接抛出TypeError:One of options 'companionEndpoint', 'signRequest', or 'getCredentials' is required。这种"互斥但必选其一"的类型设计(在 index.ts 中用 TypeScript 联合类型表达)让配置错误在编译期/实例化期就能暴露,而不是等到上传时才发现签名方式缺失。
二、三种签名模式逐一拆解
1.getCredentials:客户端 SigV4 签名(临时凭证)
这是"完全自给自足"的模式:不需要 Companion,也不需要自己的签名服务端,只要提供一个能返回临时安全凭证的函数即可。插件使用 SigV4 在浏览器端直接对请求签名。
配置要点(见 index.ts):
s3Endpoint(必填 string):S3 兼容服务的端点,例如https://s3.us-east-1.amazonaws.com/my-bucket或https://play.min.io/my-bucket;region(可选 string):AWS 区域,缺省时回退到getCredentials响应中的 region,再不行用'auto'(见 S3mini.ts);getCredentials(必填函数):返回{ credentials: { accessKeyId, secretAccessKey, sessionToken, expiration? }, region },通常由你的后端调用 STS(Security Token Service)换取。
凭证的获取与缓存逻辑在 S3mini.ts 的_getCachedCredentials()中实现:凭证会被缓存复用,并且缓存的是Promise 本身,保证并发签名请求共享同一次凭证拉取;请求结束后清空 Promise 缓存,允许下次重新获取。
更关键的是过期自动续期:当 S3 返回ExpiredToken或InvalidAccessKeyId错误码时,客户端会清空凭证缓存并用新凭证重试一次(见 S3mini.ts),避免临时凭证在长传过程中过期导致上传失败。
最小可用示例:
import Uppy from '@uppy/core' import AwsS3 from '@uppy/aws-s3' const uppy = new Uppy() uppy.use(AwsS3, { s3Endpoint: 'https://s3.us-east-1.amazonaws.com/my-bucket', region: 'us-east-1', getCredentials: async () => { const resp = await fetch('/api/s3/credentials') // 后端用 STS 签发 return resp.json() // { credentials, region } }, })2.signRequest:自带签名器(客户端或服务端皆可)
当你的后端已有签名能力(比如用 AWS SDK 的预签名器,或自定义签名算法)时,用signRequest模式最灵活。它不要求s3Endpoint,只要求一个签名函数:
uppy.use(AwsS3, { signRequest: async ({ method, key, uploadId, partNumber }) => { const resp = await fetch('/api/s3/sign', { method: 'POST', body: JSON.stringify({ method, key, uploadId, partNumber }), }) return resp.json() // { url } 或 { url, key } 或 { url, headers } }, })签名函数的输入输出类型定义在 types.ts:
- 输入是
PresignableRequest联合类型,覆盖了 S3 的全部操作:PUT(单文件上传)、POST(创建分片上传 / 完成分片)、GET(列出已传分片)、DELETE(删除对象 / 中止分片上传),带uploadId/partNumber的分片操作会附带对应参数; - 输出是
PresignedResponse:url(必填)+key?(可选的对象键覆盖)+headers?(可选的随请求发送的签名头)。
signRequest会被调用多次(创建上传、传每个分片、完成上传……),因此适合把签名逻辑完全放在服务端、客户端不持有任何密钥的架构。这是"不带 Companion、用自己的后端签名"的主流方案。
3.companionEndpoint:Companion 签名(延续经典用法)
如果你已经在跑 Uppy Companion 服务端,这是零改动接入的方式,也支持远程文件(网盘等 Provider 文件)的服务端转发上传:
uppy.use(AwsS3, { limit: 2, timeout: 1000 * 60, // 1 分钟 companionEndpoint: 'https://companion.myapp.com/', })从 CompanionS3.ts 可以看到,该模式下的客户端会与 Companion 的/s3/*系列端点交互:
POST /s3/params:获取单文件 PUT 上传的预签名 URL 与表单字段({ url, fields }),随后以 multipart/form-data 提交文件;POST /s3/multipart:创建分片上传,返回{ key, uploadId };GET /s3/multipart/{uploadId}/{partNumber}:获取上传分片的预签名 URL;POST /s3/multipart/{uploadId}/complete:完成分片上传;GET /s3/multipart/{uploadId}:列出已上传分片(断点续传用);DELETE /s3/multipart/{uploadId}:中止分片上传。
值得注意的是 Companion 模式下对象键由服务端生成:在 index.ts 的#generateKey()中,若处于companionEndpoint模式,直接返回file.name(最终键由 Companion 决定),此时传入的generateObjectKey选项会被忽略。
三、v6.1.0 新能力:signRequest返回{ url, key }的对象键覆盖
这是 changelog 6.1.0 引入的一个重要语义修正(对应 issue #6496)。在此之前,当签名服务端把对象存在与 Uppy 提议不同的键下(比如加了一个目录前缀、或使用服务端生成的随机文件名)时,upload-success事件报告的仍是客户端生成的旧键,前后不一致。
6.1.0 的行为:在创建上传的单文件PUT、分片创建请求中,签名器返回{ url, key },Uppy 就会在后续整个上传流程中使用这个key,并在upload-success事件中如实上报它。
需要特别注意的两个边界(changelog 原文明确说明):
key是可选的——只返回{ url }的签名器行为与以前完全一致;- 携带
uploadId的请求(如分片上传、列分片、完成上传)必须按收到的key来签名,这些请求返回的key会被忽略(因为键在创建阶段已经确定)。
源码佐证:在 S3mini.ts 的request()中,签名完成后会计算resolvedKey:
// A blank key from the signer is not an override. const resolvedKey = signerKey?.trim() ? signerKey : requestedKey这个resolvedKey会一路穿透到putObject的返回值;在分片场景中,S3Uploader.ts 创建分片上传后把resolvedKey保存为#resolvedKey,后续所有分片上传、完成请求都使用它,最终随UploadResult({ location, key, uploadId? })上报。
6.2.0 的补充修正:空白key不算覆盖
6.2.0 进一步收紧了语义:如果签名器返回的key是空白字符串(如''或' '),则视为没有覆盖,仍然使用 Uppy 请求的键。这正是上面源码中signerKey?.trim()判断的作用——trim()后为空则回退到requestedKey。这防止了签名器在未实现 key 返回时误传空值导致的键丢失。
四、v6.2.0 新能力:signRequest返回headers携带签名头
6.2.0 的另一项 Minor Change:signRequest可以返回headers,随预签名请求一并发送——典型用途是发送签名过的Content-Disposition(控制下载时的文件名)。
从 types.ts 的注释可以提炼出三条硬性约束,接入时必须遵守:
- 必须也在桶的 CORS
AllowedHeaders中声明:这些头部参与了 SigV4 签名(属于X-Amz-SignedHeaders),浏览器跨域请求若未在 CORS 白名单中会直接失败; Content-Type特殊:若headers里带了Content-Type,它会替换插件内置的 Content-Type(文件自身的 MIME 类型);- 禁止浏览器禁用头:
Host、Content-Length、Date等浏览器无法由 JS 设置的头部绝不能出现在headers中。
请求侧的合并逻辑在 S3Client.ts 的xhr()中:先放内置Content-Type,再铺开签名器的headers,后者的Content-Type会覆盖前者(fetcher会折叠仅大小写不同的同名头,因此小写content-type同样生效)。
uppy.use(AwsS3, { signRequest: async ({ method, key, uploadId, partNumber }) => { const resp = await fetch('/api/s3/sign', { method: 'POST', body: JSON.stringify({ method, key, uploadId, partNumber }), }) return resp.json() // 可返回形如: // { url, key, headers: { 'Content-Disposition': 'attachment; filename="report.pdf"' } } }, })五、默认值、分块与并发控制:从源码看参数语义
在 v6 中,未在 changelog 变更列表里的四个"不变选项"承担了上传策略的配置职责,它们的默认值定义在 index.ts:
| 选项 | 默认值 | 语义 |
|---|---|---|
shouldUseMultipart | 文件大小 > 100MB 时使用分片 | 也可传true(总是分片)/false(总是单 PUT)/ 函数(按文件判定) |
limit | 6 | 并发上传文件数上限,6 对应浏览器 HTTP/1.1 每域 6 条连接的并发上限,避免在浏览器层排队 |
allowedMetaFields | true | 允许作为 S3 元数据上传的字段;传null/false或数组可精确控制 |
getChunkSize | 见下 | 自定义分片大小函数 |
分片相关的两个硬性常量在 S3Uploader.ts:
MIN_CHUNK_SIZE = 5MB:S3 对除最后一片外的分片有 5 MiB 最小限制,因此小于 5MB 的文件自然退化为单片上传;MAX_PARTS = 10000:S3 允许的最大分片数。默认分片大小按Math.ceil(fileSize / MAX_PARTS)计算(保证不超上限),若自定义getChunkSize导致分片数超限,会自动放大分片到刚好不超过 10000 片。
generateObjectKey(默认crypto.randomUUID()-文件名)只在非 Companion 模式下生效,见上文第三节末尾。
这些默认值共同决定了上传的"外形":大文件自动走分片、小文件走单 PUT,并发受limit控制,元数据受allowedMetaFields过滤(见 index.ts)。
六、上传主流程与可靠性机制:单 PUT / 分片 / 断点续传
单文件 PUT 与分片上传的完整链路
S3Uploader.ts 是每个文件上传状态的载体。start()根据shouldUseMultipart分流:
- 单 PUT(L253-L270):
putObject一次完成,返回location与key; - 分片上传(L272-L351):
createMultipartUpload→ 逐片uploadPart(每片完成后触发s3-multipart:part-uploaded事件,载荷为{ PartNumber, ETag })→completeMultipartUpload(发送<CompleteMultipartUpload>XML,包含每片的 PartNumber/ETag)→ 上报{ location, key, uploadId }。
成功时插件触发upload-success,body 为{ location, key }(见 index.ts),AwsBody类型即{ location, key }。
断点续传与 Golden Retriever 集成
分片创建后,插件会把{ uploadId, key }持久化到文件状态file.s3Multipart(S3Uploader.ts)。Golden Retriever 插件会在页面刷新后恢复该状态;恢复时 resume 逻辑 先listParts查询 S3 上已上传的分片,把已完成的 ETag 同步回本地,再只补传缺失分片。上传成功后会清空s3Multipart状态。
网络容错与重试
底层请求统一走 S3Client.ts 的fetcher:
- 自动重试 3 次,重试策略为:5xx 服务端错误与 429 限流会重试;4xx 客户端错误(除 429)不重试(L102-L116);
- 离线自动挂起:
waitForOnline()检测到navigator.onLine === false时挂起请求,待online事件触发后自动续传(L25-L65),并配合 AbortSignal 支持取消; - 上传失败不中止 S3 分片:S3Uploader.ts 的
#onError刻意不在 S3 侧 abort,以便用户重试时从断点续传;只有用户主动取消(abort(),默认abortInS3: true)才会调用abortMultipartUpload清理 S3 上的孤儿分片。若想保留分片以便手动清理或稍后恢复,可传{ abortInS3: false }。
远程文件上传
对于来自 Provider(如 Google Drive)的远程文件,index.ts 通过uploadRemoteFile走 Companion 的服务端 S3 上传通道(body 中标记protocol: 's3-multipart'),上传期间会临时关闭resumableUploads能力标识(远程上传不支持浏览器端暂停恢复)。
七、事件、元数据与迁移检查清单
事件一览
upload-progress:{ uploadStarted, bytesUploaded, bytesTotal };s3-multipart:part-uploaded:(file, { PartNumber, ETag });upload-success:{ status: 200, body: { location, key }, uploadURL: location };upload-error/upload-start/file-removed:标准 Uppy 语义。
从 v5 迁移到 v6 的检查清单
- 对照上文"移除的选项"列表,删除
endpoint、getUploadParameters、signPart等旧回调,改为三种签名模式之一; - 原先使用
getTemporarySecurityCredentials的,改用getCredentials+s3Endpoint; - 原先用
headers选项统一附加请求头的,改为在signRequest返回值中提供headers(并同步在桶的 CORS 中放行); - 验证签名服务端是否返回
{ url, key }:若返回,确保键语义一致;若返回空白 key,插件会回退到请求键(6.2.0+); - 分片相关能力(
createMultipartUpload/listParts等自定义回调)已内置于独立的 S3 客户端,无需再自行实现。
八、测试与验证:仓库如何保障三种模式
@uppy/aws-s3的测试同样围绕"三种模式 + 兼容服务"展开,可作自查参考:
- index.test.ts:插件级集成测试,覆盖签名模式初始化、事件发射、元数据过滤等;
- minio.test.ts + compose.minio.yaml:用 Docker 起一个 MinIO 实例,对真实 S3 兼容服务跑客户端层测试(单 PUT、分片、列分片、完成/中止、断点续传),这正是 changelog 强调"任何 S3 兼容服务可用"的落地验证;
- test-utils/browser-crypto.ts:为测试环境提供
crypto.randomUUID等浏览器 API 垫片。
如果你打算在本地验证接入,可以参照仓库的测试思路:用docker compose -f packages/@uppy/aws-s3/test/s3-client/compose.minio.yaml up起 MinIO,再把s3Endpoint指向它、用getCredentials返回 MinIO 的临时凭证,即可在非 AWS 环境下完整跑通三种签名模式。
总结
@uppy/aws-s3v6 的重写本质上是把"签名方式"从十几个零散回调中提炼为三种互斥模式,同时让浏览器端拥有了一个独立的 S3 兼容客户端。{ url, key }的对象键覆盖修复了"服务端改名但客户端上报旧键"的语义漏洞,headers返回则解锁了Content-Disposition等签名头的场景。配合默认 100MB 分片阈值、limit: 6并发、5MB 最小分片与 10000 片上限、自动重试/离线续传/断点恢复等机制,这一插件可以作为几乎所有 S3 兼容对象存储的前端上传方案。更多细节可继续查阅仓库中的 README.md 与 源码目录。
- 前端
- UI组件
- 后端
【免费下载链接】uppy
The next open source file uploader for web browsers :dog:
相关推荐
Uppy + AWS S3 PHP 示例:基于预签名 URL 的 PHP 服务端签名上传方案
Uppy + AWS S3 PHP 示例:基于预签名 URL 的 PHP 服务端签名上传方案 导读 本文围绕仓库 examples/aws php https:
前端UI组件后端N_m3u8DL-RE 源码编译:3 条命令拿到任意平台的可执行文件
N_m3u8DL RE 源码编译:3 条命令拿到任意平台的可执行文件 N_m3u8DL RE 源码编译就是为这个场景准备的:官方 Release 版本落后于代码
CLI音视频Hive AWS S3 Tool 深度指南:基于 SigV4 签名的对象存储 MCP 工具集
Hive AWS S3 Tool 深度指南:基于 SigV4 签名的对象存储 MCP 工具集 本文以 Hive(Multi Agent Harness for
人工智能AI Agent多智能体MCP 服务工具调用浏览器控制
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考