Skip to content

Latest commit

 

History

History
307 lines (207 loc) · 19 KB

File metadata and controls

307 lines (207 loc) · 19 KB

存储迁移指南:更换 S3 服务商与本地 ⇄ 对象存储互迁

本文说明在 Ech0 中两类常见运维场景的可行做法与注意事项:

  1. 更换 S3 兼容存储(换服务商、换桶、换 Endpoint/CDN 等,仍使用对象存储)。
  2. 本地文件存储对象存储(S3)之间的双向迁移

重要:当前版本没有提供「一键迁移」或图形化迁移向导;下列流程需在理解数据格式的前提下,由管理员自行使用对象存储工具、脚本与数据库操作完成。操作前请完整备份数据库与文件。


1. 必读:Ech0 如何表示与定位文件

1.1 files 表中的关键字段

业务上每条文件记录对应一个逻辑文件,核心字段包括(详见 internal/model/file/file.go):

字段 含义
key 扁平存储键(如 uid8_时间戳_随机串.png),上传时由服务端生成,不是从对象存储回写的完整 Object Key。
storage_type local(本地)或 object(对象存储)等;决定历史记录「登记」在哪一类后端。
provider / bucket 对象存储的元数据(如提供商、桶名),用于唯一性与展示;按文件 ID 代理读取时,运行时并不用这两项去连接旧桶(见下文)。
url 前端直链快照:创建/上传时根据当时配置拼出的可访问 URL;换 CDN、换桶域名后可能过期,需视业务决定是否批量更新。

唯一索引 idx_file_route 包含 storage_typeproviderbucketkey,批量改元数据时注意避免冲突。

1.2 扁平 key 与实际磁盘 / 桶内路径

数据库只存扁平 key。真正落到磁盘或桶里的相对路径由 VireFS SchemaResolve 规则决定(internal/storage/schema.go):

  • 扩展名分到 images/audios/videos/documents/,其余走 files/
  • 文件名部分就是整条扁平 key(例如 images/abc_123_xxx.png 中的 abc_123_xxx.png 即库里的 key)。

对象存储还会在 VireFS 层叠加 PathPrefix(管理后台 S3 设置或环境变量中的路径前缀),因此:

桶内 Object Key(概念上)≈ PathPrefix(若配置) + schema.Resolve(扁平 key)

本地同理,在 DataRoot 下使用 schema.Resolve(扁平 key) 作为相对路径(默认 DataRootdata/files,见 internal/config/config.gointernal/storage/manager.go 合并逻辑)。

举例:上传一张 PNG 图片之后,各层长什么样?

下面用一组虚构但贴近真实格式的数据,把「库里的 keyschema.ResolvePathPrefix → 最终落盘/落桶」串成一条线,便于对照控制台里的对象列表或本地目录。

假设:

  • 管理员上传文件 screenshot.png,服务端生成的扁平 key(入库)为:
    a1b2c3d4_1735689600_deadbeef.png
    (格式大致为:用户 ID 缩略 + Unix 时间戳 + 随机后缀 + 扩展名,见 internal/storage/keygen.go。)
  • PathPrefix(管理后台 S3 设置或 ECH0_S3_PATH_PREFIX)配置为:prod/ech0(无首尾斜杠,合并时会规范化)。
  • 对象存储:桶名为 my-ech0-bucket,Endpoint 为 s3.example.com配置 CDN(CDNURL 为空)。
  • 本地存储(若走本地):DataRoot 为默认的 data/files

第一步:schema.Resolve(扁平 key)

.png 命中图片规则,得到相对路径(含类型目录)

images/a1b2c3d4_1735689600_deadbeef.png

第二步:对象存储 —— 桶内 Object Key(逻辑)

VireFS 会先加 PathPrefix/,再交给 ObjectKeyFunc 解析后的路径。概念上整条 Key 为:

prod/ech0/images/a1b2c3d4_1735689600_deadbeef.png

在 S3 控制台或 aws s3 ls 里,你应能在桶 my-ech0-bucket 下看到上述路径(具体是否带 prod/ech0 前缀以你配置的 PathPrefix 为准;未配置 PathPrefix 时则仅为 images/...)。

第三步:拼进 files.url 的直链快照(无 CDN 时)

internal/storage/provider.go 中无 CDN 时,会把 Endpoint + 桶名PathPrefix + Resolve 结果 拼成对外 URL,形如:

https://s3.example.com/my-ech0-bucket/prod/ech0/images/a1b2c3d4_1735689600_deadbeef.png

(若配置了 CDNURL,则主机部分会换成 CDN 域名,路径仍为 PathPrefix + images/...。)

第四步:同一文件若存在本地磁盘上

