阿鹏云盘 · 功能演进图鉴

worker.js —— 从「上游原版」到最新版,新增了什么、修了什么、怎么实现的。
时间跨度 2026-08-28 → 2026-09-27(31 天)· 53 份版本副本 · 最近更新 2026-09-27

代码行数
12,587 行
原版 7,187 · +75.1%
源文件体积
715 KB
原版 280 KB · +155%
API 路由
61 个
原版 38 · +23
页面路由
12 个
原版 5 · +7
函数声明
464 个
原版 286 · +178
部署产物
724 KiB
gzip 252.7 KiB ≈ 8.2% 额度
原版 96.7% 的代码一字未改。改动形态是「在原有骨架上逐层叠加」,不是重写 —— 所以上游出修复时相对容易手工合回来; 代价是新增代码必须顺着原版的写法(比如原版不用模板字符串的地方新增代码也别乱用),否则风格分裂、更容易踩转义坑。

一、功能总表

原版 → 2026-09-27 · 共 0 项
引入类型功能 / 改动说明
08-28新增回收站(软删除)trash: 前缀原版删除=永久消失。删前把整棵子树快照到 trash:,R2 实体不立刻删;恢复=反向搬回
08-28新增访客协作 /shared 可收件把只读分享变成「可收作业」:口令验证 + 直传 + 分片 + 建文件夹 + 配额
08-28加固节点 API 密钥回退去掉「没配 token 就用管理员密码」这条密码泄漏通道,改为直接关闭节点 API
08-28加固SSRF 防护存储节点 URL 原可填内网地址(如 169.254.169.254)→ 只允许公网 http/https
08-28加固登录爆破限流登录接口无限速 → 可跑字典。加失败计数 + 锁定时长
08-28修复孤儿分片清理误删清理任务会把正在上传中的分片当垃圾删掉 → 加「进行中」判定
08-28移除全局页脚原作者写在全局外壳里 → 登录/主页/分享页全带着。只保留 /shared 页底部一行免责声明
08-30新增PWA 支持manifest + Service Worker + 多尺寸图标,可「添加到主屏幕」全屏打开
09-01修复分片上传断点续传原版断网=从头再来。resumeKey 找回会话,新增 /api/multipart/status
09-05修复401 守卫白名单原版 bug:/s/xxx 与 /shared 也被踢去登录页 → 分享功能等于失效
09-06新增版本历史覆盖前把旧实体快照成新 R2 key,一文件一条版本索引。与回收站构成「双保险」
09-08新增在线预览五件套PDF / Word / Excel / PPT / 音视频。单版体量从 388 KB 跳到 608 KB,提升最大的一块
09-08新增可折叠侧边栏原版只有顶部应用栏,窄屏/手机内容挤成一团;移动端默认收起
09-08界面页面内弹窗替代原生15 处 alert/confirm/prompt 换成 Material 风格弹窗
09-09优化回收站列表并发读Promise.allSettled 并发化,治「回收站点开要等很久」
09-10优化目录改名 / 移动批量化原版最严重缺陷:O(N²) 子请求超限被强杀 → 留下新旧两份重复索引。子请求 ≈500 → ≈7
09-10修复云端改名一条龙把「三步操作」压成双击一次;兼容「云端已改过名」的识别 + 跳过 + 只补本地
09-11修复list-all 的 modified 取值用错字段 → 改名会把整棵树判「云端较新」→ 同步重下约 500 MB。改为内容上传时间优先
09-12修复孤儿清理漏扫(严重)只扫文件前缀 → 点「清理孤儿」会把回收站 + 所有历史版本判为孤儿,勾选即永久丢失
09-12优化回收站 N+1 → 集合查询listRaw() 一条 SQL 取回全部键值:89 条实测 0.95 ms vs 旧写法 ≈18 ms
09-12新增回收站「清理 N 天前」手动按钮而非 cron:量级小、可见可控,避免定时任务误删
09-12修复PWA 图标溢出真根因是 manifest 的 purpose:'maskable' 一行 → 删掉即好
09-14修复全局搜索(三轮大修)修 formatDate is not defined、修匹配数翻倍、结果含文件夹、输入即搜、面板可滚动
09-14新增搜索只按名称匹配刻意取舍:结果更精准(「必修一」不再被整棵子树淹没),代价是命中文件夹名的文件搜不到
09-15新增存储用量口径改造口径换成「存储桶物理占用」+ 占用构成 + 月均快照,一眼看出会不会超 R2 免费额度
09-17加固预览库同源托管5 个库原全走 jsdelivr(可用性 + 供应链双风险)→ 挪到 R2 system/lib/,CSP 得以收紧为纯 'self'
09-17加固Office 预览 XSS 清洗全站唯一真正的「数据派生 XSS」入口(Office 解析结果直接 innerHTML,输入是外来文件)→ 白名单清洗
09-17加固长期会话(删掉存密码)原来把密码 base64 存 localStorage = 明文。改为服务端签 30 天令牌 session:v3
09-17加固安全响应头 + CSPhtmlResponse() 收口全部 9 处 HTML 出口;不能被 iframe 嵌套 / 不能被 object 借道
09-17加固分享令牌密钥收紧原来拿公开的 share.id 当签名密钥 → 可伪造任意分享访问令牌
09-17修复大文件没有版本历史>512 KB 的文件优先走分布式,而分布式路径没有快照调用点 → 版本列表恒为空(教学 PPT 全在这区间)
09-17修复手机换图「读取失败」三条路(FileReader / arrayBuffer / objectURL)→ 两条路兜底 + 按字节魔法数判型 + 失败三分诊
09-17修复用量分类瑕疵system/lib/ 3.4 MB 原被算进「云盘文件」→ 并入站点资源(总量不变,只是分类修正)
09-18新增「从云盘选图」安卓安装版 PWA 读不到选图器的 content:// 引用 → 绕开选图器,直接从云盘取字节
09-18界面失败提示改「先给结论」不再摆出两个猜想让用户挑,直接给唯一动作「请改用『从云盘选图』」
09-19界面手机列表时间列可见旧表格自动布局超宽 → 时间列与操作列被挤到屏外。窄屏改固定布局 + 时间双格式
09-19界面共享文件夹改名 shared → 共享存储名跟着改,公开网址 /shared 永不改(发给学生的入口)
09-20界面表头「时间」竖排图标字体 flex-shrink:0 逼文字让步 → 文字锁死、让箭头让步
09-22界面回收站窄屏对齐主界面错位套用了主界面的 nth-child 列序规则 → 改用更高特异性选择器逐列纠正
09-22新增回收站「时间 ⇄ 大小」切换360px 放不下四列 → 表头加 ↔ 小按钮,箭头永远指向「点了会变成什么」
09-22界面641–720px 固定布局真凶是共享的 .file-row-name{max-width:400px} 把名称列 min-content 钉在 458px
09-22界面主列表 641–1110px 固定布局主网盘/共享/分享三张表同被钉住 → 溢出又被 overflow-x:hidden 静默裁掉
09-22界面侧栏抽屉断点 768 → 1000px治「769–1000px 名称列只剩 4 个字」;顺手修掉抽屉遮罩残留挡住整页的老 bug
09-22界面回收站 721–1110px 固定布局auto 布局把整页撑宽,右端被视口裁掉(时间显示成 2026/09/2)
09-26新增上传进度显示实时速度EMA 0.65/0.35 平滑,接入 5 处上传;顺手合并下载端内联的同一算法
09-26新增分享链接自定义后缀可起一个好记的后缀;没照抄上游——本项目 KV 是 upsert,没有原子性,另写了 putIfAbsent()
09-26新增复制下载直链免密文件直接给出直链(/api/share-download 路由早就存在,只是一直没暴露给用户)
09-26新增文本文件在线编辑新增 /api/text-file:ETag + If-Match 乐观锁(428 / 409 / 413),写回前留版本回滚点
09-27修复版本历史弹窗打不开浏览器端调用了服务端专属函数 → 红字「加载失败」。从 09-06 上线起坏了 21 天,第一次真点开才暴露
09-17移除版本更新检查页面加载时去 GitHub 拉 version.json → 国内常被墙,拖慢首屏。删掉后每页少一个外部请求
09-17移除下载 inline 死参数全项目搜不到任何 inline=1 生成点 → 删除后下载恒 attachment
09-26放弃上游 Hollow 毛玻璃皮肤 + 自定义壁纸上游把接口 GET 放在鉴权之前(猜到路径就能拉到壁纸);皮肤还会覆盖已调好的四套布局
09-26暂缓分享管理面板 / 分享页预览 / 全选勾选不是不做,是排在三件高感知功能之后。其中的「请求竞态序列号」值得单独抄

