想自己造安全机制之前,先查底层 API 是不是已经保证了

一次「写文件失败会不会清零原档案」的排查,让我意识到很多我想自己兜底的安全性问题,其实底层 API 已经把这件事做完了——我只是不知道。

最近在给一个个人工具接本地文件存储。核心场景很朴素:用户在浏览器里改动数据,落到硬盘上一个用户自选的文件里,下次打开时读回来。

写第一版的时候,我在自测里发现一个不算太罕见的问题:中途写入失败,原来的文件会被清零。

我当时的第一反应是很本能的那种——「那我自建一层安全机制吧」。脑子里已经开始设计方案:

  • 先写到一个 .tmp 临时文件
  • 完全写完之后 rename 覆盖原文件
  • 这样中途挂了,原文件还在,最多丢这一次的改动

这个模式对老程序员来说太熟了,Unix 世界里管这个叫「原子替换」(atomic replace),几十年的老套路。

停下来查了一下 API 文档

真正动手前我留了个心眼,先去查了下我正在用的这个 Web API(File System Access API)到底是怎么处理写入的。

结果——它自己就是原子的。

规范里写得很清楚:调用 createWritable() 时,浏览器会在内部开一个 swap 文件,你所有的写操作都落在这个 swap 上;只有当你调用 close() 的那一刻,浏览器才会把 swap 提交成正式文件。

换句话说:

  • 中途出任何事(用户拔盘、进程崩溃、你自己 abort()),只要没走到 close(),原文件一个字节都不会动
  • 我根本不需要自己造 .tmp + rename 那一层

更讽刺的是,就算我想自己造,也造不干净——这个 API 只给我文件句柄(file handle),拿不到目录句柄(directory handle)。我根本没有权限在同目录下创建一个我自己的 .tmp

底层已经替我把这件事做完了,做得比我能做的更好。

这不是第一次踩这个模式

回想起来,这类「我以为要自己造轮子,其实底层已经内置了」的场景,工程里到处都是。随便举几个:

  • 想给 HTTP 请求加重试——很多 SDK 已经内置指数退避重试
  • 想给数据库连接加连接池——ORM 层多半已经在管
  • 想给多个并发写加串行锁——单文件句柄本身可能就是串行的
  • 想给幂等操作自建去重——上游可能已经有 idempotency-key 机制
  • 想给敏感字段自建加密存储——平台的 keychain / secure enclave 一直在等你调用

这些例子有个共同点:当我处于「解决方案兴奋」状态时,很难主动停下来去查一下「这件事是不是已经被解决过了」。

一个可以粗暴执行的习惯

我给自己加了一个简单的习惯,一句话就能背下来:

凡是准备自建”安全机制”、“补偿机制”、“事务边界”这类东西之前,先花 10 分钟查一下底层 API / SDK / 平台,是不是已经保证了。

不用查多,10 分钟基本能定性——要么找到明确的原子性/幂等/重试保证,要么找到明确说「这里不保证」的文档,你就知道该不该自己上了。

10 分钟的成本,换掉的是:

  • 一层你不需要写的代码
  • 一层你以后要维护的抽象
  • 更重要的——一层可能跟底层重复、甚至互相冲突的机制

因为你自己造的那层如果跟底层”冲突”了,后果会很难查。举个具体的:你以为你的 .tmp + rename 是在保护数据,但如果底层已经在 swap,你的 rename 反而可能把它还没提交的中间状态给暴露出来。多做未必是加固,可能是引入新的失效模式。

收尾

这次排查花掉的时间加起来不到 20 分钟:10 分钟查 MDN,10 分钟改代码。省下的是我原本打算写的一整套自建原子替换逻辑、以及后续跟它有关的测试和维护。

投入产出比这么好的习惯,值得死死记住。

底层不是黑盒。查一下再动手。

—— 马启航Marvis