相对路径仍是 schema.Resolve 的结果,落在 DataRoot 下:

data/files/images/a1b2c3d4_1735689600_deadbeef.png

注意:本地布局没有 S3 的 PathPrefix 这一层;PathPrefix 只作用在对象存储侧。因此做「本地 ⇄ S3」迁移时,对比的是:

  • 本地:DataRoot + images/...
  • 桶:PathPrefix + images/...(与当前 S3 配置一致)

小结对照表

层级 示例值
数据库 key a1b2c3d4_1735689600_deadbeef.png
schema.Resolve(key) images/a1b2c3d4_1735689600_deadbeef.png
桶内 Object Key(含前缀) prod/ech0/images/a1b2c3d4_1735689600_deadbeef.png
本地文件完整路径 data/files/images/a1b2c3d4_1735689600_deadbeef.png

换桶或互迁时,只要保证「新环境下按上表同一规则还能找到字节」,并与 files 里存的 key / storage_type / url 策略一致即可。

1.3 运行时如何读文件(File 模型、local / object、与前端路由对照)

同一套 File 记录在系统里可能通过多条 HTTP 路径被访问;迁移时「改桶了代理还能下」和「首页图是否裂图」对应的路径不一定相同。下面按路由说明服务端行为与当前前端实际用法。

1.3.1 访问路径总览(建议先读)

访问方式 典型 URL 形态 服务端实现要点 当前前端主要用途
本地静态文件 /api/files/images/…(相对路径,再拼站点 base) Engine.Static("api/files", DataRoot),URL 路径直接映射磁盘(internal/router/modules.go Echo 时间线、画廊、编辑器等:通过 getFileUrl 使用入库的 url,本地多为本表第一行
对象存储直链 https:// 桶域名或 CDN + 路径 无 Ech0 读盘:浏览器直连 S3/CDN 同上;getFileUrl 见绝对 URL 则原样使用(web/src/utils/other.ts
按文件 ID 取流 GET /api/file/:id/stream StreamFileByID:用 key + 当前存储配置 Get不用 urlinternal/service/file/file.go 仅管理端TheStorageFileList.vue 中「下载」调用 fetchDownloadFileByIdweb/src/service/api/file.ts
按存储路径取流 GET /api/file/stream?storage_type=…&path=… StreamFileByPath:管理员按路径读 仅管理端:同上文件树,无 file_idfetchDownloadFileByPath 兜底
外链重定向 同上 ID 流式路由但 storage_type=external 302File.url 外链文件场景

说明:

  • /api/file/.../stream/api/file/stream 挂在需鉴权的路由组上(需 ScopeFileRead 等,见 internal/router/file.go),与直接暴露的静态 api/files 不是同一套中间件策略;管理端下载走 stream 时会带 token(见 downloadFile 与前端请求封装)。
  • RSS / 服务端拼 HTML(如 internal/service/common/common.go 中 Feed)若使用 ef.File.url,则与浏览器「看 url」一致,不经过 stream。

1.3.2 前台与 RSS:绝大多数走 File.urlgetFileUrl

上传或创建文件时,接口返回并持久化 File.url。前端统一通过 getFileUrl / getImageUrlweb/src/utils/other.ts)解析:

  1. urlhttp(s):// 绝对地址(对象存储入库时常见)

    • 浏览器直接请求该 URL,即按入库时的直链快照访问桶或 CDN。
    • 换 S3、换 CDN、换自定义域名后,若库中 url 未更新,容易出现裂图或旧域名。
  2. url 为相对路径(本地常见:/api/files/...

    • VITE_SERVICE_BASE_URL 拼接后请求本站。
    • 对应 §1.3.1 表格第一行:由 Gin Static 直接读 DataRoot 下文件,不经过 StreamFileByID

因此:时间线/卡片上的图、音频封面等,默认不是「stream API」对象HTTPS 直链为主,本地/api/files/... 静态为主。

1.3.3 本地静态映射(与 stream 的区分)

  • 注册方式:ctx.Engine.Static("api/files", root)root 为配置中的 DataRoot(默认 data/files)。
  • 浏览器请求 GET /api/files/images/xxx.png 时,等价于读磁盘 data/files/images/xxx.png(路径与 schema.Resolve(key) 一致)。
  • 这是前台本地媒体的主路径;除非产品改为全部走代理,否则不必/api/file/:id/stream 展示本地图。

1.3.4 stream API:服务端行为与前端调用面

按 ID:GET /api/file/:id/stream

  • 处理函数:StreamFileByIDinternal/service/file/file.go)。
  • local / object:使用当前合并配置 + 记录中的 key 调用 Get不使用 url 读字节;不使用行内 provider / bucket 选择连接(换桶后对象路径正确时,代理仍可能成功)。
  • external302File.url

