很多文件分享场景并不需要长期可访问,而是更接近“一次性交付”。例如给协作者临时看一个调试文件、发送一次性账单截图,或者给自动化流程传递一次性输入。这类场景里,普通对象存储更像“长期保存”,而不是“读完即失效”。
最近我在 OkFile 里补了一次文件阅后即焚能力,目标很直接:只要有人第一次成功读取到文件内容,这个文件对象和公开链接就立即失效,而不是等固定时间后过期。
1. 这次实现的目标
这里的“阅后即焚”不是简单的短期过期,也不是只限制下载次数,而是把语义收紧到:
- 第一次成功读取内容后立即失效
- 后续再次访问直接返回过期结果
- 直链、下载页、预览页都遵守同一套失效模型
对应到接口层,上传时只需要增加一个参数:
{
"burnAfterRead": true
}
2. 三种接入方式
2.1 上传页面
手动上传页增加了 Burn after read 选项。勾选后,上传成功返回的普通访问链接、下载链接和预览链接都会进入一次性消费模式。
这个入口适合临时手工分享文件,不需要额外写脚本。
2.2 CLI
CLI 增加了 --burn-after-read 参数,例如:
okfile upload secret.pdf --burn-after-read
如果是通过 Python 入口执行,也可以:
py -3 -m okfile_cli upload secret.pdf --burn-after-read
2.3 API
文件上传的两条路径都支持这个参数。
小文件可以直接走 quick upload:
curl -X POST "https://www.okfile.com/api/upload/quick" \
-F "file=@secret.pdf" \
-F "burnAfterRead=true"
如果是大文件或需要自己控制上传过程,也可以在 prepare 阶段声明:
curl -X POST "https://www.okfile.com/api/upload/prepare" \
-H "Content-Type: application/json" \
--data '{
"filename": "secret.pdf",
"size": 12345,
"contentType": "application/pdf",
"burnAfterRead": true
}'
3. 三类链接如何统一失效
上传完成后,系统通常会返回三类入口:
url:直接文件访问入口downloadUrl:受控下载入口playUrl:图片、视频、PDF 等预览入口
真正需要保证的是,这个文件只允许首次成功读取一次。也就是说,只要打开 /i/{id}、下载 /d/{id},或者访问 /i/{id}?play=1 并成功拿到预览内容,这个文件就应该立即作废,后续其余入口也要一起失效。
4. 预览页是一处容易出错的地方
这次实现里,最容易踩坑的不是直链,而是预览页。因为预览页本身通常只是一个 HTML 壳,真正的文件内容会由页面内部再去请求媒体资源。如果只让后端在原始文件链路上删除对象,而不处理预览页本身,就会出现两个问题:
- 预览页还能反复打开,看起来像没失效
- 浏览器可能把第一次成功返回的媒体内容缓存下来
为了解决这个问题,后面补了两层控制:
4.1 一次性预览令牌
playUrl 不再只是静态预览壳页,而是会在首次访问时发出一次性媒体令牌。令牌成功消费后,再次打开预览页会直接命中过期状态。
4.2 强制禁用缓存
对于一次性文件的预览响应和原始内容响应,增加了更严格的缓存控制,例如:
Cache-Control: private, no-store, max-age=0
Pragma: no-cache
Expires: 0
5. 最终行为
现在统一后的行为可以概括为:
GET /i/{id}:第一次成功读取后失效GET /d/{id}:第一次成功下载后失效GET /i/{id}?play=1:第一次成功预览后失效
后续再次访问时,返回的是过期结果,而不是继续返回原始文件内容。
6. 适用场景与边界
比较适合的场景包括:
- 临时分享敏感文档
- 给外部协作者查看一次性文件
- 给 Agent 或自动化任务传递一次性输入
- 希望缩短链接被转发后的暴露窗口
如果需求更接近“可控访问,但不是一次性”,更适合走过期时间或最大下载次数这类控制方式。
7. 小结
这次改动表面上只是增加了一个 burnAfterRead 参数,但真正需要补齐的是上传页、CLI、API 如何统一暴露这个能力,以及直链、下载页、预览页如何统一失效语义。把这些问题一起补完之后,阅后即焚才更接近真正可用的一次性文件链接,而不是一个只在部分路径上生效的布尔开关。