二、重要更新(按时间倒序,最新在上)

每一次重要改动:改了什么 · 为什么 · 怎么实现 · 踩过的坑
2026-09-27 修复Version f96b08e8

版本历史弹窗永远打不开(jsAttr is not defined)

点网页端「历史版本」→ 弹红字「加载失败:jsAttr is not defined」。

  • 根因:loadVersions() 跑在浏览器里,却调用了只存在于服务端的 jsAttr() / jsString() → 一执行就 ReferenceError,被外层 catch 吞成一句话。
  • 为什么藏了 21 天:没有历史版本时函数会提前 return,只有真存在历史版本才会走到那一行。「一直没用过」→ 第一次点开才暴露。而 node --check、部署前自检、产物校验全都抓不到(语法合法、特征字符串也还在)。
  • 顺带揪出第二个 bug:原来传 encodeURIComponent(path),而服务端 /api/versions/restore 拿到的是原样字符串(不 decode)→ 即使不报错,中文路径恢复必然 404。
  • 修法:浏览器端自备 attrJs()(只做 HTML 实体转义,别在它里面再包引号——包了点击只求值不调用);参数改用 JSON.stringify() 生成。两处一起修。
  • 加防线:worker部署前自检.mjs 新增第 4 项「内联脚本误用服务端函数」,判据自动推导,可反向对照(临时副本退回旧写法 → 精准报出 jsAttr();正常源码 0 误报)。
  • 验证:jsdom 真构建 → 真跑页面 → 真点一次「恢复」 → 断言真的发出 /api/versions/restore,15/15 全过。教训:只验 new Function(onclick) 语法会假绿(多包一层引号也能过)。
