Strapi 如何开发自定义 Upload Provider 对接自有对象存储(upload/delete/getSignedUrl)?
【免费下载链接】strapi🚀 Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi
如果你有自有的对象存储,希望 Strapi 中上传的媒体文件不再写到本地uploads/目录,而是落到你的存储里,就需要开发一个自定义 Upload Provider。完成本文任务后,Strapi 上传的媒体会写入你的对象存储,删除媒体时会同步删除对象,私有存储还能通过签名 URL 对外提供访问。本文依据仓库内的 Provider 契约与官方加载逻辑:契约见 Provider 文档,加载逻辑在 register.ts,官方 S3 provider 与 local provider 可作为参考实现。
前提:一个可用的 Strapi 项目(upload 插件已启用),以及你自己的对象存储服务及其 SDK 或 HTTP API。
Provider 契约:要实现哪些方法
官方文档的定义是:Provider 是一个包,它导出一个带init函数的对象,Strapi 启动时调用init(options),返回的对象就是 provider 实例。各方法的职责(引自 00-providers.md):
| 方法 | 要求 | 职责 |
|---|---|---|
upload(file) | 与uploadStream至少实现一个 | 把文件上传到 provider |
uploadStream(file) | 与upload至少实现一个(推荐) | 把流上传到 provider |
delete(file) | 必须 | 从 provider 删除文件 |
isPrivate() | 可选 | 返回存储是否私有,默认false;为真时改用getSignedUrl获取文件 URL |
getSignedUrl(file) | 可选 | 存储需要鉴权时,返回访问文件的签名 URL |
Strapi 在启动阶段校验实例(register.ts),对应报错如下:
- 无法解析 provider 模块:
Could not load upload provider "xxx". - 缺少
delete:The upload provider "xxx" doesn't implement the delete method. upload与uploadStream都缺失:The upload provider "xxx" doesn't implement the uploadStream nor the upload method.- 只缺
uploadStream(不致命,仅警告):The upload provider "xxx" doesn't implement the uploadStream function. Strapi will fallback on the upload method. Some performance issues may occur.
前三条决定项目能否启动,最后一条影响运行期性能,所以建议把uploadStream也实现掉。
Strapi 如何找到并调用你的 provider
配置写在./config/plugins.js:
const pluginConfig = { upload: { // provider 名称,会被转成小写后使用 provider: 'my-oss', // 整个对象会作为参数传给 provider 的 init(options) providerOptions: { // 你自己的对象存储参数,例如 bucket、endpoint、凭据 }, }, };加载逻辑(register.ts):
- 读取
plugin::upload配置,取provider名称并转小写; - 先尝试解析 npm 包
@strapi/provider-upload-<名称>;若未安装(MODULE_NOT_FOUND),则直接require你配置的这个名字——也就是说 provider 既可以是安装的包,也可以是项目内可被引用的模块; - 调用
provider.init(providerOptions)得到实例。
之后 Strapi 对实例上的每个函数做包装(register.ts):调用同名方法时会把配置里actionOptions.<方法名>作为第二个参数传入。也就是说你可以在 upload 配置里声明actionOptions: { upload: {...} },按方法名给不同方法传参,方法签名写成(file, options)即可。
实例还继承baseProvider的几个默认实现:isPrivate()默认返回false;getSignedUrl(file)默认原样返回文件;checkFileSize(file, { sizeLimit })在文件超过 upload 配置中的sizeLimit时抛出PayloadTooLargeError。
Provider 收到的 File 对象
file是 Strapi 组装好的媒体记录,provider 实现中常用到的字段(定义见 S3 provider 的File,index.ts):
file.hash/file.ext:文件哈希与扩展名,官方 provider 都用它们拼对象 Key;file.stream(流式路径)或file.buffer(缓冲路径):文件内容;file.mime:MIME 类型,写入时作为 Content-Type;file.path:目录路径,S3 provider 会把它拼进 Key 前缀;file.url:provider 必须在这里写回文件的访问地址,Strapi 最终保存的媒体 URL 就是你设置的值。
对象 Key 的拼法参考两个官方实现:local provider 直接用${file.hash}${file.ext};S3 provider 按rootPath前缀 →file.path→hash+ext的顺序拼接(getFileKey)。
实现 uploadStream
local provider 的实现(upload-local/src/index.ts)展示了最小模式:消费file.stream写入存储,成功后写回file.url:
uploadStream(file: File): Promise<void> { if (!file.stream) { return Promise.reject(new Error('Missing file stream')); } return new Promise((resolve, reject) => { pipeline( file.stream, fs.createWriteStream(path.join(uploadPath, `${file.hash}${file.ext}`)), (err) => { if (err) return reject(err); file.url = `/uploads/${file.hash}${file.ext}`; resolve(); } ); }); }S3 provider 的写法(upload):请求体为file.stream || Buffer.from(file.buffer, 'binary'),ContentType 取file.mime;上传完成后把访问 URL 写回file.url,把ETag去掉引号后写入file.etag。你的对象存储返回的 URL 拼接方式可参考它的优先级:配置了baseUrl(CDN 或自定义域名)时优先用它,其次使用存储返回的地址,兜底用endpoint/bucket/key拼接。
实现 delete
delete(file)按同样的规则算出 Key,然后删除对象即可。S3 provider 用DeleteObjectCommand按Bucket + Key删除(delete)。官方文档明确要求 provider "should be able to upload files to a remote server and delete them"——即 Strapi 删除媒体时,你存储里的对象也要随之移除。
私有文件与 getSignedUrl
如果存储不允许匿名访问,让isPrivate()返回true。URL 签名流程在 file.ts 中:
- 从 provider 读取
isPrivate;若为false,或文件自身的provider字段与当前配置的 provider 名称不一致,直接返回原文件,不签名; - 否则调用
getSignedUrl(file),把返回结果里的url写进文件的url,并标记file.isUrlSigned = true;file.formats里的每个尺寸(响应式图片)也会逐个签名。
因此getSignedUrl必须返回带url字段的对象。S3 provider 的实现(getSignedUrl)用 presigner 对GetObjectCommand生成临时 URL,expiresIn取配置params.signedUrlExpires(默认15 * 60秒),返回{ url }。
自定义 provider 的最小骨架
综合以上契约,对接自有对象存储的 provider 骨架如下。// TODO行是需要替换为你自己的对象存储 SDK 调用的部分,其余结构可直接保留:
// 自定义对象存储 provider export default { init(options) { // options 就是 ./config/plugins.js 里的 providerOptions // 对象 Key:官方惯例为 hash + ext,需要目录前缀时可自行拼接 const buildKey = (file) => `${file.hash}${file.ext}`; return { // 推荐实现:流式写入,比缓冲路径性能好 uploadStream(file) { const key = buildKey(file); // TODO: 把 file.stream 以 key 写入你的对象存储 // TODO: 成功后把对象的访问 URL 写入 file.url }, // 可选:缓冲路径;只实现 uploadStream 时可以不写 upload upload(file) { // TODO: 把 file.buffer 以 buildKey(file) 写入你的对象存储,并写回 file.url }, // 必须实现 delete(file) { // TODO: 从你的对象存储删除 buildKey(file) 对应的对象 }, // 存储私有时返回 true,启用 URL 签名 isPrivate() { return true; }, // 仅当 isPrivate() 为 true 时被调用;必须返回 { url } async getSignedUrl(file) { // TODO: 用你的存储签名机制为对象生成临时访问 URL // 形如:return { url: await signObject(buildKey(file)) }; }, }; }, };把该模块放到 provider 名称能解析到的位置(安装为 npm 包,或项目中可require的模块路径),再在./config/plugins.js中按上文格式配置。
验证方式
Strapi 启动后逐项核对:
- 启动校验:provider 包解析不到、
delete缺失或upload/uploadStream都缺失时,启动会抛出第一节列出的对应错误;能正常启动说明基础契约满足。若看到uploadStream警告,说明流式实现还没补齐。 - 上传链路:在管理端或调用上传接口做一次媒体上传,返回的媒体
url应正是你的 provider 写入file.url的地址,你的对象存储里应出现 Key 为${hash}${ext}的新对象。 - 删除链路:删除该媒体后,你存储中对应的对象应不再存在。
- 签名 URL:
isPrivate()返回true、且媒体的provider与配置的 provider 名称一致时,API 返回的媒体url应是被替换后的签名 URL,isUrlSigned字段为true。
可选分支:对象存储兼容 S3 协议时
如果你的自有对象存储实现了 S3 协议,可以不写自定义 provider,直接使用官方@strapi/provider-upload-aws-s3,把s3Options的endpoint指向你的存储即可。源码中有几点限制需要注意:
params.Bucket必填,否则启动报错:Upload AWS S3 provider: \params` are required in the config object`;endpoint、credentials等要放进s3Options对象里,平铺在providerOptions根级别是已弃用的方式,会触发告警;- 当 endpoint 不是 AWS 时,
storageClass、非AES256的加密等 AWS 专属配置可能被你的存储忽略,Strapi 会发出告警(例如Storage class 'STANDARD_IA' is AWS S3-specific and may be ignored by your S3-compatible provider.)。
限制说明
- provider 名称会被
_.toLower转小写后再解析,配置名与实际包名注意保持一致; - URL 签名只对
provider字段等于当前配置 provider 名称的媒体生效,其它 provider 写入的历史媒体不会被签名; getSignedUrl只在isPrivate()返回true时才被调用;- 大小校验的
sizeLimit是 upload 配置的顶层参数,从providerOptions传入属于已弃用方式(local provider 会发出弃用告警,提示迁移到upload.config)。
参考文件
- Provider 契约文档:docs/docs/docs/01-core/upload/01-backend/00-providers.md
- Provider 加载与启动校验:packages/core/upload/server/src/register.ts
- 签名 URL 流程:packages/core/upload/server/src/services/file.ts
- 官方 S3 provider 实现:packages/providers/upload-aws-s3/src/index.ts
- 官方 local provider 实现:packages/providers/upload-local/src/index.ts
【免费下载链接】strapi🚀 Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考