科技 · 实用指南
Python SpooledTemporaryFile意外落盘或读空?审计rollover与游标
在把spooled upload交给库或子进程前,分清隐藏的fileno落盘与EOF游标。

SpooledTemporaryFile有两个独立陷阱:消费者调用fileno()会让很小的payload落盘;写入后游标停在EOF,又会让下游读到空数据。
只用公开API定位rollover触发器,交接前seek(0),明确bytes或descriptor合同,并在真实消费者边界同时验证字节与存储策略。
1. 分开存储状态与流位置
记录交给消费者的对象、max_size、写入字节、tell()与任何fileno()调用。spool可仍在内存却停在EOF,也可已落盘但仍停在同一EOF。
不要把私有_file或_rolled当合同;在隔离测试环境观察临时目录,并只包装自己控制的consumer边界以找出descriptor请求。
2. 说清rollover触发器
Python记录三条路径:超过非零max_size、调用fileno()、显式rollover()。默认max_size=0在当前CPython不按大小触发,但后两者仍会落盘。
审计真实library/framework/subprocess;只收descriptor的API可能暗中调用fileno()。若政策禁止磁盘,它就与该政策不兼容。
3. 交接前重置公开游标
写完后,下一个reader读取同一对象前必须seek(0)。CPython问题里的空stdin正是游标在结尾,而不是对象无效。rollover不会承诺rewind。
FastAPI UploadFile异步代码用await upload.seek(0);同步代码按API合同使用公开file方法,并明确最后close的owner。
4. 选择一种交接合同
consumer接受bytes时,可用pipe、BytesIO或普通read结果保持不落盘。它若要求真实descriptor,就显式rollover、确认临时空间、seek(0)、交接并由约定owner关闭。
需要持久输出时复制到调用方选定路径,并明确fsync或atomic replace;临时文件关闭后会消失,不是持久化接口。
5. 在真实边界测试两个轴
写入独特payload,记录tell(),rewind,调用真实consumer并核对字节或解析结果。分别跑阈值以下与强制rollover用例。
增加记录fileno()调用的test double与隔离的临时目录观察。验收必须同时证明完整交付、磁盘使用符合政策,并单独断言close清理。