2026-09-26 新增Version 678aec3c

上游 v1.2.1 三项移植:上传网速 · 分享后缀 · 文本在线编辑

上游 09-15 发布 v1.2.1(跳过 1.2.0,共 102 个差异块 / +689 −61 行)。挑了三件用户可感知度最高的落地。

  • ① 上传进度显示实时速度 —— 新增 formatTransferStats() + trackTransferSpeed()(EMA 0.65 / 0.35,与本项目下载端系数一模一样)。接入 5 处:直传 / R2 原生分片 / 分布式 / 共享文件夹直传 / 共享分片(后两处是本项目自有的,上游只有前三处)。顺手把下载里内联的手算 EMA 也合并进同一函数,−6 行。
  • 合并下载时的两条纪律(别踩):下载端原本阈值是 0.4s、完成时还有一次强制刷新 → ①不要把阈值统一成 0.35(数值会更跳);②不要丢掉「完成强制刷新」。两条都保住 = 行为完全等价。
  • ② 分享链接自定义后缀 —— 正则从 32 位 hex 放宽到 1–64 位小写字母/数字/下划线/短横线,老链接全部仍合法。规则:创建留空 = 随机后缀;编辑留空 = 保持原后缀(不会因为清空输入框被悄悄改成随机)。
  • 🔴 没照抄上游:上游用 DB.prepare(INSERT) 靠主键冲突做原子占位;但本项目写 KV 走 ON CONFLICT DO UPDATE 的 upsert,根本没有原子性,照抄就是「看起来有锁、实际没锁」。改为新增 putIfAbsent() —— 不带 ON CONFLICT,只把 UNIQUE / 主键冲突翻译成 false,其余错误照抛(不吞真错误)。
  • 复制下载直链 —— publicShare 加 downloadUrl(仅「文件 + 无密码」时给)。该路由本来就存在,只是从没暴露给用户。
  • ③ 文本文件在线编辑 —— 新增 /api/text-file(鉴权之后):30+ 扩展名白名单;>2 MB → 413;分布式存储直接拒。ETag + If-Match 乐观锁:缺锁 428、锁不匹配 409「文件已被其他操作修改,请重新打开后再保存」。
  • 比上游多一步:写回前先 snapshotVersion() 留版本回滚点 —— 上游是直接覆盖,等于网页上改的文件没有版本记录。前端零第三方库:底层 <pre> 画高亮 + 上层透明 <textarea> 接管输入,滚动同步;Ctrl+S 保存。
  • 客户端同步:client/src/server.mjs 的 share-create 是逐字段白名单转发(只转 path/ttlSeconds/maxAccesses/password),新字段必须登记,否则被静默丢掉 → 补 suffix,打包 6.9.27。
2026-09-22 布局4 次部署

中间档布局四连修:从「表格溢出」到「整页被撑宽」

用户拖动窗口逐个发现:回收站很乱 → 时间列被裁成 2026/09/2 → 名称列只剩 4 个字。四轮排查都在治同一个根因家族。

  • 共同真凶:主界面共享的 .file-row-name{max-width:400px} —— auto 布局下表格宽 = 各列 min-content 之和,而名称列的 min-content 恰是「400 文字 + 图标 + 间距 + 内边距 = 458px」(实测各档都是 458,纹丝不动)。合计 ≈822px > 容器 → 溢出。
  • 修法:table-layout: fixed 让内容 intrinsic 宽度彻底退出计算,表格恒等于 100% 容器宽、与文件名长短无关。
  • 回收站窄屏:错位套用了主界面的 nth-child 列序规则(主界面是「名称/大小/时间/操作」,回收站是「勾选/名称/大小/时间」)→ 不换类,改用更高特异性的 .file-list.trash-list ... 逐列纠正。
  • 让哪一列:360px 下四列放不下 → 让「大小」不让「名称」(回收站里「多大」最不重要,名称宽度是上一轮刚调好的)。改完名称文字区反而从 212 涨到 232–284px。
  • 🔴 坑 1:切换按钮不能放在会被隐藏的列里 —— 第一版放 th4,切到「大小」后按钮跟着被隐藏,再也切不回来。改成「换第 4 列自己的内容和宽度」。
  • 🔴 坑 2:.material-icons-round 图标字体没就绪时会退化成英文单词,一口吃掉 163px(名称区只剩 85px)→ 补 width:1em。
  • 🔴 坑 3:量 flex 的视觉顺序一律看坐标(getBoundingClientRect().left),别看 DOM 顺序 —— 按 DOM 顺序判会得到假阴性。
  • 侧栏断点 768 → 1000px:769px 档名称列只有 134px(4 个字),根因是常驻侧栏吃掉 256px。断点值是算出来的:256+64+300+名称列 330 ≈ 950,取整 1000。改后 769 档名称列 134 → 437px。
  • 顺手修掉一个老 bug:抽屉遮罩 .sidebar-backdrop.open 是全局规则 → 在窄屏打开抽屉、再把窗口拉宽,侧栏回归常驻但遮罩留着,整页点不动。加 @media(min-width:1001px){display:none!important} 兜底。
  • 验证方式:真跑 worker.fetch()(内存 KV stub + HMAC 自签 session:v3 令牌)拿真实 HTML → iframe 精确模拟各档 CSS 宽度量列宽,并出真渲染截图目视(列宽达标 ≠ 不丑)。
