架构

单实例设计、分层与依赖方向、数据模型、上传与访问的数据流、outbox 围栏、周期任务,以及为什么在进程内集成 rclone。

单实例设计

ImgYoung 明确按单实例设计:一个进程、一个数据目录。

  • 元数据库是一个 SQLite 文件(数据目录下的 imgyoung.db,WAL 模式)。写操作走单连接排队,读操作走连接池;驱动是纯 Go 的 modernc.org/sqlite。
  • 任务队列在同一个库里:异步任务由 River 执行,它的任务表与业务表在同一个 SQLite 文件中,不需要额外的队列服务。
  • 其余状态都在本机:上传并发闸、按用户的上传频控、登录限速、访问活跃度的聚合都在进程内存里,衍生图缓存在本地磁盘上。不使用 Redis。
  • 单个静态二进制:全程 CGO_ENABLED=0,管理后台的前端产物与 OpenAPI 文档都编译进二进制。

因此 ImgYoung 不支持多个进程共享同一个数据目录,也不支持水平扩展。需要高可用时,升级路径是把数据库换成 PostgreSQL (River 本身支持 PostgreSQL,任务队列随之迁过去),进程内的共享状态届时再外置;当前代码只有 SQLite 实现。

分层与依赖方向

http ──→ core ──→ infra
  │        └────→ store
  └─────────────→ infra
层目录职责
storestore/公开的存储引擎:driver 接口、rclone 实现、failover 容灾原语。见 作为 Go 库使用
infrainternal/infra/基础设施与出站适配:config(引导配置与运营配置)、db(schema、查询、迁移、sqlc 生成代码)、secret(主密钥与加密)、id(短 ID)、imaging(图像处理)、cdn、urlfetch(URL 转存的抓取)、observability(指标)、version
coreinternal/core/业务核心:channel(渠道与健康探测)、replication(补齐、对账、排空、CDN purge)、upload、variants(按需处理)、dedup、lifecycle、reliability(备份恢复)、auth、audit、referer、usage、alert
httpinternal/http/HTTP 入站:publicapi(上传与图片访问)、s3api(S3 协议面)、adminapi(管理后台)、middleware、httpx、router
装配根cmd/imgyoung/build.go 按「基础设施 → 业务服务 → HTTP」顺序手工构造对象图,serve.go 用 oklog/run 编排启停,loops.go 放周期循环

store 与 infra 是两个并列的叶子:store 不依赖任何 internal/ 包,infra 不依赖 core、store 与 http;core 同时依赖二者。 依赖方向由 golangci-lint 的 depguard 规则强制,CI 不通过即不能合并:

规则禁止导入原因
store/**internal/ 下的任何包store 是公开的可复用存储引擎,不能依赖应用代码
internal/infra/**internal/core、store、internal/httpinfra 是基础设施叶子
internal/core/**internal/http业务核心不知道传输层,可以脱离 HTTP 测试
internal/http/**store选路与容灾属于 core,传输层只拿 core 给出的结果

另外两条约定:

  • 接口由使用方定义:各包在自己内部声明只含所需方法的小接口(如 upload.ChannelSelector、publicapi.BlobChannels),由具体类型隐式满足。 store/driver.Driver 是唯一的一等抽象。
  • 身份显式传参:中间件解析出认证主体放进请求上下文,handler 取出后作为参数传给 core;core 不读 context 里的身份,按用户隔离在 core 中强制执行。

HTTP handler 与 River worker 都只是把外部触发翻译成服务调用的薄适配;周期任务是各服务自带的 Run(ctx, wait) 循环,由装配根统一编排,没有单独的调度层。

数据模型

数据模型的核心是物理图与上传引用两层分离:

images(上传引用)──→ blobs(物理图)──→ blob_channels(各渠道的副本)──→ channels
       N : 1                  1 : N                          N : 1
  • blobs 是一份实际存储的字节。每次上传都新建一个 blob;后台查重把字节完全相同的 blob 合并后,一个 blob 可以被多条引用指向。 它记录净化后字节的 sha256、感知哈希 phash、渠道内对象键 storage_key(形如 {short_id}_{sha256 前 8 位})、MIME、大小、尺寸, 以及引用计数 ref_count 与待删标记。
  • images 是一次上传产生的引用,对外的短 ID(10 位)就是它的主键。归属用户、来源 Token、S3 对象键别名、到期时刻与访问活跃度、 冷图标签与清理豁免都挂在引用上,不挂在 blob 上。
  • blob_channels 记录某个 blob 在某个渠道上的副本,兼作补齐任务的状态:pending → uploading → done / failed,另有尝试次数与最近错误。

两层分离让几件事各得其所:查重合并只是把引用改指到保留的 blob;TTL、冷图与配额按引用计算;副本、补齐与物理删除按 blob 进行。

引用计数:ref_count 的增减都是原子的 UPDATE … SET ref_count = ref_count ± 1。删除引用与递减计数在同一个事务里; 计数归零的 blob 被标记待删,并按运营配置 delete_grace_minutes 记下物理删除时刻。到点后清理任务先清掉它的衍生图缓存, 再在一个事务里删除业务行并复查删除条件(期间又被引用就放弃本次删除),最后为每个持有副本的渠道入队对象删除任务。

其余的表:

表内容
channels渠道:rclone 后端名、用主密钥 AES-GCM 加密的参数 JSON、优先级、健康标记与最近探测结果、是否加密、是否允许直链、清理中标记;创建时刻 created_at(与渠道 ID 一起参与加密渠道的密钥派生,建后不变)与绑定时刻 bound_at(见对账 GC)
variants衍生图缓存的索引:一个 blob 一种处理参数一行,字节存在本地磁盘,按最近访问时间做 LRU 淘汰
settings运营配置,单行宽表;每列的 DEFAULT 就是出厂值。见 配置
users、tokens、sessions用户与配额、上传 Token(只存哈希)、后台会话(只存 session id 的哈希与绑定的 CSRF 令牌)
idempotency_keys上传幂等键,按用户隔离
dedup_candidates相似重复的候选对,人工裁决的工作队列
referer_whitelist、audit_log、alerts防盗链白名单、管理操作审计、告警记录
master_key_fingerprint单行,由主密钥派生的指纹(不能反推主密钥);每次启动据此核对主密钥,缺失或不符即拒绝启动,见主密钥

表结构以 internal/infra/db/schema/ 下每表一个文件的声明式 SQL 为准,每张表与每一列都有注释。版本化迁移由 Atlas 据此生成, 随二进制内嵌,进程启动时按顺序自动应用,已应用的版本记在 schema_revisions 表里,跨多个版本升级也会补齐中间的迁移。

迁移不支持降级:库里记有本二进制不认识的迁移版本(即库已被更新版本的二进制迁移过)时,进程拒绝启动,错误信息写明库里与本二进制各自的最新迁移版本。 回滚只能用「旧二进制 + 升级前的快照」,见 升级。

上传数据流

数据面上传、后台上传、URL 转存与 S3 PutObject 只在「字节从哪来」上不同,拿到字节之后走同一条管道。上传路径不查重:每次上传都新建 blob, 查重在后台进行,上传路径因此是一条没有分支的直线。

入口与防护:认证(上传 Token、会话或 S3 签名),并通过两道闸:按用户的上传频控(upload_rate_per_min)与全局上传并发闸 (max_concurrent_uploads,满载直接返回 429)。随后把图片字节完整读进内存,任何大小都不写临时文件。 单图上限 max_upload_mb 只约束图片本身,恰好等于上限可以上传,超出返回 413。multipart 上传的请求体整体另以「单图上限 + 64 KiB」为界, 只读 file、exif_policy、ttl 三个段:exif_policy 与 ttl 只认表单字段(不读 URL 查询参数),每个最多 1 KiB,超出返回 400。

幂等:带 Idempotency-Key 且同一用户已用过这个键时,直接返回已有的图。

格式与像素预算:按文件头的 magic bytes 判定真实格式,只接受 JPEG、PNG、GIF、WebP;只读图片头取得尺寸,并与嗅探出的格式交叉核对; 宽 × 高超过 max_pixels_mp 直接拒绝,不进入完整解码。解码后的内存由分辨率决定,与文件字节数无关,所以这道闸必须在解码之前。

配额预检:按引用计全额,超出用户配额返回 403,不写渠道。

净化:EXIF 档位为 strip 时剥离元数据(JPEG、PNG、WebP;GIF 没有标准元数据段,原样通过)。 JPEG 剥离后补回一个只含方向(Orientation)的最小 Exif 段,靠方向标记摆正的照片不会转倒;GPS、机型、时间等字段照删,方向为默认值时不补。

哈希:完整解码一次计算感知哈希 pHash,再对将要存储的字节计算 SHA-256。净化在哈希之前,所以 sha256 永远对应渠道里实际存的字节。

写渠道:对象键为 {short_id}_{sha256 前 8 位},嵌入短 ID 是元数据库全部丢失时从存储端反查的最后手段。 用 failover.Write 按优先级向健康渠道写入,凑够 min_success 份即止,单次写入超时 10 分钟,失败顺延下一个渠道。

落库:一个事务内写入 blob、引用、每个在役渠道的副本记录(写成功的为 done,其余为 pending)与幂等键。

入队补齐:为 pending 的渠道入队补齐任务后返回。入队失败只记日志,由对账补上。

整个上传在内存中完成,不写临时文件;峰值内存约为「并发数 × max(单图上限,像素预算对应的解码内存)」,调低这几项即可适配小内存机器。 渠道写入成功而落库失败时会留下孤儿对象,由对账 GC 回收,不回滚渠道写入。

访问数据流

GET / HEAD /i/{id}(以及 S3 对象键别名 /o/{user_id}/{key};后台预览图 /api/admin/images/{id}/thumb 走同一条管线,但不计访问):

  1. 防盗链中间件按 Referer 白名单放行或返回 403(后台预览图不经过这一步)。
  2. 查引用,查不到返回 404。到期时刻已过的引用立即按不存在处理,不必等清理任务回收,也不会被访问续期。
  3. 校验处理参数:w 不在宽度档位、显式 format 不在允许格式时返回 400(GIF 原图不处理,忽略这两个参数)。
  4. 计访问:只有对外入口的 GET 才在内存中累计活跃度,按 activity_flush_seconds 批量落库;HEAD、参数非法的请求与后台预览图都不计访问。
  5. 需要缩放或转格式时走按需处理:带 w、显式 format,或未指定格式而浏览器接受 WebP、档位允许且原图不是 WebP。GIF 一律输出原图。 命中衍生图缓存直接返回;未命中则在 process_workers 并发上限内处理并写入缓存,相同参数的并发请求合并为一次处理; 处理繁忙或失败时降级输出原图,并带 Cache-Control: no-store。HEAD 请求同样取或生成衍生图,只是不返回正文。
  6. 否则输出原图:候选渠道为「健康、不在清理中、该图副本为 done」的渠道,按优先级排序。访问模式为 redirect 且候选中有允许直链、 未加密并配置了公开地址的渠道时,302 到该渠道,重定向带 Cache-Control: no-store(目标随渠道健康与配置变化,不能被缓存); HEAD 请求只写响应头,不读渠道对象;其余用 failover.Open 代理读取,第一个渠道读不到立即切下一个。

缓存语义按入口区分:

入口Cache-Control原因
/i/{id}public, max-age=31536000, immutable短 ID 指向的内容不可变,URL 即版本
/o/{user_id}/{key}public, no-cache同键重传会让别名改指新图;允许缓存,但每次使用前带 ETag 向源站确认,未变即 304

两个入口的原图与衍生图都带强 ETag(原图为内容哈希,衍生图为哈希加处理参数),请求的 If-None-Match 与之相同时返回 304; 304 带上 200 响应会有的 ETag、Cache-Control 与 Vary,缓存据此续用已存的副本。格式经 Accept 协商的响应带 Vary: Accept; 降级输出的原图为 no-store,不走 304。数据库查询失败时返回纯文本 500,可以重试。

outbox 围栏:业务表是唯一真相

异步补齐由 River 执行,但 River 的任务只是幂等的触发器,副本状态的唯一真相是业务表 blob_channels:

  • 补齐任务的含义是「确保 blob X 在渠道 Y 上有副本」。worker 先读业务表:已是 done 直接返回;开始复制前置为 uploading, 失败退回 pending 交给 River 重试,最后一次尝试仍失败置为 failed,成功置为 done。
  • 入队按(blob,渠道)去重,只对尚未结束的任务去重,同一副本不会在队列里出现两份。
  • 对账在启动时与之后每 10 分钟执行一次:为清理中的渠道补入排空任务;为每个渠道补种缺失的副本行;把健康渠道上 failed 的副本复位为 pending; 再按主键游标分页扫描全部 pending / uploading 副本,重新入队;扫描期间有副本变成 done 也不会让后面的页错位漏行。

所以即使 River 的任务表丢失或损坏,队列也能从业务表完整重建;进程被强制杀死时正在执行的任务,超过 16 分钟仍停在运行态会被 River 重新排程。 新建渠道时为存量 blob 播种 pending 副本行,同样由这套机制自然收敛,没有额外的回填逻辑。

其他异步任务遵循同一原则:删除渠道上的对象(delete_object)、排空清理中的渠道(drain_channel:先按剩余的副本行逐个删除副本对象,进度就是剩余的副本行;再删除该渠道上 _imgyoung/ 下的系统对象(含元数据库快照)与深探测残留,别的程序写的对象不动;最后删除渠道记录)、 清理 CDN 缓存(cdn_purge)。它们都是幂等的,失败由 River 退避重试:对象删除最终失败留下的孤儿由对账 GC 回收; 排空任务删对象第一次失败就把原因写进渠道的探测结果并触发 drain_failed 告警,之后一直退避重试,尝试次数用尽后由对账重新入队, 不会停止(任务自身超时或停机取消不算失败),出口是在后台对该渠道选择「直接移除」(远端对象保留);CDN 清理最终失败时,边缘缓存在 CDN 自身的缓存期满后过期。

周期任务

周期任务都是服务自带的循环,由装配根统一启停。间隔取自运营配置的,在后台修改后立即按新值重新计时,不必等旧的周期走完。

任务间隔内容
健康探测health_check_seconds(默认 60 秒)先重试装载此前装载失败的渠道,再对各渠道并行轻探测;不健康的渠道轻探测通过后,再经深探测确认才恢复。见 多存储容灾
告警评估同健康探测渠道不健康、补齐失败、渠道用量超阈值、排空失败、备份失败;可推送到 Webhook
生命周期清理cleanup_sweep_seconds(默认 60 秒)回收到期引用 → 物理删除过了宽限期的 blob → 冷图打标 → 回收过期幂等键 → 回收超期审计日志
活跃度落库activity_flush_seconds(默认 60 秒)把公开访问时刻与计数批量写库;衍生图的最近访问时间按同一间隔落库
查重dedup_scan_seconds(默认 300 秒)精确重复(SHA-256 相同)自动合并,再做 pHash 相似扫描,结果进人工裁决队列
用量上报usage_report_seconds(默认 300 秒)统计各渠道存储用量并更新指标
元数据库备份backup_interval_hours(默认 24 小时)用 VACUUM INTO 做一份一致性快照,写到每个健康且不在清理中的渠道的 _imgyoung/backup/,各渠道各自保留 backup_keep 份;任一渠道没写成即本轮失败(backup_failed 告警),写成的渠道保留这份快照。启动时任一健康渠道上缺少备份间隔内的快照,就先备份一次。见 备份与恢复
认证维护1 分钟 / 1 小时(固定)批量回写 Token 最近使用时刻 / 回收过期会话
补齐对账10 分钟(固定)见上一节
对账 GC24 小时(固定)列举每个健康渠道,与库里的副本记录双向比对:回收孤儿对象,把幻影副本退回 pending 重新补齐。口径见下

对账 GC 的口径

对账 GC 处理的对象(系统目录 _imgyoung/ 与轻探测键不参与比对):

  • 孤儿:只回收同时满足以下条件的对象——键名是本服务的对象键格式 {short_id}_{sha256 前 8 位};库里没有该渠道任何状态的副本记录; 修改时间不早于该渠道的绑定时刻;已过 48 小时宽限期(覆盖「已写入渠道、尚未落库」的在途上传)。其余库里没有记录的对象, 例如别的程序写进来的文件、灾备时新实例接入旧桶而元数据还没恢复的对象、从快照恢复之前写进渠道而恢复后的库里没有记录的对象,只计数不回收,对账日志里记为 foreign。
  • 深探测残留:深探测删除失败留下的 .imgyoung-probe- 临时对象不看写入时刻,过 48 小时宽限期即删,也不计入对账统计的对象数与字节数。
  • 幻影:库里记为 done、渠道上实际缺失的副本,退回 pending 并重新入队补齐。

绑定时刻(channels.bound_at)表示这条渠道记录从什么时候起对应当前的存储位置,本服务只会在它之后往这个位置写对象:

  • 新建渠道时等于创建时刻。新实例接入旧桶时,桶里原有的对象都早于它,只计数不回收。
  • 在后台编辑渠道时,后端名或任何配置参数有改动,即重置为编辑时刻;重新输入同一个密码也算改动(rclone 编码密码时带随机 IV)。 只改名称、优先级或直链开关不重置,令牌刷新写回也不重置。
  • 从快照恢复后,全部渠道重置为恢复后启动的时刻:换入快照之前,在数据目录写下标记文件 imgyoung.db.restored; 迁移之后、加载主密钥之前见到标记即重置,成功后删除标记,日志为 换入快照后已把渠道绑定时刻重置为当前时刻,快照之后写入渠道的对象不会被对账回收。这样,快照之后上传、在恢复后的库里没有记录的对象不会被当作孤儿删除。 标记在换入暂存文件 <数据目录>/imgyoung.db.restore 时写下,后台恢复就是先暂存这个文件;手动恢复也要把快照放成这个文件再启动, 不要直接覆盖 imgyoung.db,否则不会写标记、不会重置绑定时刻。

created_at 不随绑定时刻变化:它与渠道 ID 一起参与加密渠道的密钥派生。

两个实例不能共用同一个渠道路径

对账只认本库的副本记录:另一个实例写进同一路径的对象,在本实例看来格式正确、却没有记录,过了宽限期就会被当作孤儿删除。

停机

停机(收到 SIGINT / SIGTERM,或在后台恢复备份快照后请求重启)按顺序进行:先停收新请求,在途请求最多等 6 秒,超时强制断开; 再取消后台循环与在途的补齐任务,补齐子系统最多等 2 秒,各循环退出前把内存中待落库的数据写完;最后关闭渠道驱动与数据库。 两段合计 8 秒,在 Docker 默认 10 秒的强制终止时限之内,deploy/ 下的示例配置还另留了余量(compose 的 stop_grace_period: 15s、 systemd 的 TimeoutStopSec=15s)。被取消的补齐任务状态留在业务表里,下次启动由对账重新入队。

技术选型

选型围绕同一个前提:单实例、单个静态二进制,除了存储渠道,部署时不需要任何外部服务。依赖的具体版本以仓库的 go.mod 为准。

语言与构建

  • Go:静态编译成单个二进制,Docker 镜像小;多渠道写入、异步补齐、健康探测、任务队列这类 I/O 编排用 goroutine 写起来直接; 对象存储、HTTP、指标监控的生态成熟,语法简单、编译快,参与贡献的门槛低。决定性的一条是 rclone 本身用 Go 编写, 只有 Go 能在进程内直接调用它的后端,见为什么在进程内集成 rclone。
  • 全程 CGO_ENABLED=0:不依赖 C 工具链与系统库,才能交叉编译出 Linux、macOS、Windows 各两种架构的发布包, 并放进只有一个二进制的 distroless 镜像。下面每一项依赖都是纯 Go 或不需要 cgo。构建一律带 -tags nodynamic: WebP 库默认会在运行期尝试加载系统的 libwebp,关掉这条路径后,Linux 二进制才不依赖动态链接器。

依赖

领域选择为什么
HTTP 路由chihandler 就是标准库的 http.Handler,没有私有概念;上传(流式读请求体)与图片代理(流式写响应)都直接面对 io.Reader / io.Writer,薄路由最合适
SQLite 驱动modernc.org/sqlite纯 Go 实现,不需要 cgo
数据库访问sqlc查询写成 SQL,生成类型安全的 Go 代码。不用 ORM:图床的查询都是简单的精确查询,ORM 的抽象没有收益
迁移Atlas声明式 schema 是唯一依据,开发期由 Atlas 计算差异生成版本化迁移;运行期用它的纯 Go 执行器在启动时应用,部署方不需要安装迁移工具
任务队列River持久化任务队列,重试、退避与并发控制现成;用 SQLite 驱动,任务表与业务表在同一个库里,不多一个服务。任务只是幂等触发器,状态以业务表为准,见outbox 围栏
周期任务各服务自带的 Run(ctx, wait) 循环周期任务只需要「每隔多久执行一次」,间隔取自运营配置、修改后立即重新计时;不引入 cron 库
依赖装配在 cmd/imgyoung/build.go 里手工装配组件集合固定、依赖关系静态,按顺序调用构造函数即可,能搜索、能跳转,没有反射与运行时容器。不用 DI 框架
生命周期编排oklog/runHTTP、River 与各后台循环各是一个 actor,任何一个退出即触发整组按序停机;比手写 WaitGroup 加通道更不容易写错
架构守卫depguard(golangci-lint 内置)层间依赖方向写成 lint 规则由 CI 强制,边界不靠自觉,见分层与依赖方向
感知哈希goimagehash纯 Go,输入是解码后的 image.Image,直接接在上传的解码步骤后面
图像编解码标准库、golang.org/x/image、gen2brain/webp都不需要 cgo;WebP 编码由 gen2brain/webp 在纯 Go 的 WebAssembly 运行时里执行 libwebp
加密标准库 AES-256-GCM 与 HKDF;golang.org/x/crypto 的 argon2id分别用于加密渠道配置与凭证、派生渠道密钥与 S3 secret、哈希登录密码。渠道数据本身的加密交给 rclone crypt
限流golang.org/x/time/rate上传频控与登录限速都是进程内的令牌桶
日志与指标log/slog、Prometheus 的 client_golang各自领域的事实标准
短 IDjaevor/go-nanoid对外短 ID 用成熟实现;内部主键用 SQLite 自增 rowid,不对外暴露

项目以 MIT 许可发布:与 rclone 一致,贡献门槛最低;自部署工具不存在被云厂商拿去托管转售的现实顾虑。

不引入的东西

  • Redis:单实例下没有它的位置。幂等键、引用计数这类需要原子性的地方交给 SQLite 的约束与原子更新,限流是进程内令牌桶, 衍生图缓存在本地磁盘。单实例下进程内的实现严格优于 Redis:没有网络往返与序列化,也不多运维一个服务。走向多实例时才需要重新考虑。
  • 配置文件与配置库:环境变量只负责把进程启动起来,其余运营配置以数据库里的 settings 表为唯一依据、在后台修改, 「这个值从哪来」永远只有一个答案,见配置。
  • ORM、DI 框架、cron 库:理由见上表。

SQLite 与版本化迁移

SQLite 是单个文件、没有外部依赖,一致性快照也就是一个文件,最贴合自部署,也与单实例设计相配。 版本化迁移从第一天起就有:长期运行的自部署实例会跨很多个版本升级,没有版本化迁移,表结构很快失控。迁移只有升级方向,见数据模型。

两条升级缝

只有两处预期会被替换,其余选型都是定死的:Go 生态的薄库都贴着标准库的形状,真要替换也是机械劳动,为此预留灵活性没有收益。

  • 图像处理引擎:按需处理的唯一入口是 internal/infra/imaging 的 Processor.Process(src, params)。当前是纯 Go 实现:标准库解码 JPEG / PNG / GIF,WebP 编码用 gen2brain/webp,只输出 WebP、JPEG、PNG,GIF 只收发原图不处理。AVIF、HEIF 不在其列:使用率低, 编码器(如 libaom)又重。需要更多格式或更快的处理时(例如基于 libvips 的实现),替换的是这个处理器。
  • 数据库:当前是 SQLite。表结构以声明式 SQL 为准,迁移由 Atlas 生成,查询代码由 sqlc 生成;换 PostgreSQL 时, 按目标方言重新生成查询代码与迁移,并换上 River 的 PostgreSQL 驱动。

两处目前都只有一个实现,没有为它们预先定义接口类型。

为什么在进程内集成 rclone

存储层的原则是:rclone 只负责「把字节存到某个后端、取回、查询、列举、删除」,主备切换、补齐、引用计数、查重、TTL、健康判断全部由 ImgYoung 自己实现, 两者之间隔着 store/driver.Driver 接口。

集成方式是直接 import rclone 的 fs 与后端包,在进程内以参数映射构造 fs.Fs,而不是调用 rclone 命令行、librclone 的 RPC 接口或 rclone 配置文件:

  • 流式、不落盘:上传用 fs.Fs.Put 直接写内存里的字节,读取直接把对象流转给 HTTP 响应。
  • 配置在自己的库里:渠道参数用主密钥加密后存在 channels 表,运行时解密成映射交给后端;参数含凭证,不写日志,错误链也不携带参数。
  • 令牌写回只有一个刷新者:rclone 自己刷新 OAuth 令牌,经参数写回钩子交给应用,立即加密落库;应用不另起刷新循环。
  • 一个句柄完成全部操作:读、写、探测、列举、删除走同一个 fs.Fs,错误映射只维护一套(对象缺失统一收敛为 driver.ErrNotFound)。
  • 渠道加密零额外代码:加密渠道在后端之上叠加 rclone 的 crypt 层,上传与访问路径无需改动;crypt 是公开格式,持密码可以用标准 rclone 独立解密。见 渠道存储加密。
  • 配置可以原样导出:后台能为每个渠道导出一份 rclone 配置文件,参数与驱动实际交给后端的完全一致,加密渠道另带 crypt 远端; 它由 store/rclone 的 ConfigFile 生成,与构造驱动共用同一套参数整理,标准 rclone 凭它即可脱离 ImgYoung 读写渠道数据。见 驱动接口与 rclone 实现。
  • 测试不需要云账号:rclone 的 local 与 memory 后端让整条存储链路可以在本机做集成测试。

注册全部 rclone 后端(backend/all)意味着任何 rclone 支持的存储都能直接作为渠道使用,rclone 升级即带来新后端;代价是二进制体积达到百 MB 量级。 这里用的是 rclone 的内部 API,它不承诺稳定,所以 go.mod 固定了 rclone 版本,升级由人工进行并跑完整测试。

Driver 接口不假设 rclone 存在,这也是 rclone 内部 API 变动时的退路:换一个 Driver 实现即可,业务代码不动。 接口与 rclone 实现拆成两个包,也让应用之外的 Go 项目可以单独复用这套存储引擎,见 作为 Go 库使用。

本页目录