Skip to content

feat(cache): add file-tree and download-link caches (DB by default) - #75

Merged
PIKACHUIM merged 8 commits into
OpenListTeam:mainfrom
Wudarensheng:feat/cache
Oct 15, 2026
Merged

PIKACHUIM merged 8 commits into
OpenListTeam:mainfrom
Wudarensheng:feat/cache

Conversation

@Wudarensheng

@Wudarensheng Wudarensheng commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

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.ts reference solves this with a two-level cache (file tree + download
links); this PR ports that design to this repository.

浏览目录和开始下载这两个动作,此前每次请求都会打到远端存储;其中「换链」
(driver.get() → 预签名 / 直链)通常是最贵、最容易被网盘限流的一步。
本 PR 参考上游 OpenList.ts 的实现,把它的两级缓存(文件树 + 下载链接)移植过来。

User-visible behavior / 用户可感知的变化

  • Directory listings are served from cache (default TTL 30 min, following the
    per-storage cache_expiration and custom_cache_policies). Empty directories
    are cached too, so repeated browsing stops hitting the remote drive.
    / 目录列表改为走缓存(默认 30 分钟,跟随存储级 cache_expiration 与
    custom_cache_policies);空目录同样会被缓存。
  • Download links returned by drivers are reused for a short window (default
    5 min), so repeated downloads / previews skip the link exchange.
    / 驱动换来的直链会在短时间内复用(默认 5 分钟),重复下载 / 预览不再重复换链。
  • 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. / 存储的新增 / 修改 / 启停 / 删除会清空该存储的全部缓存。
  • 4 new admin endpoints: GET /cache/status, POST /cache/clear,
    POST /storage/refresh, POST /storage/refresh_one.
    / 新增 4 个管理接口(含对齐参考实现的 refresh / refresh_one)。
  • GET /env_check now also reports the resolved cache config and the actual
    backends in use. / /env_check 额外回显生效的缓存配置与实际后端。

Implementation / 重要实现变化

  • New module 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).
  • Backend selection is entirely env-driven: CACHE_DRIVER=db (the default)
    resolves to the same backend as DB_DRIVER via getStorageBackend(), so
    zero configuration means "cache in the database".
  • Dedicated kv / blob / cfkv / do / memory backends must be opted into
    explicitly 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.
  • Cache entries are written through the driver's raw put / get / delete /
    list (deliberately not via saveDb(), which would trigger whole-config
    serialization, write guards and field encryption), under an isolated key prefix
    <CACHE_PREFIX>_<kind>_<storageId>_<encodedPath> (kind = ft / ln), so
    business-data keys are never touched. Paths longer than the EdgeOne KV key limit
    are folded with a double FNV-1a hash.
  • Safety boundary: only driver-layer results are cached (raw FileItem[] and
    raw links). Permissions, meta passwords, hide rules and signatures are still
    computed per request in server/fs.ts / server/raw.ts, so a cache hit cannot
    leak privileges across users. Any cache error degrades to a no-op and never
    fails the request.

Config / 配置 (all optional — defaults preserve "DB-only"):