回收站 641px 改前
回收站 641px · 改前:大小列只有 57px,「8.42 MB」折两行
回收站 641px 改后
回收站 641px · 改后:固定布局,大小单行,零溢出
回收站 721px 改前
回收站 721px · 改前:时间被裁成 2026/09/2,整页被撑宽 42px
回收站 721px 改后
回收站 721px · 改后:时间列 170px 单行,容器宽 = 视口
主列表 769px 改前
主列表 769px · 改前:侧栏常驻 256px,名称列仅 134px(「变更日志....」)
主列表 769px 改后
主列表 769px · 改后:侧栏变抽屉,名称列 437px,文件名完整
主列表 641px 改前
主列表 641px · 改前:日期列被裁,右侧静默消失
主列表 900px 改后
主列表 900px · 改后:时间列 96px 单行、行高整齐
2026-09-22 新增

回收站窄屏「删除时间 ⇄ 大小」一键切换

「左侧加了复选框,右边的时间没有了……既然没办法同时显示两种,能不能点一下『时间』就变成『大小』?」

  • 交互选型:三个方案(点表头文字切 / 表头加 ⇄ 图标切 / 不做切换),用户选加图标 —— 把「表头文字」这块热区留给将来的排序。
  • 箭头方向:切换态把 ↔ 用 order:-1 挪到文字左边 —— 默认「时间 ↔」、点后「↔ 大小」,箭头永远指向「点了会变成什么」。
  • 独立性(用户最关心的三点):电脑端 / 手机横屏不受影响(规则全在 @media(max-width:640px) 内,宽屏 CSS 没有任何规则引用切换 class);与排序独立;与主界面、共享页独立(class 只加在回收站这一个表格上)。
  • 已知代价:切换状态不持久 —— 刷新页面或恢复/删除文件后会回到默认「时间」列(有意为之)。
默认时间态
默认态:表头「时间 ↔」,箭头在文字右侧
切换到大小态
切换态:表头「↔ 大小」,箭头移到左侧,同一列换成文件大小
2026-09-20 修复Version 1237314d

手机表头「时间」两个字变成竖排

「1080P 显示刚刚好,只是『时间』两个字变成竖排」→ 第三版把箭头隐藏了 → 用户:「箭头看不见就没人知道能点它排序」。

  • 根因:表头是 flex,而 .material-icons-round 自带 flex-shrink:0(图标压不动)→ 空间不足时被压缩的必然是文字;48px 列宽内容盒只剩 40px,而 CJK 的 min-content 只有 1 个汉字宽 → 折成一半一个字 = 竖排。
  • 修法不是「把列加宽到装得下」,而是先分清谁能让步:文字绝不能收缩(否则就竖排)→ 文字 flex:0 0 auto; white-space:nowrap 锁死,要压就压箭头(flex-shrink:1; min-width:0)。
  • 验证:把表头字号放大到 16 / 18px 模拟手机系统字体缩放、把箭头放大到 40px 模拟极端情况 —— 三档 × 三情形「时间」全部单行 24×16px、箭头可见。
2026-09-19 新增Version 5f0c0256

手机端文件列表:时间列看得见、能排序

手机上文件列表只见名称/大小两列 —— 旧表格自动布局下「文件名不换行 + 全格式时间 + 操作按钮」总宽超出视口,时间列和操作列被挤到屏幕右边界外。要看时间只能切浏览器「电脑模式」。

  • 怎么改:窄屏三个表格(主网盘 / 共享访客页 / 分享目录页)改 table-layout:fixed(名称自适应 / 大小 58px / 时间 54px / 操作列隐藏);时间单元格渲染双格式 span(全格式 + 短格式),窄屏切短格式并允许折两行;表头「修改时间」双 span,排序箭头保留。
  • 🔴 第一版翻车:没按真实手机 CSS 宽度算 —— 1080p 屏 DPR 3 → CSS 视口只有 360px,不是 1080px。首版固定列宽后用户反馈「文件名只剩 6 个字、右边一片空白」(空白=被隐藏的操作列仍在占位)。
  • 结论:切换控件绝不能放在会被隐藏的列;量行数要用 Range(td.getClientRects() 恒为 1,会假绿)。
2026-09-19 界面Version e31c8665

