Repository navigation
feat(drivers): add Cloudflare R2 driver (Workers binding + S3 API) - #131
Conversation
新增独立 R2 驱动,同时支持两种接入方式(binding 优先,自动回退): - Workers R2 bucket binding 直连:无需 API Token,绑定名可在存储配置指定(bucket_binding),未指定时按 R2/R2_BUCKET/CF_R2/OPENLIST_R2 顺序探测;驱动实例跨请求复用,绑定在每次操作时从请求级 env 上下文(getEnvCtx)重新解析,避免绑定对象随 isolate 滞留。 - S3 兼容 API:account_id 自动推导 endpoint(https://<account>.r2.cloudflarestorage.com),或显式 endpoint;复用 S3Driver 的签名/列表/预签名实现,R2 固定 region=auto。 - 目录语义与 S3 对齐(delimiter 列目录、占位对象、root_folder_path 前缀、递归 copy/move/remove、批量删除)。 - 下载链路:binding 模式返回空 raw_url 触发服务端流式下载(createReadStream,Range 换算 R2 的 offset/length);配 custom_host(r2.dev/自定义域名)时改走公开直链。binding 不提供预签名直链(Workers 绑定无该能力),直传仅在 api 模式提供并给出明确指引。 - 注册与配置:storage.ts 路由 r2/cloudflarer2/cfr2/r2native;admin driverConfigs 新增 R2 表单并让 r2 别名指向新驱动(历史 driver=S3 的 R2 配置不受影响);wrangler.jsonc 补 r2_buckets 绑定说明;README 驱动清单更新。 - 测试:9 个用例覆盖绑定解析、cursor 翻页、root 前缀、mkdir/remove/rename/move/copy、Range 流式读取、api 回退与路由别名(内存版 R2Bucket mock)。
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
openlist-work | d0123f1 | Oct 16 2026, 12:17 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
openlist-tsworkers | d0123f1 | Oct 16 2026, 12:17 PM |
主 README 的驱动清单新增了 Cloudflare R2,本次把同一句同步到 readmes/ 下全部 12 个语言版本(ar/de/en/es/fr/hi/it/ja/ko/pt/ru/zh-TW),并按各文件既有的标点惯例处理(中日韩印用全角括号与「、」,其余用半角括号与逗号,阿拉伯语用「،」)。
…facts - README 与 12 个语言版本:把安装向导、JWT_SECRET、DB_* / CACHE_* / SINGLEFLIGHT 等大段配置说明压缩为表格 + doc.oplist.org 链接,正文只保留默认可用路径与建议显式配置的项 - 顺带修正过时描述:前端为 SolidJS + Hope UI(原写 React 19 + Ant Design)、运行平台补阿里云 ESA、数据存储口径改为 D1/KV/Blob/DO 与外部 MySQL - .env.example / .dev.vars.example:缓存配置说明由「详见 README」改为指向文档站
pikachuren
left a comment
There was a problem hiding this comment.
🙏 感谢 @PIKACHUIM 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 Claude Sonnet 4 模型进行分析。
🎯 结论
✅ Approve — 设计精巧、实现完整、测试到位,CI 全绿,无阻塞问题
📖 概要
feat(drivers): add Cloudflare R2 driver (Workers binding + S3 API)
核心改动:新增独立 R2 驱动(binding 优先 + api 回退)+ admin 表单 + 测试 + 文档
🧭 整体方案
Why:R2 在 Workers 上的首选用法是 binding 直连(零密钥、免出网、可跨账号共享桶),本项目此前只能靠"S3 驱动 + 手填 r2 endpoint"曲线使用;Go 版有独立 r2 驱动但无 binding,本 PR 在 TS 版补齐并扩展。
How:新增 drivers/r2/(driver.ts 719 行 + types.ts 106 行 + test 455 行),两条路径:
- binding —
wrangler.jsonc声明r2_buckets→ 存储配置填bucket_binding→ 驱动层三处探测(env 参数 / getEnvCtx() / globalThis)并校验方法形态(list/get/put/head/delete);下载:配了custom_host返回公开直链 302,否则raw_url为空串触发raw.ts的createReadStream流式转发(R2 binding 不提供预签名 URL),Range 从 S3 语义{start,end}转换为 R2 语义{offset,length} - api —
account_id推导 endpoint 或显式填 endpoint + key/secret,整体委托S3Driver(签名 / 列举 / 预签名直链)
Mode 选择:mode=auto(默认)→ 能解析到 binding 就用 binding、解析不到且有 api 配置就用 api、都没有时给出"缺 bucket/endpoint/密钥"指引;mode=binding 明确要求 binding(解析不到直接报错,不静默回退)
设计亮点:
- 每次操作解析绑定,不缓存在驱动实例(实例跨请求缓存,缓存 env / 绑定对象会滞留 isolate 生命周期 + 多账号场景绑定错误)
- 三处探测 + 方法形态校验(同名变量是普通字符串时判不可用)
- init() 只告警不抛错(后台
load_all/ 保存存储时尚未注入请求 env,抛错会禁用存储) - 翻页只看 truncated(R2 文档明确警告:设置了
include时单页条数可能少于 limit,不能用返回值推断) - 目录语义与 S3 对齐(占位对象
.openlist+root_folder_path+ 递归 copy/move/remove,R2 delete 支持批量数组 1000/批)
方案合理性:✅ binding 优先 + api 回退是 CF Workers 最佳实践(绑定免密钥免出网);raw_url 为空串触发流式转发是唯一可行方案(binding 不提供预签名 URL);每次解析绑定避免了 isolate 滞留与多账号混淆;Range 转换正确(S3 {start,end} 闭区间 → R2 {offset,length});测试覆盖核心分支(9 个用例 + 内存 R2Bucket mock)
📊 变更统计
22 个文件(+1562/-388 行) | 功能 ⭐⭐⭐⭐⭐ | 最小改动 ⭐⭐⭐⭐ | 前向兼容 ⭐⭐⭐⭐⭐ | 方案设计 ⭐⭐⭐⭐⭐
🚨 关键问题
无重大问题(P0/P1 均无)
P2(可选优化):
- 💡
driver.ts:619-643createReadStream 对 api 模式抛错"请使用预签名直链",但 api 模式下raw_url已返回预签名直链(S3Driver),raw.ts不会调到这里 —— 此分支实际不可达,可以throw new Error("unreachable: api mode returns presigned raw_url")标明 - 💡
driver.ts:147bindingError内建的修复指引可补充"或切换到 api 模式(填 account_id + key/secret)" - 💡 PR 描述提到"未做真机/线上 R2 验证",建议合并前由维护者在有 R2 桶的 Workers 环境执行手动验证步骤(后台新建 R2 存储 + binding 模式列表/下载/写操作 + api 模式预签名直链)
📂 逐文件分析
drivers/r2/driver.ts(719 行)
改动意图:binding + api 双模式 R2 驱动,binding 优先、api 回退,下载策略区分 custom_host 直链 / api 预签名 / binding 流式转发
核心逻辑:
- L80-100:
resolveR2Bucket三处探测(env 参数 / getEnvCtx() / globalThis)+ 方法形态校验(list/get/put/head/delete函数) - L140-160:
resolveBucket()每次操作重新解析(不缓存在驱动实例) - L256-304:
listPrefix翻页只看truncated(不用返回值条数推断),cursor原样回传,delimitedPrefixes提取子目录 - L340-350:
objectRawUrl返回custom_host直链或空串(空串触发raw.tscreateReadStream) - L618-643:
createReadStreamRange 转换(S3{start,end}闭区间 → R2{offset,length})
设计亮点:✅ 每次解析绑定避免 isolate 滞留;三处探测覆盖 CF Workers(env)+ 其他平台(globalThis);init()只告警不抛错(保存流程尚无 env 时不禁用存储);翻页策略严格按文档(truncated+ cursor 推进失败终止)
drivers/r2/r2.test.ts(455 行)
改动意图:9 组用例覆盖 binding 解析、mode 选择、目录语义、Range、回退
测试覆盖:
normalizeR2Addition默认值与root_folder_path归一化- binding 三处探测 + 同名字符串变量判不可用
- 列目录(目录+文件、占位对象过滤、
pageSize=1翻页、深层不泄漏) root_folder_path前缀作用于 list/get/put- mkdir 占位对象 + remove 区分文件/目录
- rename/move/copy(文件 copy+delete、目录递归)
createReadStream无 Range 全量 + 有 Range 切片- 绑定缺失时
mode=auto回退 api(断言 endpoint 由account_id推导)+mode=binding报错 + 无配置时指引 getDriver路由(R2/r2/CloudflareR2/cfr2均命中)
实现质量:✅ 内存R2Bucketmock 模拟完整(list翻页 +cursor、getRange、批量delete),断言精确(cursor 推进、Range offset/length、endpoint 推导)
internal/model/db.ts(+6 行)
改动意图:新增 getEnvCtx() 暴露请求级上下文给驱动层
实现质量:✅ 简单导出(export function getEnvCtx() { return envCtx }),配套 setEnvCtx 已存在,无副作用
internal/op/storage.ts(+9 行)
改动意图:路由 r2/cloudflarer2/cfr2/r2native 到 R2Driver,保留原有 S3 分支
实现质量:✅ 紧跟新增分支后保留 s3|doge|minio|ceph|aws|b2|cos|oss|kodo 分支(历史 S3 存储不受影响)
server/admin.ts(+153 行)
改动意图:新增 R2 表单(16 字段)+ normalizeDriver 让 r2/cloudflarer2/cfr2 归一到 R2 驱动
实现质量:✅ 字段完整(binding + api 双模式);normalizeDriver 逻辑清晰(历史 driver: "S3" 不变,新建 r2 归到 R2)
wrangler.jsonc / README / readmes/*(文档)
改动意图:补「方案 E:R2 对象存储」注释块 + 驱动清单
实现质量:✅ 绑定示例清晰(r2_buckets 声明 + binding vs api 行为差异说明)
✅ 待处理清单
- [P2]
driver.ts:619createReadStream api 模式抛错改为 unreachable 标注 - [P2]
driver.ts:147bindingError 补充 api 模式回退指引 - [P2] 合并前由维护者在有 R2 桶的 Workers 环境执行手动验证
🎯 结论:✅ Approve — 设计精巧(binding 优先 + 每次解析)、实现完整(9 测试用例 mock 覆盖)、文档到位,CI 全绿,无阻塞问题
…sed lookups - 驱动名由 R2 改为 Cloudflare R2(与既有的 GitHub Releases 等带空格命名一致); normalizeDriver 把 R2 / r2 / cloudflare_r2 / cfr2 全部归一到它。 本分支可能已存在的 driver=R2 记录会在下次保存时自动改名,运行时两种写法 都能命中新驱动。 - 修 server/fs.ts 的直传工具判定:改为按「归一化后的驱动名」匹配(小写 + 去非 字母数字)。原实现拿原始字符串做 ^ 锚定正则,驱动名带空格后会漏判,导致 api 模式下 s3_presigned 直传入口消失。 - R2 的直传工具只在 api 模式宣告:binding 无预签名能力,新增 r2CanSignDirectUpload()(判定与 R2Driver.resolveMode 一致), 不再向前端暴露必然报错的入口。 - 删除 op/storage.ts 的 S3 分支里已成死代码的 r2(已被上方 R2 分支接管)。 - 测试 +2:驱动名归一(含 S3 家族不被吞掉的回归护栏)、直传能力三态判定。
该文件是本机部署配置(含真实 JWT_SECRET、KV namespace id 与 R2 绑定),与 .gitignore 中已有的 wrangler.local.jsonc / wrangler.d1.jsonc 属同类「本地专用配置、可能含真实密钥、禁止入库」条目。
feat(drivers): add Cloudflare R2 driver (Workers binding + S3 API)
Base:
main← Compare:feat/r2-storageCommit:
2e4e74a· Files changed: 8 (+1447 / −2)Open PR: https://github.com/OpenListTeam/OpenList-Worker/pull/new/feat/r2-storage
Summary / 摘要
Adds a dedicated Cloudflare R2 storage driver with two access paths,
binding-first with automatic fallback:
resolved from the request env (
r2_bucketsinwrangler.jsonc), the storage'sbucket_bindingfield names which binding to use.account_id(auto-derives the endpoint) or an explicitendpoint, plus access key / secret. Reuses the existing
S3Driverfor signing,listing and presigned links.
新增独立的 Cloudflare R2 存储驱动,两种接入方式,binding 优先、自动回退:
env(
wrangler.jsonc的r2_buckets),存储配置里的bucket_binding字段指定用哪个绑定。
account_id(自动推导 endpoint)或显式 endpoint,加 access key / secret;签名、列举与预签名直链复用现有
S3Driver。Why / 为什么:R2 在 Workers 上的首选用法是绑定直连(零密钥、免出网、可跨账号
共享桶),而本项目此前只能靠「S3 驱动 + 手填 r2 endpoint」曲线使用;Go 版
OpenList 有独立
r2驱动,本 PR 在 TS 版补齐并扩展出 binding 能力。用户可感知的行为变化
/r2)。wrangler.jsonc声明"r2_buckets": [{ "binding": "R2", "bucket_name": "your-bucket" }],再把存储的
bucket_binding填R2。不再需要创建 R2 API Token。custom_host(r2.dev 公开域名或自定义域名)即可。
仍然照原样工作(见下方「兼容性」)。
重要实现变化
src/backend/drivers/r2/(driver.ts719 行 /types.ts106 行 /r2.test.ts455 行)。op/storage.ts:r2/cloudflarer2/cfr2/r2native路由到R2Driver;紧跟新增驱动分支之后保留原有 S3 分支(
s3|doge|minio|ceph|aws|b2|cos|oss|kodo)。internal/model/db.ts:新增getEnvCtx(),把请求级 env 上下文暴露给驱动层(原先只有
setEnvCtx,仅内部使用)。server/admin.ts:新增 R2 表单(16 个字段);normalizeDriver让r2/cloudflarer2/cfr2归一到新的R2驱动(此前是S3)。wrangler.jsonc:新增「方案 E:R2 对象存储」注释块,含绑定声明示例与binding vs api 的行为差异说明。
配置 / 存储 / API 变化
新增可选配置:
wrangler.jsonc的r2_buckets(不配也能用 api 模式)。无破坏性变更:不涉及已有存储的
addition格式、不涉及数据库/缓存格式、不涉及对外 API 与 WebDAV/S3 网关行为。
唯一的行为差异在
normalizeDriver:新建存储时选r2别名现在会落到新R2 驱动,而不是旧 S3 驱动(详见「兼容性」)。
This PR has breaking changes.
/ 此 PR 包含破坏性变更。
This PR changes public API, config, storage format, or migration behavior.
/ 此 PR 修改了公开 API、配置、存储格式或迁移行为。
This PR requires corresponding changes in related repositories.
/ 此 PR 需要关联仓库同步修改。
Related repository PRs / 关联仓库 PR:
Related Issues / 关联 Issue
无直接 issue(新功能)。与 Go 版
drivers/r2对齐,并额外支持 Workers 绑定直连。Design / 设计说明
两条路径如何选择
bucket_binding(或mode=binding)R2Bucket直连custom_host→ 服务端流式转发;有 → 公开直链 302mode=autoS3Drivermode=binding但解析不到绑定auto是默认值:配了绑定就用绑定,没配就按 api 配置校验(缺bucket/endpoint/ 密钥时给出「缺哪一项、去哪里拿」的中文指引)。绑定名未填写时按
R2→R2_BUCKET→CF_R2→OPENLIST_R2顺序探测,env、请求级上下文、
globalThis三处分别查找(部分平台把绑定注入为全局标识符)。关键取舍一:为什么 binding 模式的下载是「服务端流式转发」
Cloudflare 的 Workers 绑定 API 不提供预签名 URL:
R2Bucket的方法集是head/get/put/delete/list/createMultipartUpload/resumeMultipartUpload,预签名 URL 属于 S3 接口能力(R2 文档中R2 → S3 → Presigned URLs单列一节)。因此本驱动的直链策略是:
custom_host→ 拼公开直链(r2.dev / 自定义域名),浏览器直连;S3Driver预签名直链;custom_host→raw_url返回空串,由server/raw.ts既有的
createReadStream分支流式回传(支持 Range)。顺带覆盖了一个协议差异:
server/raw.ts传下来的 Range 是 S3 语义的{ start, end }(闭区间),而 R2 的range是{ offset, length },驱动内做了换算。
关键取舍二:绑定在「每次操作」时解析,不缓存在驱动实例上
驱动实例是跨请求缓存的(
cacheKey = ${storageConfig.id}_${storageConfig.modified})。若把绑定对象或某个请求的
env存进实例,会让绑定随 isolate 生命周期滞留(以及在多账号/多 Worker 场景下指向错误的绑定)。因此驱动每次
list/get/put都从当前请求的上下文重新解析:getEnvCtx()(index.ts中间件已按请求写入)→
globalThis。这也让「存储保存流程拿不到 env」的情形自然降级而不污染缓存。
关键取舍三:
init()只告警、不抛错驱动实例可能在尚未注入请求 env 的时机被创建(后台
storage/load_all、保存存储时)。若此时因解析不到绑定而抛错,存储会被标成 error 甚至禁用。
因此
init()仅打印带修复指引的告警,真正的失败发生在list/get/put。目录语义
与 S3 驱动对齐:
delimiter="/"列目录 + 占位对象表示空目录(默认
.openlist)+root_folder_path前缀 + 递归copy/move/remove(R2 的delete支持批量数组,按 1000/批)。分页只依据
truncated判断是否继续(R2 文档明确警告:设置了include时单页返回条数可能少于
limit,不能拿条数推断),cursor原样回传续查。Compatibility / 兼容性
driver字段已是"S3"的历史存储:不受任何影响,仍然走 S3 驱动(endpoint 指向 R2 的用法继续有效)。
driver字段是"r2"的历史存储(非通过后台表单创建、手写导入的配置):现在会命中新的 R2 驱动。两者目录语义一致;若该存储原先只填了
endpoint + key + secret,api 模式可直接工作(region默认auto)。如需强制回到旧行为,改填
driver: "S3"即可。Testing / 测试
go test ./...— 本仓库为 TypeScript,不适用。已执行的自动化检查 / Automated checks(分支
feat/r2-storage@2e4e74a):npx tsc -p tsconfig.json --noEmitnpx tsx --test "src/backend/drivers/**/*.test.ts"npx tsx --test "src/backend/server/*.test.ts"npx tsx --test "src/backend/internal/model/*.test.ts"npx tsx --test src/backend/internal/model/store/store.test.tsnpx tsx --test "src/backend/internal/cache/*.test.ts"npx tsx --test "src/backend/pkg/*.test.ts"npx tsx --test src/backend/drivers/189/*.test.tsnpx tsx scripts/_regress.mjsnpx prettier --check "src/backend/drivers/r2/*.ts"New tests / 新增测试(9 例,
drivers/r2/r2.test.ts,内存版R2Bucketmock):normalizeR2Addition默认值与root_folder_path归一化;globalThis),且同名变量只是普通字符串时必须判为不可用;
pageSize=1时跟随cursor翻页、深层对象不泄漏到当前层;
root_folder_path前缀作用于list/get/put的桶内 key;mkdir写占位对象;remove区分文件(单 key)与目录(递归);rename/move/copy:文件 copy+delete、目录递归(复制不删源);createReadStream:无 Range 全量、有 Range 按offset/length切片;mode=auto回退 api(并断言 endpoint 由account_id推导)、mode=binding明确报错、无任何配置时给出「缺少 bucket」指引;getDriver路由:R2/r2/CloudflareR2/cfr2均命中R2Driver。供审查者手动验证 / Manual verification steps:
wrangler.jsonc声明r2_buckets→ 后台新建 R2 存储并填bucket_binding→ 浏览该挂载点,应能列出桶内对象且未配置任何 API Token。
/d...服务端流式(Response 带Accept-Ranges: bytes),拖拽进度条可正常 seek。.openlist占位对象)、上传、重命名、删除目录(对象全部移除)。
bucket_binding、填入 R2 API Token 三项,确认改用预签名直链 302。
未做真机/线上 R2 验证:本机无 R2 桶与 Workers 部署环境,因此 binding 侧的正确性
由内存 mock 与
tsc保证,真实桶上的行为需由审查者按上面 4 步确认。这是本 PR唯一未经真实环境验证的部分。
Checklist / 检查清单
/ 我已阅读 CONTRIBUTING。
/ 我确认此贡献符合仓库许可证、贡献规范和行为准则。
gofmt,go fmt, orprettierwhere applicable./ 我已按适用情况使用
gofmt、go fmt或prettier格式化变更代码。/ 我已在适用情况下请求相关维护者或代码所有者审查。
AI Disclosure / AI 使用声明
/ 此 PR 包含 AI 辅助内容。
Tools used / 使用工具:
Usage scope / 使用范围:
Code generation / 代码生成
Refactoring / 重构
Documentation / 文档
Tests / 测试
Translation / 翻译
Review assistance / 审查辅助(用 R2 官方文档核对绑定 API 能力边界)
I have reviewed and validated all AI-assisted content included in this PR.
/ 我已审核并验证此 PR 中的所有 AI 辅助内容。
I have ensured that all AI-assisted commits include
Co-Authored-Byattribution./ 我已确保所有 AI 辅助提交都包含
Co-Authored-By归属信息。I can reproduce all AI-assisted content included in this PR without any AI tools.
/ 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。
Operational Note / 运维提示
(R2 出网到 Worker 免费,但 Worker 需要处理请求)。大文件并发播放压力会比
api 模式(浏览器直连预签名 URL)高。生产上如果已有公开域名,建议填
custom_host让流量回到直连 302 路径。r2.dev公开域名有速率限制:适合测试,正式对外建议绑自定义域名。get_direct_upload_info)仅在 api 模式可用:binding 不提供 PUT预签名,调用时会得到明确报错而不是静默失败。
Scope / Follow-ups / 范围与后续
createMultipartUpload)与s3_presigned直传工具对 binding模式的适配;
env_check/ 存储「测试连接」按钮对 R2 的专门体检项(目前沿用check_status的通用列表探测);raw_url与base_path的拼接(与本 PR 无关);但各家绑定 API 差异需要分别对接。
Commits / 提交
2e4e74a—feat(drivers): add Cloudflare R2 driver (Workers binding + S3 API)(8 files changed, +1447 / −2)