☰
Uppy `@uppy/aws-s3` v6 重构深度解析:三种签名模式、对象键覆盖与自定义签名头实战指南
2026/9/30 2:29:31 网站建设 项目流程
  • 前端
  • UI组件
  • 后端

【免费下载链接】uppy

The next open source file uploader for web browsers :dog:

项目地址:https://gitcode.com/gh_mirrors/up/uppy
点击查看免费下载

本指南以@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),其核心变化可以概括为三句话:

  1. 插件构建在一个独立的 S3 客户端之上,不再依赖散落的数十个回调选项;
  2. 配置被收敛为三种互斥的签名模式:getCredentials、signRequest、companionEndpoint;
  3. 任何 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 原文明确说明):

  1. key是可选的——只返回{ url }的签名器行为与以前完全一致;
  2. 携带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 的注释可以提炼出三条硬性约束,接入时必须遵守:

  1. 必须也在桶的 CORSAllowedHeaders中声明:这些头部参与了 SigV4 签名(属于X-Amz-SignedHeaders),浏览器跨域请求若未在 CORS 白名单中会直接失败;
  2. Content-Type特殊:若headers里带了Content-Type,它会替换插件内置的 Content-Type(文件自身的 MIME 类型);
  3. 禁止浏览器禁用头: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)/ 函数(按文件判定)
limit6并发上传文件数上限,6 对应浏览器 HTTP/1.1 每域 6 条连接的并发上限,避免在浏览器层排队
allowedMetaFieldstrue允许作为 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 的检查清单

  1. 对照上文"移除的选项"列表,删除endpoint、getUploadParameters、signPart等旧回调,改为三种签名模式之一;
  2. 原先使用getTemporarySecurityCredentials的,改用getCredentials+s3Endpoint;
  3. 原先用headers选项统一附加请求头的,改为在signRequest返回值中提供headers(并同步在桶的 CORS 中放行);
  4. 验证签名服务端是否返回{ url, key }:若返回,确保键语义一致;若返回空白 key,插件会回退到请求键(6.2.0+);
  5. 分片相关能力(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:

项目地址:https://gitcode.com/gh_mirrors/up/uppy
点击查看免费下载
上一篇:Seraphine 安装教程:10 分钟跑通英雄联盟战绩查询工具
下一篇:chromatic(BetterNCM 重写版)Chromium/V8 注入修改器安装与上手教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询