共享文件夹改名 shared → 共享

用户根目录是「工作 / 麦芽糖 / 软件 / 生活 / 探索 / 同步」,就它一个英文,不协调。

  • 🔴 公开网址永不跟着改:页面路由、401 白名单、sharedBase = '/shared' 全部保持 /shared —— 发给学生的那个入口地址不能变。存储名(SHARED_PREFIX)与公开网址本来就是两个变量,这次只动前者。
  • 顺手发现:浏览器端脚本不能引用服务端 const(静默 ReferenceError)→ 必须在模板里用 ${jsString(SHARED_PREFIX)} 注入字面量。
  • 收尾:常量改完,云端那个空的 shared 文件夹就成了普通文件夹 → 用 /api/rename 把它改名(实测该文件夹当时完全为空,影响为 0)。
2026-09-18 新增Version 46aa979e

「从云盘选图」—— 绕开手机选图器

前两轮修复都失败后,用户给出决定性线索:电脑能设、手机浏览器能设、单单装了 PWA 的不能设。

  • 真根因:安卓安装版 PWA 跑在独立进程里,<input type=file> 选完图得到的是 content:// 引用,PWA 进程没有读它的权限 → size=0、arrayBuffer() 抛 NotFoundError。FileReader / arrayBuffer / createObjectURL 三条路全断在同一个权限上 → 不是代码能绕的,只能绕开选图器本身。
  • 怎么改:新增「从云盘选图」按钮(图标行、背景行各一个)—— 弹窗用 /api/list 列当前目录(可进文件夹、可回上一级、有面包屑),只列图片;点图后走同源 fetch('/api/download') 拿字节(PWA 里完全正常)→ 复用 canvas 缩图 → 上传。服务端零改动。
  • 收尾(同日):失败提示语改「先给结论、先给出口」—— 不再摆出「①云相册云端图 / ②PWA 权限」两个猜想让用户挑,直接给唯一动作「请改用『从云盘选图』」;catch 前缀由「读取失败:」改为「出错:」。
  • 说明:不是 PWA 的 bug 修好了,而是给了绕行方案 —— 系统级权限问题前端改不动。
2026-09-17 晚 修复Version a6f58672

手机 PWA 换背景图「读取失败」(两轮)

手机里「设置 → 外观 → 更换登录页背景图」,选完照片提示「读取失败」;电脑上同样操作正常。

  • 第一轮:uploadSiteAsset() 改成两条路 —— ① 原样读(≤3 MB 且 MIME 在白名单,桌面一直走这条,保真、保留 PNG 透明);② URL.createObjectURL → <img> → canvas 重编码(<img> 走浏览器自己的加载器,云相册的图也能拿到),顺带等比缩到 1920 / 512。
  • 第二轮真根因:旧代码信 file.type 标签、不信真实字节 —— 手机选图器经常给空标签/怪标签,好好的 JPG 被当成「不认识的格式」赶去走重编码路;而 img.onerror 把三种完全不同的病因全塞进同一句「可能是 HEIC」,没法排障。
  • 定稿修法:sniffImageType() 按字节魔法数认格式(JPEG/PNG/GIF/WebP + ftyp 盒品牌判 HEIC/AVIF),file.type 只做兜底;diagnoseAssetFailure() 三分诊:字节读不出 → 「云相册图没下载到本机,先点开原图/存本地」;真 HEIC/AVIF → 「导出 JPEG」;字节正常也认得 → 「渲染失败,换 Chrome/Edge」。
  • 验证:CDP 驱动真实 Chrome 20/20(空标签 JPG 走原样路、怪标签 PNG 按字节判型、HEIC 给精准提示且不发起上传、引用图给「点开原图」提示、11.6 MB 噪声大图走压缩路)。
2026-09-17 安全加固Version e01d5550

外部审计 7 项落地:库同源 + CSP 收紧 + XSS 清洗 + 长期会话

