news 2026/9/18 7:48:30

Epic Stack 图片存储架构:从 SQLite BLOB 迁移到 Tigris 对象存储的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Epic Stack 图片存储架构:从 SQLite BLOB 迁移到 Tigris 对象存储的完整实践

Epic Stack 图片存储架构:从 SQLite BLOB 迁移到 Tigris 对象存储的完整实践

【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack

Epic Stack 项目曾在 SQLite 中以 BLOB 形式直接存储上传图片,本篇文章以决策文档 docs/decisions/040-tigris-image-storage.md 为主线,完整还原这一架构决策的背景、迁移方案与落地实现。读完你将掌握:Tigris(S3 兼容对象存储)的完整配置方式、无 SDK 的 AWS SigV4 签名请求实现原理、SQLite 仅存元数据的混合存储模型,以及本地离线开发与测试的 Mock 机制。

一、为什么放弃在 SQLite 中存放图片二进制

在转向 Tigris 之前,Epic Stack 的图片处理遵循 docs/decisions/018-images.md 中的决策:将用户上传的图片以二进制 BLOB 形式直接存入 SQLite 数据库。当时的理由很充分——SQLite 官方甚至专门论证过从数据库读取小文件可能比文件系统更快,配合 LiteFS 还能免费获得多节点复制存储。

但该方案存在明确的硬伤,也正是 040 号决策文档开篇列出的四条核心痛点:

  1. 数据库膨胀:二进制数据显著增大 SQLite 文件体积,备份复杂度随之上升;
  2. 性能衰减:大块二进制数据会拖慢数据库整体读写性能;
  3. 备份变慢:包含二进制数据的 SQLite 备份更大、耗时更长;
  4. 缺乏 CDN 能力:SQLite 无法为图片分发提供任何边缘加速手段。

此外 018 号文档还补充了一个量级参考:SQLite + LiteFS 的组合经过测试可支撑到 10GB 规模,这对多数应用足够,但"足够"不是 Epic Stack 的追求。同时,旧的方案没有任何图片优化/压缩能力,客户端请求什么原图就返回什么原图(该问题随后由 041-image-optimization.md 引入的 openimg 按需优化解决)。

二、决策:切换到 Tigris 对象存储

2025-02-20,Epic Stack 正式通过 040-tigris-image-storage.md 决策(Status: accepted),核心结论如下:

  1. 将图片二进制数据从 SQLite 中移出,放入专用的对象存储;
  2. SQLite 只保留图片的元数据(引用关系、所有权等);
  3. 借助 Tigris 的 S3 兼容 API 完成图片的高效存取;
  4. 为大量图片上传的应用提供更好的扩展性。

决策中有一个非常关键的实现取舍:不引入任何 S3 SDK,而是自己用经过认证的 fetch 请求管理上传与下载。这让依赖面保持极轻,也正因为没有 SDK 抽象,底层签名逻辑完全透明可控。

积极影响

  • SQLite 体积显著缩小,备份效率提升;
  • 关注点分离更清晰(二进制数据 vs 关系数据);
  • 借助 Tigris 基础设施获得潜在更优的图片服务性能;
  • 对重图片应用更具扩展性;
  • 未来接入 CDN 更容易;
  • 数据库维护与备份流程简化;
  • Tigris 存储成本远低于 Fly volume 存储。

需要接受的代价

  • 新增外部服务依赖(但 Fly.io 原生集成,无需额外注册账号);
  • 需要管理 Tigris 配置;
  • 部署配置略微复杂;
  • 图片上传与检索逻辑复杂度上升。

三、配置 Tigris:环境变量与本地 Mock

3.1 必需的环境变量

根据 app/utils/env.server.ts 中的 Zod schema 校验,以下五个变量缺一不可(均为必填字符串,AWS_ENDPOINT_URL_S3还需通过z.string().url()的 URL 格式校验):

AWS_ACCESS_KEY_ID="mock-access-key" AWS_SECRET_ACCESS_KEY="mock-secret-key" AWS_REGION="auto" AWS_ENDPOINT_URL_S3="https://fly.storage.tigris.dev" BUCKET_NAME="mock-bucket"
  • AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY:Tigris 的访问凭据;
  • AWS_REGION:区域,Fly + Tigris 集成场景固定为auto
  • AWS_ENDPOINT_URL_S3:Tigris 的 S3 兼容端点,Fly 上默认为https://fly.storage.tigris.dev
  • BUCKET_NAME:对象存储桶名称。

在 Fly.io 上,创建 Epic Stack 项目时会自动为应用创建 storage 并注入这些变量;在本地开发时,这些值写入.env文件,配合 MSW(Mock Service Worker)Mock,整个开发过程可以完全离线运行

3.2 本地 Mock 的实现