Variable Default Meaning
CACHE_ENABLED true Master switch for both caches.
CACHE_DRIVER db Backend list, comma-separated: db / kv / blob / cfkv / do / memory / none.
CACHE_FILE_TREE true Toggle the file-tree cache.
CACHE_DOWNLOAD_LINK false Toggle the download-link cache. Off by default: some drives mint links with a very short TTL, so reuse could hand out an already-expired link. Enable once you confirmed your drive's link lifetime. / 默认关闭:部分网盘直链 TTL 极短,复用可能把失效链接发给用户,确认后再开启。
CACHE_TTL 0 File-tree TTL in minutes; 0 = follow per-storage cache_expiration (30).
CACHE_LINK_TTL 5 Download-link TTL in minutes.
CACHE_EXCLUDE_DRIVERS virtual,alias,url_tree,strm,chunk Drivers excluded from caching.
CACHE_PREFIX openlist_cache Key prefix isolating cache entries from business data (only [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 (or
    CACHE_DRIVER=none) to restore the previous behavior exactly; a single storage
    can opt out with cache_expiration=0 or a custom_cache_policies rule.
    / 缓存默认开启;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:

  • OpenList: — (no change required; the 4 new admin endpoints have no frontend UI yet, follow-up optional)
  • OpenList-Docs: to be opened — documents the 8 new CACHE_* variables and the cache admin endpoints

Testing / 测试

Platform: Windows 10/11, Node.js 22.22.2, tsx --test; plus wrangler dev (real
workerd 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 Workers
    port; no Go sources are changed.
  • Type check: npx tsc -p tsconfig.json --noEmit — 0 errors.
  • npm run test:cache — 24/24 pass
  • npm run test:model — 51/51 pass
  • npm run test:store — 12/12 pass
  • npm run test:pkg — 42/42 pass
  • npm run test:server — 190/190 pass
  • npm run test:drivers — 150/151 pass
  • npm run test:regress — 71/71 pass

test:drivers has exactly one failure: github_releases/util.test.ts →
"gh_proxy 已配置时为 http(s) 下载链接加代理前缀". It is pre-existing on
upstream/main
— a control run against a clean upstream/main extraction fails
identically, 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 dev with two real remote drives):

Check Result
Mount a real Lanzou share and list a directory 50 entries, ~250 ms (real network)
List the same directory again 35 ms, identical content — served from cache
Download-link cache (via the Github driver, which returns real download_urls) 1st fs/get 444 ms → 2nd 38 ms, same raw_url
Entries really land in the configured backend confirmed in Miniflare's persisted KV (_mf_entries contains openlist_cache_ft_*)
GET /api/admin/cache/status / POST /api/admin/cache/clear pass, but only with the KV fix below — see caveat

⚠️ Caveat on the two management endpoints. cache/status and cache/clear
only work once the pre-existing KV bug is fixed: on Cloudflare Workers KV the
driver's binding-mode list() implemented only the EdgeOne semantics
(keys[].key + complete), so it returns [] there and both endpoints
report/remove nothing. A fix for it was written during this work but has been
moved out to PR #127 (fix/125-kv-binding-list) to avoid duplicating it.
Until #127 lands, /cache/status, /cache/clear and /storage/refresh do not
work on Cloudflare Workers KV. This is pre-existing and does not affect cache
reads/writes — keys are written and hits are served correctly either way.

One defect was found by these live runs and is fixed on this branch:

  • fix(fs): getStorageRequestContext() returned undefined whenever
    executionCtx.waitUntil was unavailable, dropping env along with it. The cache
    then resolved its backend from env === undefined → the in-memory driver, while
    business data kept using the configured backend — cache and DB silently split
    across two backends. env is now always passed; only waitUntil degrades
    (awaited inline), with a one-time warning. Regression test added.

Merge note: the branch has been merged with upstream/main (merge commit
d1f8176), which resolved the earlier PR conflicts. The merge is additive on both
sides — the cache hooks live inside listItemsResolved, and upstream's singleflight
wrapper (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 / 检查清单

  • I have read CONTRIBUTING.
    / 我已阅读 CONTRIBUTING.
  • I confirm this contribution follows the repository license, contribution policy, and code of conduct.
    / 我确认此贡献符合仓库许可证、贡献规范和行为准则。
  • I have formatted the changed code with gofmt, go fmt, or prettier where applicable.
    / 我已按适用情况使用 gofmt、go fmt 或 prettier 格式化变更代码。
  • I have requested review from relevant maintainers or code owners where applicable.
    / 我已在适用情况下请求相关维护者或代码所有者审查。

AI Disclosure / AI 使用声明

  • This PR includes AI-assisted content.
    / 此 PR 包含 AI 辅助内容。

Tools used / 使用工具:

  • ChatGPT
  • Codex
  • GitHub Copilot
  • Claude
  • Gemini
  • Other (please specify) / 其他(请注明): WorkBuddy AI (DeepSeek-V4.1-Flash)

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-By attribution.
    / 我已确保所有 AI 辅助提交都包含 Co-Authored-By 归属信息。

  • I can reproduce all AI-assisted content included in this PR without any AI tools.
    / 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。

@pikachuren

This comment has been minimized.

@PIKACHUIM

Copy link
Copy Markdown
Member

这个改动比较大,由于有些网盘的直链TTL非常短
建议考虑一下使用独立的缓存时间选项控制(并且默认关闭

Wudarensheng and others added 4 commits October 2, 2026 16:54
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
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 指向的
真实后端(修复前该用例失败)。

@pikachuren pikachuren left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[OUTDATED] 此评审已过时,请参阅下方增量评审(v4)。


🙏 感谢 @Wudarensheng 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 GLM 模型进行分析。
⚠️ AI 分析结果仅供参考,可能存在误判或遗漏。如您发现任何问题或有不同意见,欢迎随时提出讨论和纠正。
⚠️ 重要提醒:即使 AI 评审认为代码质量良好且建议合并,最终是否合并仍需由项目维护者进行人工判定。

🔄 增量评审:上次评审 → 当前 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 碰撞修复到位、失效边界补齐,集成测试可后续补充。

@PIKACHUIM

Copy link
Copy Markdown
Member

代码没有明显问题但是不敢合,主要是怕影响已有稳定性
要不这个等多测试一下再合并吧?

在真实 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 翻页、空结果、游标不前进时的终止性,以及无绑定时不静默返回 []。
@Wudarensheng

Copy link
Copy Markdown
Member Author

@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 pikachuren left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙏 感谢 @Wudarensheng 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 Claude Sonnet 5 模型进行分析。
⚠️ AI 分析结果仅供参考,可能存在误判或遗漏。如您发现任何问题或有不同意见,欢迎随时提出讨论和纠正。
⚠️ 重要提醒:即使 AI 评审认为代码质量良好且建议合并,最终是否合并仍需由项目维护者进行人工判定。项目维护者会综合考虑代码质量、项目规划、技术方向、团队资源等多方面因素做出是否合并的决策。

🔄 增量评审: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),无新增阻塞问题。

@pikachuren

Copy link
Copy Markdown
Collaborator

AI 代码评审(补充意见)

基于对本 PR 关键源码的审查(cache/store.ts、cache/filetree.ts、op/storage.ts、server/raw.ts、server/fs.ts、server/admin.ts),补充几点意见,仅供参考:

结论:方向正确、代码质量不错,关键安全问题(缓存键碰撞)已修复;但改动落在下载/列表热路径上且默认开启,建议谨慎上线。

已确认做得好的地方

  • 单射缓存键编码(encodeCachePathSegment)正确修复了 /a/b 与 /ax2fb 碰撞导致跨目录/跨存储串数据的问题
  • 缓存读取发生在 canAccess 权限/密码校验之后,hide_pattern 每次重新过滤,不依赖缓存命中与否,不存在越权
  • 管理接口均挂在 adminRouter 的 checkAdminAuth 中间件下,鉴权完整
  • getStorageRequestContext 的 env 透传修复是真实 bug fix,避免缓存被静默解析到内存驱动

建议关注的风险

  1. CACHE_ENABLED 默认 true、CACHE_DRIVER 默认 db(与业务数据同库):合并后所有现网部署会立即开始往数据库/KV 写缓存,增加存储和读写成本,建议在 Release Note 中明确告知,或考虑先默认关闭观察一个版本周期。
  2. 没有主动过期清理机制,只有惰性删除(读到时才判断过期),长尾路径的缓存条目会一直占用存储空间,直到管理员手动调用 /cache/clear。
  3. 经典 cache-aside 时序竞争(TOCTOU):慢速回源与并发写请求交错时,可能把已失效的旧数据重新写回缓存,最坏情况下用户在 TTL(文件树默认 30 分钟)内看到删除/重命名后文件还在的现象,建议至少在文档中说明这是最终一致性,有余力可加简单版本校验缓解。
  4. 本次改动集中在 server/raw.ts/server/fs.ts,与 fix(authz): enforce current role, token revocation, S3 permissions #92(authz 加固)改动的是同一批文件,建议协调好合并顺序,避免冲突,并在合并后补一次交叉回归,确认缓存读取仍然在新的权限校验之后。

以上基于当前最新 commit 的代码审查,仅供参考,最终请以人工评审结论为准。

@PIKACHUIM
PIKACHUIM merged commit bb2db24 into OpenListTeam:main Oct 15, 2026
1 check passed
@PIKACHUIM

Copy link
Copy Markdown
Member

@Wudarensheng 感谢您的贡献,本PR先合并

针对AI给出的风险建议请关注,必要时另外提PR优化修复

AI 代码评审(补充意见)

基于对本 PR 关键源码的审查(cache/store.ts、cache/filetree.ts、op/storage.ts、server/raw.ts、server/fs.ts、server/admin.ts),补充几点意见,仅供参考:

结论:方向正确、代码质量不错,关键安全问题(缓存键碰撞)已修复;但改动落在下载/列表热路径上且默认开启,建议谨慎上线。

已确认做得好的地方

  • 单射缓存键编码(encodeCachePathSegment)正确修复了 /a/b 与 /ax2fb 碰撞导致跨目录/跨存储串数据的问题
  • 缓存读取发生在 canAccess 权限/密码校验之后,hide_pattern 每次重新过滤,不依赖缓存命中与否,不存在越权
  • 管理接口均挂在 adminRouter 的 checkAdminAuth 中间件下,鉴权完整
  • getStorageRequestContext 的 env 透传修复是真实 bug fix,避免缓存被静默解析到内存驱动

建议关注的风险

  1. CACHE_ENABLED 默认 true、CACHE_DRIVER 默认 db(与业务数据同库):合并后所有现网部署会立即开始往数据库/KV 写缓存,增加存储和读写成本,建议在 Release Note 中明确告知,或考虑先默认关闭观察一个版本周期。
  2. 没有主动过期清理机制,只有惰性删除(读到时才判断过期),长尾路径的缓存条目会一直占用存储空间,直到管理员手动调用 /cache/clear。
  3. 经典 cache-aside 时序竞争(TOCTOU):慢速回源与并发写请求交错时,可能把已失效的旧数据重新写回缓存,最坏情况下用户在 TTL(文件树默认 30 分钟)内看到删除/重命名后文件还在的现象,建议至少在文档中说明这是最终一致性,有余力可加简单版本校验缓解。
  4. 本次改动集中在 server/raw.ts/server/fs.ts,与 fix(authz): enforce current role, token revocation, S3 permissions #92(authz 加固)改动的是同一批文件,建议协调好合并顺序,避免冲突,并在合并后补一次交叉回归,确认缓存读取仍然在新的权限校验之后。

以上基于当前最新 commit 的代码审查,仅供参考,最终请以人工评审结论为准。

PIKACHUIM pushed a commit that referenced this pull request Oct 15, 2026
…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 项)。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants