一堆银行对账单、开户凭证、交易明细散在硬盘各处。要能随时翻、能在外面翻, 但绝不能落到第三方手上。做法是:把目录树派生成一个纯静态索引站,放在自己的服务器上,整站挂闸。
财务文件有一组彼此打架的要求,任何一条单独看都好办,凑在一起就不好办:
| 要能随时翻 | 对账、报税、办事的时候要立刻找到某一期的某一份,翻硬盘目录太慢。 |
| 要能在外面翻 | 人不在电脑前的时候也要能查 —— 这一条把「只放本机」这个最安全的方案排除了。 |
| 不能交给第三方 | 对账单里有账号、余额、姓名。传给任何一家网盘,它就同时存在于你控制不了的地方。 |
| 不能被搜到、不能被顺手转发 | 私密不只是「别人打不开」,还包括「别人不知道它存在」。 |
| 攻击面要尽可能小 | 这类站一旦被攻破,泄的是最不能泄的那批文件。宁可功能少。 |
| 现成网盘 | 这套做法 |
|---|---|
| 文件在别人机器上,加密与保留策略由对方定 | 文件在自己的服务器上,删了就是删了 |
| 分享链接是产品功能,随手一点就外发;撤回要靠对方守约 | 站上根本没有「分享」这个动作,没有可写端点就没有可误触的开关 |
| 登录方式由对方定,通常绑第三方账号 | 闸自己写,通道自己选(见第 5 节) |
| 会被搜索、会被推荐、会进「最近访问」这类聚合面 | noindex + 不进任何导航、目录页、feed、站内搜索索引 |
| 客户端会自动同步、自动上传、自动生成缩略图 | 整条链是单向的:本机 → 服务器,没有回程 |
location /,未登录一律跳登录页。这就是索引页本身的样子(数据是编的)。按账户筛、按期间筛、按标签筛、搜文件名、点表头排序, 勾中若干条导出清单 CSV —— 报税或找会计的时候,要交的往往不是文件本身而是「有哪些文件」。
data-v 里),不是显示出来的 1.2M;
不然 900K 会排在 1.2M 后面。字节转人类可读用的是 Python 的
四舍六入五成双,不是 JS 的 toFixed —— 那份 1.25M 的文件上能直接看出区别。
索引解决「文件在哪」,不解决「这个月到底花了多少、有没有记重、有没有一笔离谱的」。
下面这块贴几行 日期,摘要,金额 就能算:月度收支汇总、重复条目、异常金额。
这部分是真算的,页面里预置的是编的示例,你可以整段删掉贴自己的。
| 决策 | 为什么,以及代价 |
|---|---|
| 零可写端点 | 没有 API、没有上传、没有删除、没有后端进程 —— 站上只有 nginx 在发静态文件。 拿不到的东西不会被改。代价:想加一份文件,必须回本机跑一次生成再上传,没法在手机上传。这个代价是主动付的。 |
| 源目录只读 | 生成器只读源、只写产物目录。让一个会碰真实财务文件的脚本永远不具备写源的能力, 比「小心不要写错路径」可靠得多 —— 前者是结构上做不到,后者是每次都得记得。 |
| 产物整个删掉重建 | 不做增量 diff。增量的经典失败是:源里删掉一份文件,产物里那份还在,站上就留了个不该在的残影, 而且页面一切正常、没人会发现。全量重建让「站上有什么」永远等于「源里现在有什么」。 |
| 后缀白名单,不是黑名单 | 只有八种后缀能进站,见第 3 节那个折叠面板里的三条剔除记录。 |
| 空集拒绝生成 | 一个文件都没收集到时非零退出,不生成。因为源路径写错、外置盘没挂上这类故障, 如果照常生成,得到的是一个「页面 200、结构正常、内容为空」的站 —— 它看起来完全健康。这类静默失败必须显性化。 |
| 默认 dry-run | 不加 --apply 只打印「收集到几个文件、分几组、分别来自哪」,不落盘不拷贝。
碰真实财务文件的脚本,默认档必须是不产生副作用的那一档。 |
| 索引从目录树派生,不手维护清单 | 没有任何一份「文件列表」需要人去更新 —— 列表就是目录树本身。 分组也不是配的:按顶层子目录自动分桶,因为你当初怎么归的档,那个层级本身就是信息。 手维护的清单必然漂移,而漂移的索引比没有索引更糟。 |
| 文件不进版本库 | 进版本库的只有生成器(几百行代码),文件一份都不进。产物落在本机临时区,
是派生物不是资产 —— 丢了就重跑一次。这条同时解决了「财务文件被 git add . 顺手带上远端」这个最容易犯的错:
它压根不在任何仓的工作树里。 |
| 目录浏览关掉 | autoindex off:即使猜到某个子路径,也拿不到目录列表。
目录结构本身就是信息 —— 光看文件夹名字就能推出你在哪几家开了户。浏览一律走生成的那页索引。 |
| noindex 写两遍 | HTML 里一份 meta robots,HTTP 响应头里再一份 X-Robots-Tag。
PDF、CSV 这类文件里没有地方放 meta,只有响应头能覆盖到它们。另加 Referrer-Policy: no-referrer,
从站内点出去不把来源地址带走。 |
坑一:闸不能建在边缘。 这个站最初整站挂的是云端边缘访问控制(邮箱验证码)。后来要加「输密码也能进」这条通道时才发现: 边缘拦截罩住哪条路径,源站在那条路径上的登录页就永远轮不到出场 —— 请求在到达源站之前就被截走了。 两者不是并联关系,只能各占一条路径。最终形态:整站的闸建在源站 nginx 里(密码通道), 邮箱验证码通道单独占一条子路径,仍由边缘罩着。登录后的 cookie 在同域下全子域通用,一次登录管一片。
坑二:一条审计规则绿了,但它绿的理由是错的。 有个巡检脚本负责核对「这个站到底有没有挂访问控制」,它当时报的是「没有」。 真相是:一个访问控制应用可以挂多条目标域名,而脚本只打印了其中一个字段 —— 它看见的东西本来就不全。 站是有保护的,脚本却说没有。比「守卫报红」更危险的是「守卫报绿,而它绿的理由是错的」, 因为前者会被处理,后者会被当成结论引用。修法是让它逐条目标打印,从此这条对账才真的看得见东西。
坑三:私密站也必须登记进注册表。 直觉是「越私密越不该写进任何清单」。实际相反:不登记 = 没有任何巡检认识它, 它挂了没人知道、闸掉了也没人知道。正确做法是登记,但打上「不公开」标记 —— 它照常进健康巡检与访问控制对账,但被挡在导航、目录页、站内搜索索引这些公开派生物之外, 并且有一条反向断言盯着:这个域名一旦出现在任一公开派生物里,审计立刻报红。
| 部件 | 实现 |
|---|---|
| 生成器 | 单文件 Python 3,只用标准库(pathlib / shutil / html / datetime)。
零第三方依赖 —— 一个跑在真实财务文件上的脚本,依赖树越短越好审。 |
| 枚举 | rglob("*") 排序遍历,逐条过后缀白名单与跳过目录名单(.git / __pycache__ /
node_modules / .venv)。判定只看路径与后缀,不读文件内容。 |
| 索引页 | 一个自包含 HTML:CSS 与 JS 全部内联,零外部请求。 站在闸后,任何外链都意味着一次向外的连接 —— 何况页面上还有档案的结构。本页同样如此。 |
| 大小格式化 | 逐级除 1024 到 B/K/M/G/T。细节:1310720 字节正好是 1.25M,
Python 的 .1f 用四舍六入五成双给出 1.2M,而 JavaScript 的
toFixed(1) 会给 1.3M。本页为了和生成器输出逐字一致,自己实现了成双规则。 |
| 排序 | 点表头触发,数值列按数值比、其余按中文本地化字符串比,同值时用文件名兜底成全序。 早期版本没有兜底,同一年内的日期在比较器眼里相等、排序结果不稳定 —— 这是个真实的粗糙处,不是「设计如此」。 |
| 托管 | nginx 静态站:root 指到产物目录,autoindex off,
try_files ... =404。注册表里这个站的进程字段与端口字段都是空的 —— 它真的没有进程。 |
| 闸 | 源站自建,两条通道:密码页 + 邮箱验证码。登录后下发的 cookie 在整个域下通用,
一次登录,同域下所有受保护站点都过。整站保护挂在 location / 上,
端点声明挂在 server 级 —— 位置写反会导致闸自己的登录端点也被闸住。 |
| 上传这一步的诚实记录 | 生成器不含上传步骤(读源码确认:它只写本机产物目录),
所以运维注册表里这个站的「部署命令」字段填的是 null —— 如实记录「没有一条可跑的命令」,
而不是照着注释拼一条看起来合理的。编出来的部署命令是所有机器检查都抓不到的那类错:
它长得和真的一模一样,只在真正执行的那一刻炸。 |
| 在线 | 一个不公开的子域,纯静态,无后端进程。运行中 |
| 访问 | 整站闸,密码 / 邮箱验证码双通道。未登录访问任意路径一律跳登录页。 |
| 可见性 | 不进导航、不进目录页、不进站内搜索索引、不进 feed、不进 sitemap;noindex 双写。 |
| 纳管 | 已登记进子域表与部署表两张注册表,进健康巡检与访问控制对账; vhost 配置已从服务器收编进配置仓,受漂移审计约束。 |
| 没做的 | 没有全文检索、没有 OCR、没有按金额筛选、没有移动端上传。 每一条都要么需要读文件内容,要么需要一个可写端点 —— 都与第 5 节第一条冲突。 |