财务私档索引

一堆银行对账单、开户凭证、交易明细散在硬盘各处。要能随时翻、能在外面翻, 但绝不能落到第三方手上。做法是:把目录树派生成一个纯静态索引站,放在自己的服务器上,整站挂闸。

以下全部为合成数据。本页不是那个站本身 —— 真实的档案站在闸后,不出现在任何导航与目录页里, 这里一个真实的账户名、金额、日期、文件名都没有,全部是编的。 但两个工具跑的是真实规则与真实计算:第 3 节的白名单/分桶/大小格式化逐条对着生成器源码实现, 第 4 节的对账是现算的 —— 你可以把自己的流水贴进去用,数据不出你的浏览器

1 · 这套系统解决什么问题

财务文件有一组彼此打架的要求,任何一条单独看都好办,凑在一起就不好办:

要能随时翻对账、报税、办事的时候要立刻找到某一期的某一份,翻硬盘目录太慢。
要能在外面翻人不在电脑前的时候也要能查 —— 这一条把「只放本机」这个最安全的方案排除了。
不能交给第三方对账单里有账号、余额、姓名。传给任何一家网盘,它就同时存在于你控制不了的地方。
不能被搜到、不能被顺手转发私密不只是「别人打不开」,还包括「别人不知道它存在」。
攻击面要尽可能小这类站一旦被攻破,泄的是最不能泄的那批文件。宁可功能少。

为什么不是网盘

现成网盘这套做法
文件在别人机器上,加密与保留策略由对方定文件在自己的服务器上,删了就是删了
分享链接是产品功能,随手一点就外发;撤回要靠对方守约站上根本没有「分享」这个动作,没有可写端点就没有可误触的开关
登录方式由对方定,通常绑第三方账号闸自己写,通道自己选(见第 5 节)
会被搜索、会被推荐、会进「最近访问」这类聚合面noindex + 不进任何导航、目录页、feed、站内搜索索引
客户端会自动同步、自动上传、自动生成缩略图整条链是单向的:本机 → 服务器,没有回程

取舍很清楚:放弃了协作、评论、版本历史、移动端 App 这些网盘功能。 对「一个人翻自己的对账单」这个场景,这些功能一个都用不上,但每一个都是一条攻击路径。

2 · 方案:一条单向管线

本机源目录各账户的资料与对账单归档。只读,生成器一个字节都不改。
生成器枚举 → 白名单过滤 → 按顶层子目录分桶 → 拷贝文件 + 渲染单页索引。
产物目录本机临时区,每次整个删掉重建
服务器一个静态目录,nginx 直接发文件。没有进程、没有数据库、没有 API。
整站拦在 location /,未登录一律跳登录页。

这条链上没有任何一步是从服务器往本机走的。服务器被攻破的最坏后果,是那份已经上传的副本泄露, 而不是攻击者顺着链路摸回本机源目录 —— 因为链路根本不通那个方向。

3 · 工具一:档案索引表

这就是索引页本身的样子(数据是编的)。按账户筛、按期间筛、按标签筛、搜文件名、点表头排序, 勾中若干条导出清单 CSV —— 报税或找会计的时候,要交的往往不是文件本身而是「有哪些文件」。

账户
期间起
期间止
搜文件名
 
标签为「与」关系:勾多个 = 必须同时带有这几个标签。
被规则剔掉的候选(点开看生成器为什么不收它们)

白名单只放行八种后缀。白名单的意义在于新出现的未知类型默认进不去;黑名单则相反, 哪天源目录里多出一类没预料到的文件,它会自己上站。

「大小」列排的是原始字节数(藏在 data-v 里),不是显示出来的 1.2M; 不然 900K 会排在 1.2M 后面。字节转人类可读用的是 Python 的 四舍六入五成双,不是 JS 的 toFixed —— 那份 1.25M 的文件上能直接看出区别。

4 · 工具二:对账小工具

索引解决「文件在哪」,不解决「这个月到底花了多少、有没有记重、有没有一笔离谱的」。 下面这块贴几行 日期,摘要,金额 就能算:月度收支汇总、重复条目、异常金额。 这部分是真算的,页面里预置的是编的示例,你可以整段删掉贴自己的。

数据不出浏览器。这段计算全部在你本机的 JavaScript 里跑完,本页没有任何网络请求、没有后端、 不写 cookie、不存 localStorage。关掉标签页,你贴进去的东西就没了。

5 · 关键设计决策

决策为什么,以及代价
零可写端点 没有 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 在同域下全子域通用,一次登录管一片。

坑二:一条审计规则绿了,但它绿的理由是错的。 有个巡检脚本负责核对「这个站到底有没有挂访问控制」,它当时报的是「没有」。 真相是:一个访问控制应用可以挂多条目标域名,而脚本只打印了其中一个字段 —— 它看见的东西本来就不全。 站是有保护的,脚本却说没有。比「守卫报红」更危险的是「守卫报绿,而它绿的理由是错的」, 因为前者会被处理,后者会被当成结论引用。修法是让它逐条目标打印,从此这条对账才真的看得见东西。

坑三:私密站也必须登记进注册表。 直觉是「越私密越不该写进任何清单」。实际相反:不登记 = 没有任何巡检认识它, 它挂了没人知道、闸掉了也没人知道。正确做法是登记,但打上「不公开」标记 —— 它照常进健康巡检与访问控制对账,但被挡在导航、目录页、站内搜索索引这些公开派生物之外, 并且有一条反向断言盯着:这个域名一旦出现在任一公开派生物里,审计立刻报红。

6 · 技术上怎么做的

部件实现
生成器 单文件 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 offtry_files ... =404。注册表里这个站的进程字段与端口字段都是空的 —— 它真的没有进程
源站自建,两条通道:密码页 + 邮箱验证码。登录后下发的 cookie 在整个域下通用, 一次登录,同域下所有受保护站点都过。整站保护挂在 location / 上, 端点声明挂在 server 级 —— 位置写反会导致闸自己的登录端点也被闸住。
上传这一步的诚实记录 生成器不含上传步骤(读源码确认:它只写本机产物目录), 所以运维注册表里这个站的「部署命令」字段填的是 null —— 如实记录「没有一条可跑的命令」, 而不是照着注释拼一条看起来合理的。编出来的部署命令是所有机器检查都抓不到的那类错: 它长得和真的一模一样,只在真正执行的那一刻炸。

现状

在线一个不公开的子域,纯静态,无后端进程。运行中
访问整站闸,密码 / 邮箱验证码双通道。未登录访问任意路径一律跳登录页。
可见性不进导航、不进目录页、不进站内搜索索引、不进 feed、不进 sitemap;noindex 双写。
纳管已登记进子域表与部署表两张注册表,进健康巡检与访问控制对账; vhost 配置已从服务器收编进配置仓,受漂移审计约束。
没做的没有全文检索、没有 OCR、没有按金额筛选、没有移动端上传。 每一条都要么需要读文件内容,要么需要一个可写端点 —— 都与第 5 节第一条冲突。
本页第 3 节的账户名、文件名、期间、字节数、标签,以及第 4 节预置的那份流水,全部为合成数据, 凭空编造,与任何真实账户、真实档案、真实收支无关;账户只写到「甲乙丙丁行 + 编造的尾号」, 既没有真实机构也没有真实卡号。真实的档案站是一个不公开、整站挂闸的子域, 不出现在本站导航、目录页与站内搜索里,本页也不链接它。 两个工具的全部计算都在你的浏览器内完成:本页不发起任何网络请求、不加载任何外部资源、不上传你贴进去的任何内容。