3338 字
17 分钟

OSS Hub 后端实现:多云适配、直传与数据对账

OSS Hub 后端实现:多云适配、直传与数据对账#

OSS Hub 后端不仅是一个 CRUD API。

它需要同时处理用户身份、云存储权限、加密配置、对象列表、预签名 URL、分片上传会话、文件索引、标签、Bucket 配置和远端数据对账。

这篇文章按照一条文件从“用户选择”到“对象落盘并进入索引”的路径,分析后端如何组织这些能力。

后端目录#

后端采用 Fastify + TypeScript,并按业务模块组织:

backend/src/
├── app.ts
├── server.ts
├── config/
├── plugins/
│ ├── auth.ts
│ ├── multipart.ts
│ ├── prisma.ts
│ └── static.ts
├── modules/
│ ├── auth/
│ ├── cloud-storage/
│ ├── files/
│ ├── storage/
│ ├── tags/
│ └── system-settings/
├── openapi/
└── utils/

app.ts 负责创建 Fastify 实例、注册插件和路由、初始化平台权限与默认配置,并统一处理 Zod、Prisma、业务错误和对象存储上游错误。

server.ts 只负责监听端口。这个拆分让测试可以直接调用 buildApp(),不必真正占用网络端口。

认证模式#

后端通过 AUTH_MODE 支持三种模式:

  • sso:校验外部 SSO 签发的 JWT。
  • local:校验 OSS Hub 自己签发的 JWT。
  • both:两条链路同时存在。

请求进入受保护接口时,认证插件会:

  1. Authorization: Bearer <token> 读取 token。
  2. 根据 token 和认证模式完成验签。
  3. 查找或同步本地用户。
  4. 检查用户状态是否为 ACTIVE
  5. 读取数据库中的有效权限。
  6. 把用户信息挂到 request.user

外部 token 中的权限不会直接替代数据库权限。SSO 身份负责证明“你是谁”,本地数据库负责保存 OSS Hub 内部授权状态。

点号层级权限#

权限判断支持点号层级的管理员继承。

例如接口要求:

storage.media.read

以下权限都可以通过:

admin
oss-hub.admin
storage.media.admin
storage.media.read

这里的关键不是字符串前缀匹配,而是明确的 .admin 层级提升。普通 storage.media.other 不会误获得读取权限。

每条云存储创建后,系统会确保存在:

storage.<storageKey>.admin
storage.<storageKey>.read

文件路由先解析当前 cloudStorageId,再检查用户是否拥有对应存储的读取或管理权限。

云存储配置加密#

云存储配置中最敏感的是 AccessKey ID、AccessKey Secret、Endpoint 和其它 SDK 参数。

数据库中的 CloudStorage 不直接保存明文凭据,而是保存 encryptedConfig。后端使用独立的 CONFIG_ENCRYPTION_KEY 加密和解密配置。

编辑云存储时,如果用户没有填写新的密钥,服务端会保留原有密文,而不是要求前端回传旧密钥。

这条边界很重要:

  • API 响应只告诉前端“是否已配置”。
  • 前端不会收到旧 AccessKey Secret。
  • 日志和错误响应不得输出解密后的配置。
  • 配置加密密钥投入使用后不能随意更换。

多云适配层#

项目没有在文件业务代码中到处判断 if aliyunif s3,而是定义统一的 StorageProvider 接口。

接口覆盖主链路需要的能力:

  • 生成公共对象 URL。
  • 获取和设置 Object 标签。
  • 写入对象。
  • 生成预签名 PUT URL。
  • 生成预签名 GET URL。
  • 创建、签发分片、完成和取消分片上传。
  • headObject、读取对象、删除对象。
  • 按前缀和游标列出对象。
  • 读取和更新生命周期规则。
  • 读取和更新 CORS。

然后提供两套实现:

  • AliyunOssProvider:基于 ali-oss
  • S3Provider:基于 AWS SDK v3。
classDiagram class StorageProvider { +listObjects() +signedPutObjectUrl() +signedGetObjectUrl() +createMultipartUpload() +signedUploadPartUrl() +completeMultipartUpload() +headObject() +deleteObject() +getObjectTags() +putObjectTags() } StorageProvider <|.. AliyunOssProvider StorageProvider <|.. S3Provider FileService --> StorageProvider SyncService --> StorageProvider

云服务商类型和 SDK 协议被分开保存。例如某个服务商可能显示为腾讯云,但实际通过 S3 兼容协议接入。

为什么保留供应商扩展能力#

多云兼容不等于所有功能都必须取交集。

文件浏览、上传、下载、删除、Object 标签和生命周期可以通过统一接口实现;阿里云 OSS 图片处理参数和防盗链则属于供应商扩展能力。

后端会根据当前 sdkProtocol 判断能力是否可用,前端路由也会隐藏或阻止不兼容的管理页面。

这种做法比伪造一个“所有供应商都支持”的抽象更诚实。

实时文件浏览#

文件浏览接口不是简单查询 FileObject,而是先调用对象存储的列表接口:

prefix + delimiter + cursor + pageSize

对象存储返回两类结果:

  • prefixes:当前层级的逻辑文件夹。
  • objects:当前层级的对象。

后端再批量查询这些 ObjectKey 对应的 FileObject,把远端结果合并成统一响应:

远端对象 + 数据库索引 => 文件浏览结果

因此文件页面可以同时展示已登记文件和未登记对象。

目录分页继续使用对象存储返回的游标,而不是数据库页码。这样大 Bucket 不需要一次性扫描全部对象。

数据库搜索#

搜索接口主要查询 FileObject

  • 当前云存储。
  • 未软删除。
  • 可选路径前缀。
  • 文件名、原文件名或 ObjectKey 关键字。
  • 系统标签组合。
  • 创建时间排序和分页。

如果系统设置开启“搜索时检查远端对象”,后端还会对当前结果执行 headObject

  • 远端不存在,记录 MISSING_OBJECT
  • 大小或 ETag 变化,记录 OBJECT_CHANGED
  • 状态正常,刷新 storageSeenAt

这样搜索页既保持数据库查询速度,又能逐步暴露数据偏差。

小文件预签名直传#

小文件上传分为三个阶段。

1. 创建上传会话#

前端先把文件元信息提交给后端:

  • 当前路径。
  • 原始文件名。
  • MIME、大小、ACL、存储类型。
  • 是否跳过数据库登记。
  • Object 标签。

后端完成权限、路径、覆盖策略和命名规则检查,生成最终 ObjectKey,并创建 UploadSession

2. 浏览器直传#

后端通过对应的 StorageProvider 生成预签名 PUT URL 和必须携带的请求头。

前端使用 XMLHttpRequest 把文件直接上传到对象存储,以便获取上传进度。

sequenceDiagram participant F as Vue 前端 participant B as Fastify 后端 participant O as 对象存储 F->>B: 创建直传会话(元信息) B->>B: 鉴权、生成 ObjectKey、写 UploadSession B-->>F: uploadUrl + headers + sessionId F->>O: PUT 文件内容 O-->>F: 200 + ETag F->>B: complete(sessionId) B->>O: HEAD 校验对象 B->>B: 写 FileObject / 标签 / 审计 B-->>F: 上传完成

3. 完成登记#

浏览器上传成功后调用完成接口。后端通过 headObject 验证远端对象,再更新上传会话并创建 FileObject

如果选择“不登记上传”,对象仍会写入 Bucket,但不会进入数据库搜索和系统标签体系。

大文件分片上传#

前端默认在文件达到 100 MB 时切换到分片上传。默认分片大小也是 100 MB,并行数为 3;分片大小和并行数可以保存在浏览器本地设置中调整。

后端分片链路包含:

  1. 创建远端 multipart upload,获得 uploadId
  2. 创建 UploadSession,保存 ObjectKey、上传参数和会话状态。
  3. 按窗口签发一批分片 URL,而不是一次返回所有 URL。
  4. 前端切片并行上传,读取每个响应的 ETag。
  5. 前端提交分片编号和 ETag。
  6. 后端调用对象存储完成合并。
  7. headObject 校验后写入文件索引。

按窗口签发 URL 可以减少超大文件的响应体,也降低大量预签名 URL 在真正使用前就过期的概率。

前端会把分片上传状态保存到 localStorage。用户重新选择同一个文件时,可以复用尚未完成的会话,跳过已经上传的分片。

分片上传还处理了:

  • 单分片最多重试 3 次。
  • 用户主动取消后调用 abort 接口。
  • 过期或失败会话的清理。
  • 上传进度按所有分片已传字节合并计算。
  • S3 与阿里云返回 ETag 头大小写差异。

OSS Hub 分片上传队列(待补图)

文件命名与覆盖策略#

云存储可以配置是否重命名上传文件,以及命名模板。

模板可以组合时间戳、UUID 等变量。后端生成 ObjectKey 时统一执行:

  • 路径标准化。
  • 文件名清理。
  • 重命名规则。
  • 扩展名保留。
  • 是否允许覆盖检查。

因此前端不能通过手写路径绕过命名或覆盖策略。

文件预览与下载#

不同文件类型使用不同策略:

  • 图片和视频:优先返回短期预签名 URL,由浏览器直接加载。
  • TXT、JSON、Markdown 和配置文件:后端限制大小后读取内容,返回只读文本。
  • 普通下载:返回短期预签名 GET URL。
  • 批量下载:后端读取多个对象并实时生成压缩流。