外部审计给出问题清单,复核后落地 7 项。

  • 预览库同源托管:原来 5 个库 6 个 URL 全走 cdn.jsdelivr.net —— 可用性风险(被墙/抽风 → 预览全废)+ 供应链风险(第三方脚本可在你的页面上执行)。改为 R2 同源托管 system/lib/ + 白名单只读路由 /assets/lib/,CSP 得以收紧为纯 'self'。
  • 为什么不能「内嵌进 worker.js」(审计的原建议):5 库 ≈3.0 MB、base64 后 ≈4.0 MB,加上当时的 629 KB → ≈4.7 MB 源码,而 Cloudflare 免费版按压缩后 3 MB 限制;更硬的理由是部署方式是「浏览器里粘贴文本」,4.7 MB 没法维护。
  • Office 预览 XSS 清洗:sanitizeDocHtml() 标签/属性白名单 + 删所有 on* + 去 url()/expression() + 协议白名单挡 javascript:。这是全站唯一真正的「数据派生 XSS」入口 —— 站内其余 ~58 处 innerHTML 要么写死字符串、要么已转义。
  • 「记住密码」→ 30 天长期会话:原来把密码 btoa(encodeURIComponent(pwd)) 存 localStorage —— base64 不是加密,等于明文。用户要的是「少输密码」不是「把密码存下来」→ 服务端签 session:v3 令牌,密码永不落地;兼容 v2 避免已登录用户被踢。
  • 安全响应头 + CSP:htmlResponse() 收口全部 9 处 HTML 出口(避免「加了 8 个漏了 1 个」);HSTS 不带 includeSubDomains;下载恒 attachment + nosniff。
  • 分享令牌密钥收紧:原来 shareTokenSecret 会回退用 passwordHash 甚至 share.id —— 而 share.id 是公开字符串(就出现在分享链接里)→ 可伪造任意分享访问令牌。改为只从真正的密钥类环境变量取,取不到就不签(fail-closed)。
  • 🔴 边界必须理解:script-src 必须保留 'unsafe-inline'(页面到处是 onclick)→ CSP 拦不住「已注入的内联脚本」。CSP 的价值在 frame-ancestors / object-src / base-uri / 限外域;防 XSS 只能靠清洗。任何「加了 CSP 就安全了」的想法都是错的。
  • 复核更正:审计说「inline=1 让访客上传的 HTML 被内联渲染」→ 复核结论是死参数(全项目搜不到任何 inline=1 生成点),严重性被高估,但顺手删掉仍是正确清理。
2026-09-17 修复

大文件(>512 KB)根本没有版本历史

核实「大文件无版本历史」缺口后如实记账,当天修掉。教学 PPT 基本都在这个区间 —— 该功能对主要使用场景是空转的(面板能开、能列,但永远没内容)。

  • 病灶链:snapshotVersion() 原来只有两个调用点(/api/upload 与 /api/multipart/complete),没有 /api/distributed/complete;而前端 >512 KB 的文件优先走分布式,且 init 里主节点恒在 → 即使一个外部节点都没配也照样走分布式 → 永远走不到有快照的那条路。
  • ⚠️ 别把两套「分片」看混:r2multipart_session_(R2 原生,有快照)/ multipart_session_(分布式,本次补上)/ shared_multipart_session_(访客上传,有意不做)。
  • 修了 4 处联动,不是「加一行」:① 分布式 complete 补快照调用;② snapshotVersion() 学会认 manifest(用真实大小做上限判定,否则任何上限都拦不住;contentType 存清单里声明的原始类型);③ /api/versions/restore 加分布式分支;④ 🔴 新增分片保护 VERSION_SRC_PREFIX —— 快照只存清单,而覆盖上传时旧分片会被清理,分片一删历史版本就变成「能列出、下载不了」的坏文件。
  • 档位一并上调:VERSION_MAX_MB 20 → 60、VERSION_KEEP_MAX 10 → 5(客户端/脚本版默认值同步改 60)。
  • 线上实测 10/10:1.5 MB 文件覆盖 → 版本出现且 size=1572864B(修前 0 条)→ 再覆盖累计 2 版 → 恢复最旧版 → 下载内容 sha256 与旧版本逐字节一致 → 删完版本后标记计数 0(无 KV 泄漏)。
2026-09-15 新增

存储用量改造:口径 + 占用构成 + 月均快照

原口径是「云盘内文件总大小 / 配额」,但桶里还躺着历史版本 / 回收站 / 站点资源 → 看不出会不会超 R2 免费额度。

  • 口径换成「存储桶物理占用」;calculateR2Usage() 本来就整桶遍历一遍,顺手按 key 前缀分类累加(历史版本 / 站点资源 / 其他对象 / 云盘文件),不增加任何一次 R2 请求。
  • 月均靠去重快照:只在真正全量扫描时写一条「当天」快照,按北京时间记日期,一天最多一条、保留 180 天,整数组存一个 KV 键。
  • 回收站单列「其中回收站 X」解释「删了怎么还占着」。
  • 已知代价:样本 <3 条或跨度 <3 天不给增长结论(保守,避免抖动误判);/api/storage 有 60 s 缓存,改完不要立刻断言分类没生效。
2026-09-14 修复新增

全局搜索三轮大修

原版只能盯着当前目录手翻,文件一多就是瞎。三轮用户实测反馈又暴露三个体验问题。

  • 第一轮:修 formatDate is not defined(结果行整个渲染失败)+ 修匹配数翻倍(网格与列表装的是同一批文件,两个容器一起统计了)。
  • 第二轮:/api/list-all 加可选 ?folders=1(不新增路由,同步端不传 → 调用开销与返回体零变化)+ 结果含文件夹 + 输入即搜 + 空态文案按上下文分流。
  • 第三轮:筛选从 hit(name) || hit(path) 收紧为只看 f.name + 搜索面板可滚动(480 → 760px)。
  • 效果(88 文件 / 18 文件夹真实数据):搜「必修一」从 文件夹 8 + 文件 50 收敛到 文件夹 2 + 文件 1。
  • ⚠️ 形状陷阱:带 folders=1 时 folders 是 {name,path} 对象数组;而 /api/list 的 folders 是字符串数组。写测试 stub 时必须同形,否则「断言全绿但功能是坏的」。
2026-09-12 修复数据安全

孤儿清理会删光回收站与全部历史版本

复核原版代码时发现 findAllReferencedStorageKeys() 只扫文件前缀,漏了回收站与版本索引。

  • 后果:点「清理孤儿文件」会把回收站对象和所有历史版本对象判为孤儿 → 勾选删除即永久丢失(D1 条目还在,所以列表照常显示,点「恢复」才报「对象不存在」)。
  • 修法:抽出 collectStorageKeysByPrefix() 消掉「同一件事两份实现」这个根因;补扫回收站 + 版本索引前缀;deleteOrphanStorageKeys() 加双层兜底(前缀白名单物理拒删 + 删除前再反查一遍)。
  • 效果:扫描出 155 个孤儿,与 D1 在引用的 key 完全一致者 = 0 → 安全清理释放 536.2 MB(R2 对象 283 → 128)。
  • 🔴 这是全项目最容易埋雷的地方:以后往 R2 放任何非文件系统资源(新的库、缩略图缓存、字体……),必须同时做两件事 —— ① 加进引用集合;② 扩兜底前缀。漏任何一半,跑一次清理就永久丢。
2026-09-12 优化

回收站 N+1 → 集合查询 + 清理 30 天前 + PWA 图标真根因

  • N+1 根治:原实现 = 1 次 list 只取 key + 每条再单独 SELECT。新增 listRaw() 一条 SQL 取回全部键值,返回结构与排序不变 → 调用方零改动。实测 89 条:新写法 0.95 ms vs 旧写法累计 ≈18 ms(还没算跨机房往返)。
  • 「清理 N 天前」用手动按钮而不是 cron:量级小(当时 78 文件 / 513 MB),手动「可见可控」,且避免定时任务误删。
  • PWA 图标溢出的真根因:只是 manifest 里 purpose:'maskable' 一行(安卓按中心 80% 安全区遮罩裁切满幅图)。
  • 教训:当天试过「双图架构」(PWA 用带底图、页头用透明图)→ 用户实测「连页头 logo 也变了」→ 当天回退。默认走最小改动,别上纲成系统工程。
2026-09-11 修复事故级

/api/list-all 的 modified 取值错误

曾写成 updatedAt || uploaded —— 而 updatedAt 是条目写入时刻,改名会被刷成 now。

  • 后果:整棵树被判「云端较新」→ 同步把约 500 MB 重下到「云端更新」。这是全项目第二严重的事故。
  • 修法:modified 必须是内容上传时间优先 —— uploaded || updatedAt。
2026-09-10 优化修复

目录改名 / 移动批量化(原版最严重缺陷)

  • 原版缺陷:改名 = 逐文件搬 + 每个文件都重建整个父目录索引 = O(N²) 子请求,超过 Worker 免费版 50 子请求上限被强杀 → 留下新旧两份重复索引(云端出现「幽灵目录」)。
  • 修法:把「逐条读改写」改成「一次批量取源 + 全部塞进 DB.batch()」—— 子请求 ≈500 → ≈7,实测 2.3 秒完成。
  • 配套:改名一条龙(把三步操作压成双击一次);09-11 控制台改名按钮「没生效」三层排查(根因是前端引用未定义变量导致 onclick 静默中断)。
  • 纪律:改云端目录名的唯一正确入口是 UI 的「改名…」;脚本版与客户端互不联动,一侧改名后另一侧必须补做,且补做前严禁跑同步。
2026-09-09 优化

回收站列表并发读

  • 怎么改:Promise.allSettled 并发化,治「回收站点开要等很久」(清空后就秒开,这是典型 N+1 症状)。
  • 演进:这是第一轮;09-12 用 listRaw() 集合查询根治。
2026-09-08 新增跳跃最大

在线预览五件套 + 侧边栏 + 页面内弹窗(单版 +220 KB)

原版这些格式全都只能下载。教师场景里 PDF / pptx 是最高频的预览需求 —— 这一块是提升最大的。

  • PDF:pdf.js canvas 自绘。特意没用 <iframe> —— iframe 方案在 PWA / Electron 里会挂(Electron 无内置 PDF 插件,白屏)。带翻页 / 缩放 / 适应窗口 / 左右键。
  • Word:mammoth 转 HTML,保留标题、表格、图片结构。旧版 .doc(二进制格式)无法解析。
  • Excel:SheetJS 解析 → 自绘表格,多 sheet 用标签页切换。
  • PPT:自研 OOXML 解析器(JSZip 解包 + 按 a:xfrm 坐标还原版式)。⚠️ 网页端至今仍是自研还原器,补不起「主题/母版继承 + 表格 + 背景图」,属已知待办。
  • 音视频:关键是代理层透传 Range —— 否则 mp4 要整包下完才能播、不能拖进度条。
  • 可折叠侧边栏 + 遮罩层,移动端默认收起。
  • 15 处原生 alert/confirm/prompt 换成 Material 风格弹窗(PWA / 移动端尤其难看)。