按路径:GET /api/file/stream?storage_type=…&path=…&name=…

  • 处理函数:StreamFileByPath,供已知存储路径、但树节点上可能没有绑定 file_id 时的兜底下载。

前端谁在用(截至当前仓库)

API 封装函数 使用位置
/file/:id/stream fetchDownloadFileById web/src/views/panel/modules/TheSetting/TheStorageFileList.vue(存储文件树 · 下载)
/file/stream?… fetchDownloadFileByPath 同上,无 file_id

web/src/lib/file/api/adapter.ts 中的 buildStreamUrl(手动拼 /file/:id/stream + token)已导出,但全项目无其它引用,可视为预留;上传队列file-queue.ts)只调用 uploadFile不使用 stream。

结论stream 面向「带鉴权的按 ID/路径下载」(当前实现集中在设置 → 存储文件列表);不是 Echo 主站看图路径。

1.3.5 与迁移、换 S3 的对应关系

你关心的现象 优先检查
首页/时间线裂图、RSS 图裂 多为 url 仍是旧域名或旧桶 URL;对象存储需更新 url 或修正 CDN/桶策略
管理端文件树下载失败 key + 当前 S3/本地配置 能否读到对象;与 §1.2 路径规则是否一致
仅代理能下、直链不能(或反之) 直链依赖 url/桶权限/CDN;代理依赖 key 与存储配置,两套问题独立排查

2. 场景一:更换 S3 服务商 / 桶 / Endpoint

2.1 典型目标

  • 从 A 厂商(或自建 MinIO)迁到 B 厂商;
  • 或同一厂商下更换桶、Region、自定义域名/CDN;
  • 数据库仍为同一套 SQLite(或同一实例),希望尽量少改库只改必要字段

2.2 核心原则

  1. 对象字节:在新桶中的 Object Key 必须与新配置下的
    PathPrefix + schema.Resolve(key) 一致(与旧环境对比时,注意旧环境的 PathPrefix 是否不同)。
  2. 应用配置:在管理后台或环境变量中更新 Endpoint、密钥、桶名、PathPrefixCDNURLUseSSL 等,并保证 ECH0 进程重载或重启后使用新配置(StorageManager 会从 DB/环境合并配置)。
  3. 数据库
    • 最低限度:迁完对象、改好配置后,先验证按文件 ID 访问是否正常。
    • 建议:将 files 表中仍指向对象存储的行的 providerbucket、必要时 url 更新为与新环境一致,避免管理列表、统计与外链混乱。
  4. url 字段:若前端、RSS、Webhook 等依赖库中直链,换 CDN 或桶公共访问域名后,应批量重算或按新规则更新 url(新 URL 的拼接逻辑可参考 buildS3PathURLResolver,见 internal/storage/provider.go:含 CDN、Endpoint+桶、PathPrefixschema.Resolve 后的路径)。

2.3 推荐操作顺序(降低停机风险)

以下顺序可按实际窗口调整;核心是先保证新桶里已有正确对象,再切流量与配置

  1. 备份:SQLite 文件、当前 S3 设置快照、旧桶对象列表(或整桶同步到本地 staging)。
  2. 确定路径规则:列出旧环境与新环境的 PathPrefix、是否使用 CDN;对每条 files.key 计算旧桶源路径与新桶目标路径是否一一对应(同一 keyResolve 结果相同,仅桶/前缀/域名变)。
  3. 拷贝对象:使用厂商控制台、CLI(如 aws s3 syncrclone)或批量复制,将对象从旧桶复制到新桶,保持相对 Key 一致(含前缀与 images/ 等目录)。大桶时注意限速、校验与失败重试。
  4. 在测试环境或只读验证:用新凭证 + 新桶配置启动实例(或临时改配置),抽查若干 key 对应的代理访问与直链。
  5. 切换生产配置:更新管理后台 S3 设置或环境变量,重启/重载服务。
  6. 更新数据库(建议):批量 UPDATE providerbucketurl(按需)。
  7. 回归:前台发图、RSS、备份任务(若也使用 S3)等。

2.4 常见踩坑

问题 说明
只改配置未迁对象 新桶为空或路径不一致,读文件 404。
PathPrefix 与拷贝路径不一致 配置里是 upload/,对象却放在桶根,或反之。
误以为必须改 key 一般不需要key;除非你在新桶刻意用了另一套命名,则需同步改库或做映射(项目内无通用映射层)。
忽略 url 界面仍显示旧 CDN 链接或 403;需更新 url 或改为使用本站代理 URL。
迁移期间双写 切换窗口内应避免同一 key 在旧桶与新桶被不同版本覆盖;建议维护窗口内只读或停写。

