Flask 的 FileStorage 对象在调用 save() 后内部文件指针会移动到末尾且不可重置,导致后续 save() 调用写入空内容;正确做法是重置文件指针或使用 stream.read() + BytesIO 重新构造可复用的文件对象。
flask 的 filestorage 对象在调用 `save()` 后内部文件指针会移动到末尾且不可重置,导致后续 `save()` 调用写入空内容;正确做法是重置文件指针或使用 `stream.read()` + `bytesio` 重新构造可复用的文件对象。
在 Flask 应用中处理前端上传的文件时,开发者常需对同一 FileStorage 对象执行多次操作(如计算哈希、保存副本)。但直接连续调用 file.save() 会导致第二次保存生成空文件——这并非 Bug,而是 FileStorage 的设计行为所致。
? 根本原因分析
FileStorage.save() 本质是将底层 stream(通常是 io.BytesIO 或临时文件句柄)的内容写入磁盘,并自动将流指针移动至末尾。当再次调用 save() 时,流已无剩余数据可读,因此写入空文件。即使 file.seek(0) 在部分环境下看似可行,但 FileStorage 的 stream 类型不保证支持 seek()(尤其当底层为 SpooledTemporaryFile 且已溢出到磁盘时),故不可靠。
✅ 推荐解决方案:重置流 + 内存复用(推荐)
避免依赖 shutil.copyfile(需额外临时路径)或反复磁盘 I/O,最优实践是一次性读取全部内容,再分别用于哈希计算和保存:
import io
from werkzeug.datastructures import FileStorage
def handle_uploaded_file(file: FileStorage, mandant: str) -> str:
# 1. 一次性读取全部字节(安全、高效)
file_bytes = file.read() # 注意:此操作消耗流
# 2. 计算 MD5 哈希
hash_ = hashlib.md5(file_bytes).hexdigest()
# 3. 构造可复用的 BytesIO 流供多次 save()
file_stream = io.BytesIO(file_bytes)
file_for_save = FileStorage(
stream=file_stream,
filename=file.filename,
content_type=file.content_type
)
# 4. 保存到目标路径
file_path = os.path.join(config('UPLOAD_FOLDER'), mandant, f"{hash_}.pdf")
os.makedirs(os.path.dirname(file_path), exist_ok=True)
file_for_save.save(file_path)
return file_path
⚠️ 注意事项
- 切勿在 file.read() 后再调用 file.save():read() 已耗尽流,save() 将写入空内容。
- 避免 file.seek(0) 作为通用解法:SpooledTemporaryFile 在内存不足时会切换为磁盘模式,此时 seek(0) 可能失败或无效。
- 若必须分步操作(如大文件不能全加载内存),应使用 file.stream.seek(0) 配合 file.stream.read(),但需提前确认流支持 seek(可通过 hasattr(file.stream, 'seek') 判断)。
? 总结
核心原则是:FileStorage 是单次消费型对象。生产环境应优先采用「一次读取 → 多次复用」模式,既规避指针问题,又提升性能。临时文件方案(如 shutil.copyfile)虽可行,但增加磁盘开销与路径管理复杂度,非必要不推荐。