Mock 逻辑位于 tests/mocks/tigris.ts,它拦截指向${AWS_ENDPOINT_URL_S3}/${BUCKET_NAME}/:key*的 PUT 与 GET 请求:

  • PUT:校验请求头必须包含Authorization(以AWS4-HMAC-SHA256开头)、X-Amz-DateX-Amz-Content-SHA256: UNSIGNED-PAYLOAD,且 Credential 中携带正确的 Access Key,校验通过后将请求体写落到tests/fixtures/uploaded目录;
  • GET:优先从tests/fixtures/images读取测试夹具图片,否则回落到tests/fixtures/uploaded,并返回带Cache-Control: public, max-age=31536000, immutable的响应。

这意味着即使没有真实 Tigris 账号,本地开发与端到端测试也能完整体验完整的上传 → 存储 → 读取链路。

四、混合存储模型:元数据在 SQLite,二进制在 Tigris

4.1 数据库 Schema

prisma/schema.prisma 中的UserImageNoteImage模型与决策文档描述一致,实际实现还比决策文档多了一个altText可选字段(用于图片的替代文本):

model UserImage { id String @id @default(cuid()) altText String? objectKey String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) userId String @@index([userId]) } model NoteImage { id String @id @default(cuid()) altText String? objectKey String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt note Note @relation(fields: [noteId], references: [id], onDelete: Cascade, onUpdate: Cascade) noteId String @@index([noteId]) }

两个模型的关键设计:

  • objectKey是核心桥梁字段:它指向对象在 Tigris 桶中的唯一路径,是 SQLite 与对象存储之间的唯一关联;
  • 级联删除onDelete: Cascade保证用户/笔记被删除时,对应的图片元数据记录一并清除(二进制对象需另行清理);
  • 外键索引@@index([userId])@@index([noteId])保证按所有者查询图片元数据的性能。

从源码结构看,图片本身不再以字节形式落库,数据库只扮演"引用登记表"的角色,这正是决策文档强调的"混合存储(hybrid approach)"。

五、核心实现:无 SDK 的 AWS SigV4 签名请求

决策文档明确"不使用 S3 SDK",全部实现集中在 app/utils/storage.server.ts,对 Node 内置crypto模块完成 AWS Signature Version 4(SigV4)手工签名。

5.1 对象 Key 的组织策略

上传函数使用cuid2生成唯一 ID,并结合时间戳与原始扩展名构造可读、可归类的 Key:

  • 用户头像:users/${userId}/profile-images/${timestamp}-${fileId}.${fileExtension}
  • 笔记图片:users/${userId}/notes/${noteId}/images/${timestamp}-${fileId}.${fileExtension}

这种用户/模块/时间戳-随机ID.扩展名的层级结构天然支持按前缀列出/清理对象。

5.2 签名流程:AWS SigV4 一步步拆解

getBaseSignedRequestInfo是签名核心,它完全手工实现了 SigV4 四步流程:

  1. 构造规范化请求(Canonical Request):按HTTP方法 / 资源路径 / 规范化查询串 / 规范化头 / 签名头列表 / 载荷哈希拼接。本项目使用UNSIGNED-PAYLOAD(对内容不做流式哈希,适用于流式上传);
  2. 构造待签名字符串(String to Sign)AWS4-HMAC-SHA256 + amzDate + credentialScope(dateStamp/region/s3/aws4_request) + sha256(canonicalRequest)
  3. 派生签名密钥(Signing Key):通过getSignatureKeyAWS4${secretKey}依次做 HMAC(date → region → service → aws4_request);
  4. 生成签名:对待签名字符串做最终 HMAC-SHA256,组装进Authorization头。

5.3 上传与下载两个方向的封装

上传(PUT)uploadToStorage接受File@mjackson/form-data-parserFileUpload,对FileUpload直接使用.stream()流式发送(决策与代码都在追求轻量),并在头部附带Content-TypeX-Amz-Meta-Upload-Date元数据;响应非 2xx 时记录状态码并抛出错误。

下载(GET)getSignedGetRequestInfo(key)复用同一签名基座生成带签名的 GET URL 与头信息,供代理层拉取对象。

5.4 上传在业务层的调用链

签名上传被两处业务逻辑复用:

  • 头像上传:app/utils/auth.server.ts 在用户认证/同步流程中调用uploadProfileImage(user.id, imageFile),并把返回的objectKey写入UserImage记录;
  • 笔记图片上传:app/routes/users/$username/notes/+shared/note-editor.server.tsx 在笔记表单处理时对每个带文件的图片字段调用uploadNoteImage(userId, noteId, file),将objectKey存入NoteImage
  • 头像设置页面 app/routes/settings/profile/photo.tsx 同样通过uploadProfileImage完成上传。

六、图片服务出口:本地代理 + 按需优化

