杂记 › 技术笔记 › 拖进去一整个文件夹,候选池只多了 1 个名字:文件随机抽取读不到目录里的 2558 个文件

拖进去一整个文件夹,候选池只多了 1 个名字:文件随机抽取读不到目录里的 2558 个文件

一整个文件夹拖进来,候选池只多了 1

把装着 2558 个文件的目录拖到页面中间的拖放区,松手后池里的数字只加 1:磁盘上排着 2558 个文件,进池的是 1 个名字。

这页从头到尾只处理名字,而文件随机抽取的名字口径刚好解释了目录、同名、大小、数量上限四处容易踩空的地方。

代码层先给答案:六个目录 API 各 0 次命中

459 行的模板里,showDirectoryPicker、webkitdirectory、webkitGetAsEntry、getAsFileSystemHandle、dataTransfer.items、webkitRelativePath 这六个 API 的命中都是 0。唯一的文件入口是 L204 那个只带 multiple 的 <input type="file">,change(L301)与 drop(L299)都汇进 handleFiles(L303-311),而它对每个条目只读一句 f.name(L306)。多选、drop 文件、drop 目录,三条入口在实现上是同一条。

真把目录交给 input,只拿到一个空壳

Chrome 给 L204 的 FileList 是 len:1 / sizeSum:0,池里只多出一个空壳名字 tmp_dir_633;drop 一个 type 为空串的目录条目同样只入池 1 项,名字是 项目资料2026。

有一条边界要说清楚:showDirectoryPicker() 在无头环境里的真实返回是 SecurityError,message 原文含 Must be handling a user gesture to show a file picker,探针里 typeof 是 function、isSecureContext 是 true;从 JS 设上 webkitdirectory = true 再用 CDP 注入目录,三次都是 setFileInputFiles 返回 ok 而捕获到的 change 数组为空、池恒为 0。这两条是无头环境的实测限制(不可触发),不能写成「浏览器不支持读文件夹」。本页结论只能停在代码层:六个 API 各 0 次命中,真实用户在这条入口上只有 multiple 多选文件。

能吃进多少:没有硬上限

2558 个真实磁盘文件一次注入,池 2558、预览 35748 字符、handleFiles + updatePool 全链 413.1ms。合成 DataTransfer 派发真 drop,规模再大也全量入池:

条目规模模板侧耗时
10001 ms
2000016 ms
5000065 ms

50000 条只花 65 ms,代价落在可读性:预览 74630px 对容器 120px,屏幕上只剩 0.16%。

drop 规模与模板侧耗时1000 条1 ms20000 条16 ms50000 条65 ms预览 74630px 对容器 120px

图 1 柱长按毫秒等比。

名字进池,字节留在原地

.size、.type、FileReader、Blob、createObjectURL、fetch、accept 各 0 次命中,所以 10485760 字节的 big_10mb.bin 与 33 字节的 file_0001.txt 在池里完全等价。页面也没有「类型不支持」「文件过大」这两条失败路径,连 .rfile-empty 空态样式(L160-168)都没被任何元素引用。注入完成后 L301 立刻清空 input.value,文件句柄当场丢弃。

同一个目录里,名字会撞车

磁盘实测有 500 组同名文件,每组各占 2 份:池是 2558 个槽位,不同名字只有 2058 个,同名占掉 1000/2558 = 39.1% 的槽位。2558 池、count=1、开关关着连跑 2000 轮,按槽位加总的命中是 2795 次而实际只抽了 2000 次,多出的 795 次(39.8%)全落在同名名字上——重名文件的中签率是单名的 2 倍,而结果与复制串都只有裸名字,看不出抽中哪一份。

不放回去重的是槽位不是名字(L411-415):真实 2558 池、count=20 连抽 100 轮,有 4 轮在同一轮抽出两个一模一样的文件名,count=5 抽 400 轮也有 1 轮。跨轮完全独立:池 5 项 count=2 连抽 3 轮,第 2、3 轮都出现 delta.txt;L418 的整体覆盖让统计格只读最后一轮的条数(L352),不是累计值。

槽位、名字与同名占比槽位2558不同名字2058同名槽位1000同名占比 1000/2558 = 39.1%

图 2 柱长按项数等比。

数量框只有 min,没有 max

L224 的抽取数量框只写了 min="1"。L367 的 parseInt(v)||1 把 0 / abc / 1000 / 1e3 / 0x10 全静默当 1,2.7 当 2。全模板 validity、checkValidity、reportValidity 各 0 次,会中断抽取的只有 3 条 toast:候选池为空(L363)、抽取数量至少为 1(L369)、不能超过候选数(L374)。填 100000 时页面真跑:墙钟 112291ms、300070 个页面元素、复制串 1397501 字符。

随机源是 Math.random 的 3 个调用点:动画(L390)、放回(L406)、洗牌(L412),crypto、getRandomValues、seed 各 0 次,给不出可复现序列。不放回抽 1 个也要洗完 2558 项;而 rfileRolling 写 2 次读 0 次(L379、L399),动画期间再点不会被拦。

导出只有一个字符串

copyResults()(L442-445)交给 UU.copy 的就一个字符串:纯 LF 分隔,无序号、无表头、无末行换行。屏幕上序号排到 1..7,复制出来只有 7 行裸名字,1000 项是 3999 字符。app.js 的 L37 与 L53 两条分支弹同一句「已复制到剪贴板」,兜底失败也看到成功提示;download 命中 0 次,没有导出文件这条路。

对照 config/tools.php L1513-1525 的 14 条宣称,[冲突] 9 条里就有 intro 那句「不重复:每个文件只被抽取一次」——同名那一节直接把它推翻。

三个常被问到的边界

Q:这页能不能把整个文件夹读进来?

代码层不能:六个目录 API 各 0 次命中,drop 目录和把目录交给 input 都只入池 1 个名字。

Q:同名文件算一个候选还是两个?

算两个槽位、各有一次机会,中签率是单名的 2 倍,但结果看不出抽中哪一份。期望算术转盘那篇旧文推过,本篇只给口径。

Q:能按大小或类型先筛一遍吗?

模板从不读字节,10485760 与 33 等价,既没有过滤也没有按权重的抽法——weight 的命中全是 CSS 字重。

📎 相关文章

文本对比的代价全在 (m+1)×(n+1) 这张表:6000 行还能跑,8000 行直接堆内存崩 2026-10-28 10 万个键全进 DOM:JSON 视图的渲染规模、爆栈深度与复制重排 2026-10-28 带 g 的正则是有状态的:同一个 RegExp 连打 4 次 test 只过 2 个 2026-10-28 JPG 转 PNG:27760000 个像素一位没变、体积涨到 15.881 倍,JPEG 的块效应被原样锁进无损容器 2026-10-28 20 行文件名拆成 18 行两列:粘进 Excel 只占 1 列,1e5 变成 100000 2026-10-28