阿里云图片还可以生成带 x-oss-process 参数的处理 URL,用于缩放、裁剪、质量和格式调整。

两套标签系统#

系统标签保存在数据库中:

TagNamespace -> Tag -> FileTag -> FileObject

它支持全局范围、云存储范围、命名空间、软删除和恢复,适合搜索与业务分类。

Object 标签通过对象存储 API 直接读写,适合云端生命周期和策略。OSS Hub 只额外保存常用的 ObjectTagPreset,方便用户快速套用键值组合。

如果把两者混成一套,会导致本地搜索语义和云端策略语义互相牵制。

索引同步与数据对账#

同步任务不能假设 Bucket 很小,因此采用可续跑的分步扫描。

StorageSyncRun 保存:

  • 当前阶段。
  • 对象存储游标。
  • 数据库游标。
  • 已扫描数量、导入数量和问题数量。
  • 任务状态、错误和完成时间。

一次同步分为两个主要阶段:

阶段一:扫描远端对象#

按前缀和游标读取对象:

  • 数据库没有记录:按规则导入,或作为未登记对象处理。
  • 数据库有记录且大小、ETag 一致:更新 storageSeenAt
  • 数据库有记录但内容变化:创建或更新 OBJECT_CHANGED 问题。

阶段二:扫描数据库缺失#

查找本次同步开始前存在、但扫描过程中没有被看到的文件:

  • 创建或更新 MISSING_OBJECT 问题。
  • 解决已经恢复的旧问题。
  • 保留问题详情,等待管理员重新检查或清理。
flowchart TD A[启动同步] --> B[分页扫描远端对象] B --> C{数据库有记录?} C -->|否| D[导入或标记未登记] C -->|是| E{大小/ETag 一致?} E -->|是| F[更新 storageSeenAt] E -->|否| G[记录 OBJECT_CHANGED] B --> H[远端扫描完成] H --> I[分页扫描未被看到的数据库记录] I --> J[记录 MISSING_OBJECT] J --> K[完成同步]

管理员可以对问题执行:

  • 重新检查远端状态。
  • 导入未登记对象。
  • 清理确认缺失的数据库记录。
  • 对失败文件操作执行重试。

OSS Hub 数据对账问题(待补图)

删除与状态恢复#

删除文件不是简单地先删数据库再删对象。

系统会记录文件操作状态:

  • 标记操作进行中。
  • 调用对象存储删除。
  • 成功后设置 deletedAt 软删除。
  • 失败时保存错误信息,允许后续重试。

这样即使远端请求超时或应用重启,也能在管理页看到未完成操作,而不是留下不可解释的半状态。

Bucket 管理#

后端还通过存储适配层提供 Bucket 级能力:

  • 生命周期规则。
  • CORS 规则。
  • 阿里云 OSS Referer 防盗链。

生命周期与 CORS 是通用能力,但字段差异仍由 provider 负责转换。防盗链则明确限制为阿里云 OSS 能力。

错误处理#

app.ts 把错误分为几类:

  • 业务错误:统一的 HTTP 状态和错误码。
  • Zod 校验错误:返回字段级校验信息。
  • Prisma 已知错误:转换为重复、外键、记录不存在或数据库繁忙等可理解响应。
  • 对象存储上游错误:只返回状态、错误码和请求 ID,不暴露密钥与内部堆栈。
  • 未知错误:记录服务端日志,向前端返回通用错误。

对外错误格式稳定,前端才能统一显示 Toast、页面状态和重试操作。

后端实现总结#

OSS Hub 后端的核心不是调用多少个 SDK 方法,而是把这些方法组织成可恢复的业务流程:

  • 认证决定用户身份。
  • 数据库权限决定用户能访问哪个 Bucket。
  • provider 隔离不同对象存储协议。
  • 上传会话保证直传和分片流程可追踪。
  • FileObject 提供搜索与标签索引。
  • 同步任务负责校验索引和远端事实。
  • 审计与操作状态负责解释失败和恢复路径。

有了这层后端,前端才可以把对象存储能力整理成真正可用的文件工作台。

OSS Hub 后端实现:多云适配、直传与数据对账
https://march7th.online/blog/posts/0040-oss-hub-后端实现多云适配直传与数据对账/
作者
Yiguo
发布于
2026-07-28
许可协议
CC BY-NC-SA 4.0
最后更新于 2026-07-28,距今已过 3 天

部分内容可能已过时

所属合集

OSS Hub 对象存储管理平台

记录 OSS Hub 从项目定位、多云对象存储适配、浏览器直传、索引对账,到 Vue 管理后台、部署和工程化收尾的完整实现。

查看完整合集

目录