决策文档提到"图片 URL 指向本地服务器,由本地服务器代理到 Tigris",这一层实现在 app/routes/resources/images.tsx:

  • loader 接收objectKey查询参数,调用getSignedGetRequestInfo生成带签名 URL,通过 openimg 的getImgResponsefetch方式回源拉取,并设置Cache-Control: public, max-age=31536000, immutable的强缓存;
  • 端点在服务图片的同时,还基于 openimg + sharp 提供whformatfit等按需变换参数,优化结果落盘到/data/images(生产环境)或tests/fixtures/openimg(测试环境)做文件缓存,见 docs/image-optimization.md;
  • 出于安全考虑,回源域名通过allowlistedOrigins白名单限制(仅允许当前应用域名与AWS_ENDPOINT_URL_S3)。

这套"签名直连 + 本地代理 + 按需优化"的组合,既避免了将私有存储桶直接暴露给浏览器,又为后续接入 CDN 留好了位置。

七、迁移与向后兼容

决策文档的实施清单明确包含"数据库迁移 + 既有图片手工迁移 + 提供迁移工具"三项工作:

  1. 设置 Tigris 配置;
  2. 修改图片上传处理器,将文件存入 Tigris;
  3. 更新图片检索路由,从 Tigris 提供服务;
  4. 迁移期间保持向后兼容(需要数据库迁移,以及既有图片的手工迁移);
  5. 为已有应用提供迁移工具。

对正在升级的存量应用,建议的迁移路径是:先把既有 SQLite 中的 BLOB 图片逐个导出,按新 Key 规则上传到 Tigris 桶,再执行 Prisma 迁移将 BLOB 列替换为objectKey引用,最后在验证图片可正常代理访问后释放数据库中的二进制数据。

八、小结:这套架构带来的工程启示

从 018-images.md 的"先存 SQLite",到 040-tigris-image-storage.md 的"迁往 Tigris",Epic Stack 的图片存储演进完整呈现了一次典型的架构升级路径:当数据形态(二进制 vs 关系型)与业务诉求(扩展性、CDN、成本)不匹配时,果断把关注点拆开。

最终形态值得直接借鉴:

  • 关系数据库只存引用:SQLite 表里只有一个objectKey字符串,体积小、备份快、查询快;
  • 对象存储只管二进制:Tigris 负责海量字节的存取与分发;
  • 无 SDK 的签名实现:约 180 行代码完成 SigV4 签名(app/utils/storage.server.ts),零第三方依赖;
  • 本地全离线可测:MSW Mock 让开发者不配真实账号也能跑通全链路(tests/mocks/tigris.ts)。

如果你的应用同样面临"图片越来越多、数据库越来越重"的问题,可以直接复用这套模式:选一个 S3 兼容存储,用 SigV4 签名手写上传下载,数据库只留objectKey,再配一层按需优化代理,即可获得一份轻量、可扩展、可离线开发验证的图片存储方案。

相关文档:docs/decisions/040-tigris-image-storage.md · docs/decisions/018-images.md · docs/decisions/041-image-optimization.md · docs/image-storage.md · docs/image-optimization.md

【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 7:48:22

现代Web应用架构模式解析与选型指南

1. Web应用架构概述在当今互联网时代,Web应用架构决定了系统的性能、可扩展性和开发效率。作为一名从业十余年的全栈工程师,我见证过各种架构模式的兴衰演变。目前业内最主流的Web应用架构已经形成了相对稳定的格局,但不同场景下的选择依然存…

作者头像 李华
网站建设 2026/9/18 7:48:18

OptiScaler 安装配置手册:三步替换游戏原生超采样器

OptiScaler 安装配置手册:三步替换游戏原生超采样器 【免费下载链接】OptiScaler OptiScaler bridges upscaling/frame gen across GPUs. Supports DLSS2/XeSS/FSR2 inputs, replaces native upscalers, enables FSR-FG/XeFG on non-FG titles. Supports Nukem mod …

作者头像 李华
网站建设 2026/9/18 7:47:56

Linux软链接处理指南:tar、zip、cp与rsync的默认行为详解

前一阵子给客户迁移一套服务,我把整个应用目录用 tar 打包拷到新服务器,结果解压完发现一堆软链接变成了普通文件,服务起不来;另一处又遇到 cp -r 拷完目录后,里面指向绝对路径的软链接全部失效,链接还在&a…

作者头像 李华
网站建设 2026/9/18 7:47:33

小学数学公式PDF的结构化提取与教学应用

简介:本资源是一份专为小学生及家长、教师设计的数学学习工具包,系统梳理小学阶段必需掌握的各类公式与单位换算规则,覆盖数学基础应用全场景。内容涵盖长度、重量、时间、人民币、面积、体积六大类单位换算表,辅以分数四则运算法…

作者头像 李华
网站建设 2026/9/18 7:46:51

录屏视频损坏打不开?用untrunc重建moov索引修复MP4/MOV

录屏软件突然崩了、电脑断电、进程被强制结束,辛苦录了大半天的素材打不开,播放器提示“文件已损坏”或“无法渲染此文件”——这种经历,拍过视频、做过在线课程、搞过游戏解说的人大概率都撞上过。市面上号称能“万能修复”的工具不少&#…

作者头像 李华