3. 场景二:本地存储 ⇄ 对象存储(S3)互迁

3.1 可行性说明

  • 逻辑上:本地与对象存储使用同一套 FileSchemaschema.Resolve),同一 key 在两侧的相对路径部分一致(都是 Resolve(key)),差异在于根是 DataRoot 还是 桶 + PathPrefix
  • 工程上:项目未提供内置「本地 ⇄ S3」迁移命令;需要自行复制文件并更新 files中的 storage_type 及关联字段。

3.2 路径对应关系(用于编写脚本)

设扁平文件名为数据库中的 key,则:

  • 本地绝对路径(概念)
    本地根目录 = 配置中的 DataRoot(默认 data/files
    相对路径 = schema.Resolve(key)
    完整路径 ≈ filepath.Join(DataRoot, Resolve 结果)(注意操作系统路径分隔符)。

  • 对象存储 Object Key(概念)
    trim(PathPrefix) + "/" + schema.Resolve(key)(与 VireFS WithPrefix + WithObjectKeyFunc(schema.Resolve) 一致;具体拼接以 VireFS 实现为准,迁移前应用少量样本在测试桶验证)。

迁移脚本应对每条 files 记录读取 keystorage_type,仅处理需要从一种后端迁到另一种的行。

3.3 本地 → 对象存储

目标:文件只保留在 S3,记录改为 object

  1. 备份数据库与 data/files
  2. 启用并正确配置 S3(管理后台或 ECH0_S3_* 等环境变量),确认 PathPrefix 与计划上传路径一致。
  3. 对每条需迁移的记录:从本地读出文件,上传到目标桶的 目标 Object Key(按上一节规则计算)。
  4. 校验对象大小与 Content-Type(可选)。
  5. 在事务或分批中更新 SQLite:
    • storage_type = 'object'
    • 填写新的 providerbucket
    • 按当前配置重算或写入 url
  6. 确认应用读取正常后,再删除本地对应文件(或先改名目录做回滚备份)。

3.4 对象存储 → 本地

目标:文件只保留在本地磁盘,记录改为 local

  1. 备份数据库;确保磁盘空间与 DataRoot 权限足够。
  2. 对每条需迁移的记录:从桶中按 Object Key 下载到
    DataRoot + "/" + schema.Resolve(key)
    (需创建子目录,与本地 FS 布局一致)。
  3. 更新 SQLite:
    • storage_type = 'local'
    • provider / bucket 可置空或与项目约定一致(注意唯一索引是否允许空字符串,需与现有数据约束一致)
    • 更新 url 为本地访问方式(例如本站 /api/files/... 类路径,以当前部署为准)
  4. 验证代理访问后,再从桶中删除对象(或生命周期策略延后删除)。

3.5 互迁共同注意事项

项目 说明
key 一般不改 保持与现有 Echo、评论等关联一致;迁移的是字节位置storage_type
批量与事务 大量文件时分批提交,避免长时间锁库;记录失败行以便重试。
唯一索引 更新 provider/bucket 时避免与已有行冲突。
TempFile 若有未确认的临时上传,先完成业务确认或清理逻辑,避免迁了一半的孤立记录。
双后端并存 配置允许「启用对象存储」与本地并存时,新上传走哪种由产品与上传接口决定;历史行仍以 storage_type 为准。

4. 校验与回滚建议

  • 抽样:随机抽取图片/音频,对比文件大小与 Content-Type;用浏览器或 curl 访问代理 URL。
  • 清单:导出 files 表中 id, key, storage_type, url 迁移前后对比。
  • 回滚:保留旧桶或旧 data/files 副本直至观察期结束;数据库恢复需配合文件状态一致。

5. 相关代码与配置索引(便于对照实现)

说明 位置
File 模型 internal/model/file/file.go
Schema(Resolve 规则) internal/storage/schema.go
本地 / S3 FS 与 PathPrefixObjectKeyFunc internal/storage/provider.go
存储配置合并(DB + 环境变量) internal/storage/manager.go
按 ID 流式读取(仅用当前配置 + key internal/service/file/file.goStreamFileByID
S3 设置键名 internal/model/common/common.goS3SettingKey
环境变量前缀 internal/config/config.go(如 ECH0_S3_*

6. 文档版本与免责

  • 本文基于当前仓库实现整理;路径拼接细节以 VireFS 与运行时配置为准,大规模迁移前务必在测试环境用真实 key 验证。
  • 生产变更需由具备权限的运维执行;作者不对误操作导致的数据丢失负责。

如有条件,建议在项目后续版本中提供官方迁移工具或只读校验脚本,以减少人为错误。