Repository navigation
feat(cache): add file-tree and download-link caches (DB by default) - #75
Conversation
This comment has been minimized.
This comment has been minimized.
|
这个改动比较大,由于有些网盘的直链TTL非常短 |
Implement the two-level cache from the OpenList.ts reference: - File-tree cache (`ft`): directory listings are served from cache, keyed by storage + virtual path. Empty directories are cached too, so repeated browsing no longer hits the remote drive every time. - Download-link cache (`ln`): the raw_url returned by the driver is reused instead of being re-signed/re-exchanged on every download, which is usually the most expensive and most rate-limited step. Caching is DB-only by default. `CACHE_DRIVER=db` (the default) reuses the backend resolved by `getStorageBackend()`, i.e. the same one as `DB_DRIVER`, so zero configuration means "cache in the database". Dedicated KV / Blob / cfkv / do / memory backends must be opted into explicitly via `CACHE_DRIVER` (e.g. `db,kv`, `kv`, `blob`) and are never auto-detected or auto-substituted, matching the existing DB_DRIVER rule that an explicitly configured driver never falls back. Safety boundary: only driver-layer results are cached (raw FileItem lists and raw links). Permissions, meta passwords, hide rules and signatures are still computed per request in server/fs.ts and server/raw.ts, so a cache hit cannot leak privileges. Any cache error degrades to a no-op and never breaks the request. Invalidation: writes (mkdir/rename/remove/move/copy/put) invalidate the affected path plus its parent directory for the file tree, and the path itself for links. Storage create/update/enable/disable/delete clears that storage's cache entirely. New environment variables (all optional): CACHE_ENABLED, CACHE_DRIVER, CACHE_FILE_TREE, CACHE_DOWNLOAD_LINK, CACHE_TTL, CACHE_LINK_TTL, CACHE_EXCLUDE_DRIVERS, CACHE_PREFIX. New admin endpoints: GET /cache/status, POST /cache/clear, POST /storage/refresh, POST /storage/refresh_one.
The local assistant keeps its project memory under .workbuddy-ai/, which must never be committed. Ignore it next to the existing .codebuddy entry.
- encodeCachePathSegment 改为单射编码:转义前缀 x 本身转义为 x78, 消除 /a/b 与 /ax2fb 键碰撞(评审实测复现的跨目录串列表/直链问题); 超长路径折叠标记用 xg + 双 FNV-1a,xg 不可能出现在普通编码结果中, 折叠键与普通键不会互相碰撞;补参数化「任意不同路径键必不相等」回归 - 补缓存失效:/fs/upload/complete、/fs/multipart/complete、/fs/other、 /fs/get_direct_upload_info(含 other 回退分支)写入/签发直传 URL 后 调用 invalidatePaths,避免「上传成功但刷新看不到」 - CACHE_PREFIX 字符集校验:仅允许 [A-Za-z0-9_](KV 键约束),非法回退 默认值并告警,防止与业务键空间 / 整表 DELETE 冲突 - CACHE_DOWNLOAD_LINK 默认改为 false(维护者建议):部分网盘直链 TTL 极短,缓存复用易把失效直链发给用户;CACHE_LINK_TTL 保持独立选项, 同步 .env.example / .dev.vars.example / wrangler.jsonc / README
fecc76a to
218669d
Compare
getStorageRequestContext() 原先在缺少 executionCtx.waitUntil 时直接返回 undefined,把 env 一并丢掉。缓存(CACHE_DRIVER=db)与 singleflight (SINGLEFLIGHT=db)都靠它解析后端:拿不到 env 时 getStorageBackend(undefined) 会解析成内存驱动,而业务数据仍走全局 env ctx 的持久化后端 —— 缓存与数据库 被静默拆到两个后端。 表现为「列表看似命中缓存(内容陈旧),但持久层里一条缓存键都没有、 /cache/status 计数恒为 0、/cache/clear 永远 removed=0」,排查成本很高。 真正依赖 executionCtx 的只有 waitUntil,缺失时降级为同步等待 (scheduleStoragePersistence 已支持该路径),业务语义不变;另加一条 warn-once 提示该降级,避免同类问题再次静默。 新增回归测试 src/backend/server/storage_request_context.test.ts:刻意不传 app.request() 的 executionCtx,断言缓存键必须落到 CACHE_DRIVER=db 指向的 真实后端(修复前该用例失败)。
There was a problem hiding this comment.
[OUTDATED] 此评审已过时,请参阅下方增量评审(v4)。
🙏 感谢 @Wudarensheng 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 GLM 模型进行分析。
🔄 增量评审:上次评审 → 当前 head(218669d)
新增提交 fix(cache): 缓存键单射编码、补上传失效、前缀校验、链接缓存默认关闭,逐条回查如下。
新增改动的问题(按 P0/P1/P2 分级):
- 💡 [P2] 超长路径折叠键(编码 >180 时取前 100 字符 +
xg+ 双 FNV-1a)在两个超长路径之间依赖 64 位哈希区分,理论上仍可碰撞——概率层面可接受,建议在注释中保留此已知限制(当前注释已写明,确认即可)。 - 💡 [P2]
storageIdFromCacheKey按第一个_切分 storage id,而encodeKeyPart会原样保留_;若 storage id 本身含_会切错。实际 id 为自增数字,当前无触发路径,属防御性提醒。
本轮补充实测:用真实依赖运行 internal/cache/cache.test.ts 24/24 通过;并把上一轮评审中 5/5 碰撞的路径对(/a/b ↔ /ax2fb 等)在新 head 代码下用真实 buildCacheKey 重跑,0/5 碰撞——键单射修复经真实执行验证成立。
旧问题解决情况:
- ✅ 缓存键碰撞(P0,实测复现)→ 已修复。
isCachePlain不再原样保留小写x(x→x78),CACHE_PLAIN_RE同步排除x,/a/b与/ax2fb不再同键;长键折叠用xg标记(普通编码中x后必跟两位十六进制,xg不可能出现),短键与长键不会互撞。原评审建议的参数化断言(任意两路径键不相等)建议补上。 - ✅
/fs/upload/complete、/fs/multipart/complete、/fs/other未失效缓存 → 已修复(fs.ts:1041、1625、1128、1368 直传 URL 预失效)。 - ✅
CACHE_PREFIX未校验字符集 → 已修复,parseCachePrefix只允许[A-Za-z0-9_](≤64 字符),非法回退默认值并告警。 - ✅ 链接缓存 TTL 过短风险(评论区维护者建议)→
CACHE_DOWNLOAD_LINK默认改为false,并文档注明「确认网盘直链有效期后再显式开启」。 - ❌ 测试仍集中在 store 层假驱动,
resolveDrivers的 db 解析、op 层命中/失效集成无用例 → 未补。
🎯 结论:✅ Approve — P0 碰撞修复到位、失效边界补齐,集成测试可后续补充。
|
代码没有明显问题但是不敢合,主要是怕影响已有稳定性 |
在真实 Cloudflare Workers 上跑起来才暴露:binding 模式的 list() 只按 EdgeOne 语义解析(keys[].key + page.complete),而 Cloudflare KV 返回的是 keys[].name + list_complete + cursor ⇒ 一条都取不到,且首轮就被判为 complete,恒返回 []。 缓存模块的 cacheListKeys() 依赖本方法,于是「缓存统计恒为 0、 清空缓存 / 刷新文件树永远 removed=0」在 Cloudflare Workers 上静默失效 —— 而键其实早已写进 KV(实测:列目录 271ms → 命中缓存 36ms,KV 中确实存在 openlist_cache_ft_* 键)。 改为两套语义都支持:键名取 item.key ?? item.name;翻页终止看 list_complete || complete;下一页游标优先用 page.cursor,其次取本页最后一个 键;游标无进展即退出,避免语义不符时死循环;并用 Set 去重。 新增 src/backend/internal/model/store/driver/kv_list.test.ts,覆盖两套语义、 cursor 翻页、空结果、游标不前进时的终止性,以及无绑定时不静默返回 []。
|
@PIKACHUIM 实机测试过了 |
解决 PR OpenListTeam#75 的冲突(上游 main 从 bfb963e 前进到 89b7345)。 冲突全部是「两边在同一位置各自新增」,处理方式是两边都保留: - .dev.vars.example / .env.example / wrangler.jsonc: CACHE_* 块与 SINGLEFLIGHT 块并列保留(注释与变量,互不影响)。 - package.json:cloudflare.bindings 同时保留 CACHE_* 与 SINGLEFLIGHT; scripts 同时保留 test:cache 与 test:pkg,test:all 串上两者。 - src/backend/internal/op/storage.ts:toCacheTarget() 与 fsSingleflightKey() 两个新增函数都保留(冲突块共享了文档注释开头与函数闭合大括号,已补回)。 其余文件由 git 自动合并,其中 listItems 的合并结果正好是理想形态: upstream 的单飞包裹(listItems → singleflight → listItemsResolved) 与缓存逻辑(listItemsResolved 内的读缓存 / 回源 / 写缓存)自然共存。 验证: - tsc --noEmit 干净(上游已修掉此前的既有错误) - cache 24/24、model 58/58、server 190/190、pkg 42/42、 store 12/12、op 9/9、_regress 71/71 - drivers 150/151:唯一失败是 upstream/main 自带的 github_releases/util.test.ts(纯上游提取下同样失败,与本次合并无关) - 端到端功能:fn-cache 28/28、fn-core 25/25
同一问题已由 PR OpenListTeam#127 (fix/125-kv-binding-list) 以更完整的方式修复:除 binding list 兼容 Cloudflare 语义外,还收敛了 format/key.ts 的 list 配额消耗,并加了 重复页检测。为避免两个 PR 重复修改 driver/kv.ts 并互相冲突,这里把该修复从本 PR 移除,交由 OpenListTeam#127 负责。 本 PR 因此只专注缓存功能。注意:在 OpenListTeam#127 合入之前,Cloudflare Workers 上缓存的 统计(GET /api/admin/cache/status)与清空(POST /api/admin/cache/clear、 POST /api/admin/storage/refresh)仍受该既有缺陷影响 —— 键能正常写入与命中, 但列举不到,属于既有问题,非本 PR 引入。 This reverts commit fb96e65.
pikachuren
left a comment
There was a problem hiding this comment.
🙏 感谢 @Wudarensheng 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 Claude Sonnet 5 模型进行分析。
🔄 增量评审:v3 → v4(上次评审 218669d → 当前 head 9e973dd)
本轮新增 4 个 commits:
| commit | 内容 |
|---|---|
8099fe40 |
fix(fs): getStorageRequestContext() 始终传递 env,修复缓存静默换后端 |
fb96e659 |
fix(kv): list() 同时支持 Cloudflare 与 EdgeOne 分页语义 |
d1f8176e |
Merge upstream/main into feat/cache |
9e973dd2 |
Revert "fix(kv): list() 同时支持 Cloudflare 与 EdgeOne 的分页语义" |
新增改动的问题(按 P0/P1/P2 分级):
无 P0/P1 问题。
- 💡 [P2]
server/fs.ts—warnedMissingExecutionCtx是模块级单例变量,在 Node 测试进程中只触发一次 warn(Worker 冷启动会重置,生产无影响)。若将来有测试断言 warn 日志出现,需注意用例顺序依赖。当前测试不依赖此行为,无实际影响,记录即可。
旧问题解决情况(逐条对应上次评审):
- ✅
getStorageRequestContext()丢失env导致缓存静默切换到内存后端 → 本轮修复(8099fe40):重构函数使env始终传递,waitUntil缺失仅打一次 warn 并降级为同步等待,逻辑正确。配套新增storage_request_context.test.ts(156 行),精确覆盖"缺少executionCtx时缓存写入配置后端(KV)"场景,断言缓存键落入正确后端、信封格式正确、/cache/status计数 ≥ 1,回归覆盖到位。 - ✅
fb96e659/9e973dd2— KV list() 双语义修复已引入后又被 revert,移到 PR #127 单独处理。revert 干净,仅撤回 fb96e65 新增的代码,未影响 merge commit 带入的上游 kv.ts 改动(+21/-2)。逻辑链路清晰,无残留问题。 - ✅
d1f8176e— merge upstream/main:上游新增了 singleflight(pkg/singleflight.ts)、Yun139 token 刷新回调、cnb_releases / github_releases driver 增强等。与缓存模块的交叉点是op/storage.ts,singleflight 包裹的是listItemsResolved的上层 (listItems),而缓存 hook 在listItemsResolved内部,两者互不干扰,符合 PR 描述。 - ⏳ 集成测试缺失(旧 ❌ 部分解决)→
storage_request_context.test.ts补充了 env 透传的端到端场景(含/cache/status接口验证),但resolveDrivers多驱动组合解析、op 层失效路径集成仍无用例。对合并无阻碍,可后续补充。
🎯 结论:✅ Approve — 新增 commits 均为修复性改动,env 透传缺陷修复正确且有充分回归测试,CI 全绿,KV 双语义修复已合规移出(#127),无新增阻塞问题。
AI 代码评审(补充意见)基于对本 PR 关键源码的审查( 结论:方向正确、代码质量不错,关键安全问题(缓存键碰撞)已修复;但改动落在下载/列表热路径上且默认开启,建议谨慎上线。 已确认做得好的地方
建议关注的风险
以上基于当前最新 commit 的代码审查,仅供参考,最终请以人工评审结论为准。 |
|
@Wudarensheng 感谢您的贡献,本PR先合并 针对AI给出的风险建议请关注,必要时另外提PR优化修复
|
…th-isolation 冲突仅 src/backend/server/fs.ts 一处(来自 #75 文件树/链接缓存),解决口径: 1) /fs/upload/{create,part,complete}:保留本分支的删除(按评审决定,分片只保留官方 /fs/multipart/*)。main 在该端点内新增的 invalidatePaths 随之移除;其余缓存失效点全部保留,写操作的失效集中在 internal/op/storage.ts(6 处),不受影响。 2) /fs/get_direct_upload_info:main 新增的预失效代码引用 reqPath/file_name,而本分支已把校验后的目录/文件名归一化为 dirPath/dstPath(沿用 main 的内联拼接会指向未归一化路径)。改为 invalidatePaths([dirPath, dstPath]),语义等价且沿用已校验路径。 缓存隔离性核对(不削弱本分支的安全不变量): - 缓存键为 (storage.id, 存储内相对路径),与用户无关,不缓存任何按用户计算的数据; - 所有缓存读取都在鉴权/ACL 之后:/fs/* 的 meta 判定先于 listItems/getItem,WebDAV/S3 的 meta 判定同样先于存储调用,/d、/p 的链接缓存(raw.ts)在验签之后; - meta.hide 过滤在 route 层逐项执行,缓存不改变「按请求过滤」的语义。 验证:tsc 0 error;server 197 / cache 24 / model 39 / pkg 42 / store 12 / drivers 151 全绿(共 465 项)。
feat(cache): add file-tree and download-link caches (DB by default)
Summary / 摘要
Browsing a directory and starting a download both hit the remote drive on every
single request. The download-link exchange (
driver.get()→ presigned / raw URL)is usually the most expensive and most rate-limited step of the two. The upstream
OpenList.tsreference solves this with a two-level cache (file tree + downloadlinks); this PR ports that design to this repository.
浏览目录和开始下载这两个动作,此前每次请求都会打到远端存储;其中「换链」
(
driver.get()→ 预签名 / 直链)通常是最贵、最容易被网盘限流的一步。本 PR 参考上游
OpenList.ts的实现,把它的两级缓存(文件树 + 下载链接)移植过来。User-visible behavior / 用户可感知的变化
per-storage
cache_expirationandcustom_cache_policies). Empty directoriesare cached too, so repeated browsing stops hitting the remote drive.
/ 目录列表改为走缓存(默认 30 分钟,跟随存储级
cache_expiration与custom_cache_policies);空目录同样会被缓存。5 min), so repeated downloads / previews skip the link exchange.
/ 驱动换来的直链会在短时间内复用(默认 5 分钟),重复下载 / 预览不再重复换链。
mkdir/rename/remove/move/copy/put) invalidate theaffected path plus its parent directory for the file tree, and the path
itself for links. / 写操作后自动失效:文件树连带父目录,链接只失效自身。
entirely. / 存储的新增 / 修改 / 启停 / 删除会清空该存储的全部缓存。
GET /cache/status,POST /cache/clear,POST /storage/refresh,POST /storage/refresh_one./ 新增 4 个管理接口(含对齐参考实现的 refresh / refresh_one)。
GET /env_checknow also reports the resolved cache config and the actualbackends in use. /
/env_check额外回显生效的缓存配置与实际后端。Implementation / 重要实现变化
src/backend/internal/cache/—config.ts(env switches),store.ts(backend resolution + key encoding + envelope get/set/list/clear),filetree.ts,link.ts,index.ts(barrel + invalidation orchestration),plus
cache.test.ts(24 cases).CACHE_DRIVER=db(the default)resolves to the same backend as
DB_DRIVERviagetStorageBackend(), sozero configuration means "cache in the database".
kv/blob/cfkv/do/memorybackends must be opted intoexplicitly and are never auto-detected or auto-substituted — consistent with
the existing rule that an explicitly configured driver never falls back. An
unavailable backend is skipped with a one-time warning; if none are available
the cache silently degrades to a no-op.
put/get/delete/list(deliberately not viasaveDb(), which would trigger whole-configserialization, write guards and field encryption), under an isolated key prefix
<CACHE_PREFIX>_<kind>_<storageId>_<encodedPath>(kind=ft/ln), sobusiness-data keys are never touched. Paths longer than the EdgeOne KV key limit
are folded with a double FNV-1a hash.
FileItem[]andraw links). Permissions, meta passwords, hide rules and signatures are still
computed per request in
server/fs.ts/server/raw.ts, so a cache hit cannotleak privileges across users. Any cache error degrades to a no-op and never
fails the request.
Config / 配置 (all optional — defaults preserve "DB-only"):
CACHE_ENABLEDtrueCACHE_DRIVERdbdb/kv/blob/cfkv/do/memory/none.CACHE_FILE_TREEtrueCACHE_DOWNLOAD_LINKfalseCACHE_TTL00= follow per-storagecache_expiration(30).CACHE_LINK_TTL5CACHE_EXCLUDE_DRIVERSvirtual,alias,url_tree,strm,chunkCACHE_PREFIXopenlist_cache[A-Za-z0-9_], max 64 chars).Compatibility / 兼容性
Purely additive: no existing endpoint, config key, storage format or
migration path is modified.
Caching is enabled by default. Set
CACHE_ENABLED=false(orCACHE_DRIVER=none) to restore the previous behavior exactly; a single storagecan opt out with
cache_expiration=0or acustom_cache_policiesrule./ 缓存默认开启;
CACHE_ENABLED=false(或CACHE_DRIVER=none)可完全恢复原行为,单个存储也可用
cache_expiration=0退出。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:
CACHE_*variables and the cache admin endpointsTesting / 测试
Platform: Windows 10/11, Node.js 22.22.2,
tsx --test; pluswrangler dev(realworkerd runtime + real Cloudflare KV / D1 bindings) for the end-to-end runs.
Unit / integration suites (measured after merging
upstream/main):go test ./...— N/A: this repository is the TypeScript / Cloudflare Workersport; no Go sources are changed.
npx tsc -p tsconfig.json --noEmit— 0 errors.npm run test:cache— 24/24 passnpm run test:model— 51/51 passnpm run test:store— 12/12 passnpm run test:pkg— 42/42 passnpm run test:server— 190/190 passnpm run test:drivers— 150/151 passnpm run test:regress— 71/71 passtest:drivershas exactly one failure:github_releases/util.test.ts→"gh_proxy 已配置时为 http(s) 下载链接加代理前缀". It is pre-existing on
upstream/main— a control run against a cleanupstream/mainextraction failsidentically, and this PR does not touch that driver.
Live / manual testing — performed (this section previously said "not performed";
the runs below were done on a local
wrangler devwith two real remote drives):Githubdriver, which returns realdownload_urls)fs/get444 ms → 2nd 38 ms, sameraw_url_mf_entriescontainsopenlist_cache_ft_*)GET /api/admin/cache/status/POST /api/admin/cache/clearOne defect was found by these live runs and is fixed on this branch:
fix(fs):getStorageRequestContext()returnedundefinedwheneverexecutionCtx.waitUntilwas unavailable, droppingenvalong with it. The cachethen resolved its backend from
env === undefined→ the in-memory driver, whilebusiness data kept using the configured backend — cache and DB silently split
across two backends.
envis now always passed; onlywaitUntildegrades(awaited inline), with a one-time warning. Regression test added.
Merge note: the branch has been merged with
upstream/main(merge commitd1f8176), which resolved the earlier PR conflicts. The merge is additive on bothsides — the cache hooks live inside
listItemsResolved, and upstream's singleflightwrapper (
listItems→singleflight→listItemsResolved) sits in front of them,so the two features coexist without touching each other's logic. A follow-up commit
(
9e973dd) reverts the earlier KV fix so that PR #127 owns it.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 / 审查辅助
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 辅助内容。