2026-09-06 新增

版本历史

同名文件覆盖上传 = 旧内容直接消失。云盘最常见的「手滑」。

  • 怎么改:覆盖前把旧实体快照成一个新 R2 key(r2drive:version:<hash>/<id>/<name>);每个文件的版本索引存一条 D1 记录(value 是数组);新增 /api/versions / restore / delete + 网页端面板。
  • 价值:与回收站配合构成双保险(删错、覆盖错都能救)。
  • 后来才知道:这一版对 >512 KB 的文件是空转的(见 09-17 那条);网页端弹窗也从上线起就坏着(见 09-27 那条)。
2026-09-05 修复

401 守卫白名单(修了原版的 bug)

  • 原版 bug:/s/xxx(分享链接)与 /shared(访客页)也被踢去登录页 → 分享功能等于失效。
  • 修法:PUBLIC_PREFIXES = ['/login', '/s/', '/shared'] 白名单式放行;install401Guard() 被所有页面继承,漏一个就把访客全踢走。
  • 回归风险:以后加任何免登录路由都要考虑它会不会被拦 —— /assets/lib/ 就是放在鉴权之前处理来避开的。
2026-09-01 修复

分片上传断点续传

  • 为什么:原版断网 = 从头再来。大文件(课件、录课视频)重传代价极高。
  • 怎么改:resumeKey = sha256(路径|大小|mtime) 找回分片会话;/api/multipart/init 支持复用;新增 /api/multipart/status 查已收哪些片;会话 TTL 7 天(对齐 R2 的分片回收策略)。
  • 🔴 TTL 不要改小:改小会让 7 天内可能续传的文件变成「必须重传」。
2026-08-30 新增

PWA 支持

  • 怎么改:manifest + Service Worker + 多尺寸图标;图标优先读用户上传的站点图标,没上传就用内置 base64 回落。可安装到桌面 / 手机主屏,全屏无地址栏。
  • 反复踩的坑:Service Worker 会缓存旧 UI —— 改前端后用户不 Ctrl+F5 看不到新版。改 UI 后要提示一次强刷。
2026-08-28 基线原版 + 首轮加固

初稳定版:首轮 6 处安全修复 + 访客协作 + 回收站

从上游原版(HandsomeMJZ / R2-Cloud-Drive,7,187 行 / 280 KB)跑通并做第一次加固 —— 这一轮同时定下了整个项目的走向:从「能用」到「敢用」。

  • 安全六处:节点 API 密钥回退(密码泄漏通道)/ 孤儿分片清理误删(会把正在上传的分片当垃圾删)/ 分片 complete 校验 / SSRF 防护(节点 URL 可填内网地址)/ CSRF 覆盖到全部写接口 / 登录爆破限流。
  • 访客协作:/shared 从只读变成可收件 —— 口令验证 + 直传 + 分片 + 建文件夹 + 配额;新增两个环境变量。
  • 回收站(软删除):原版删除=永久消失,误删无法挽救。删前把整棵子树快照到 trash: 前缀,R2 实体不立刻删。
  • 删掉全局页脚:原作者写在全局外壳里 → 登录 / 主页 / 分享页都带着。

三、「不做」也是决策

项目处理为什么
版本更新检查checkVersionUpdate删除 页面加载时去 raw.githubusercontent.com 拉 version.json —— 运行时依赖 GitHub,国内常被墙/超时,拖慢首屏。代价:上游出新版不会自动知道。
下载 inline 参数删除 全项目搜不到任何 inline=1 生成点 → 死参数。删除后下载恒 attachment,「上传 HTML 再骗人点开」的攻击链彻底不存在。
双图架构SITE_FAVICON_KEY当天回退 用户实测「连页头 logo 也变了」—— 本意只是「不让 PWA 图标主体溢出」。真正的根因就是 manifest 的 maskable 一行。教训:默认走最小改动。
favicon 清理临时按钮用完即撤 一次性运维操作,不应长期留在界面上。
上游 Hollow 毛玻璃皮肤
+ 自定义壁纸
放弃 ① 上游把两个接口的 GET 放在鉴权之前(为了让登录页也套主题)→ 任何人猜到路径就能拉到壁纸;② 皮肤会覆盖 .file-card / .sidebar / .file-list,与本项目已调好的四套布局正面打架 —— 成本最高、收益最低。
分享管理独立面板
分享页在线预览
全选圆形勾选
暂缓 不是不做,是排在后面。⚠️ 其中的请求竞态序列号值得单独抄 —— 本项目分享面板现在是裸 fetch,同样有「旧响应覆盖新渲染」的毛病。

四、这份文档怎么维护

定位:`worker改动全史.md` 是纵向总账(按功能域回答「这个功能经历了什么」);这份 HTML 是横向 + 可视化(按时间回答「哪一天改了什么、长什么样」),两者互补,不互相替代。