Flask创建文件夹压缩包返回响应时出现OSError: [Errno 5]输入输出错误
Flask下载ZIP文件夹后出现OSError: [Errno 5] Input/output error的解决方法
问题重现
我在Flask应用中实现了文件夹下载接口,先将文件夹压缩为ZIP包再返回,代码如下:
def download_data(zip_file_name): current_time = datetime.datetime.now().strftime("%Y%m%d%H%M%S") shutil.make_archive(zip_file_name + current_time, 'zip', folder_path) @after_this_request def cleanup(response): try: os.remove(zip_file_name + current_time + ".zip") return response except Exception as ex: LOG.error(f"Error while removing the zipped dataset after downloading {ex}") return send_file(zip_file_name + current_time + ".zip")
API返回200状态码,但日志抛出以下错误:
OSError: [Errno 5] Input/output error data = self.file.read(self.buffer_size) File "/usr/local/lib/python3.10/site-packages/werkzeug/wsgi.py", line 365, in __next__ Traceback (most recent call last): Debugging middleware caught exception in streamed response at a point where response headers were already sent.
注:下载源为OBS存储桶,单个文件下载功能正常。
问题根因
after_this_request回调会在响应头发送后立即执行,但此时send_file的流式传输尚未完成——Werkzeug会分块读取ZIP文件发送给客户端,提前删除文件会导致后续读取操作失败,触发IO错误。
解决方案
方案1:使用响应关闭钩子(最可靠)
Flask的Response对象提供call_on_close方法,该钩子会在响应完全发送、文件句柄关闭后触发,确保文件不再被使用时再删除:
from datetime import datetime import os import shutil from flask import send_file def download_data(zip_file_name): current_time = datetime.now().strftime("%Y%m%d%H%M%S") zip_filename = f"{zip_file_name}{current_time}" zip_path = f"{zip_filename}.zip" # 生成ZIP包 shutil.make_archive(zip_filename, 'zip', folder_path) # 创建响应对象 response = send_file(zip_path) # 定义清理函数 def cleanup_zip(): try: os.remove(zip_path) except Exception as ex: LOG.error(f"Failed to delete zip file: {ex}") # 添加关闭时的清理钩子 response.call_on_close(cleanup_zip) return response
方案2:使用临时文件自动管理
借助tempfile创建临时文件,减少手动文件管理的风险:
import tempfile import shutil from datetime import datetime from flask import send_file def download_data(zip_file_name): # 创建临时文件(delete=False避免自动删除,因为需要先写入ZIP内容) with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as tmp_file: temp_zip_path = tmp_file.name # 生成ZIP到临时文件(注意去掉后缀,make_archive会自动添加.zip) temp_zip_base = temp_zip_path.rsplit('.', 1)[0] shutil.make_archive(temp_zip_base, 'zip', folder_path) response = send_file( temp_zip_path, as_attachment=True, download_name=f"{zip_file_name}_{datetime.now().strftime('%Y%m%d%H%M%S')}.zip" ) def cleanup_temp_zip(): try: os.remove(temp_zip_path) except Exception as ex: LOG.error(f"Failed to delete temp zip: {ex}") response.call_on_close(cleanup_temp_zip) return response
方案3:流式生成ZIP(无本地文件)
如果可以直接从OBS读取文件内容,建议跳过本地ZIP文件生成,直接在内存中流式生成ZIP并发送,彻底避免文件IO问题:
from flask import Response import zipfile from io import BytesIO from datetime import datetime # 需自行实现OBS文件遍历和读取函数 def list_obs_folder_files(obs_folder_path): """遍历OBS文件夹下的所有文件,返回文件路径列表""" pass def read_obs_file(obs_file_path): """读取OBS文件内容,返回字节数据""" pass def generate_zip_stream(obs_folder_path): zip_buffer = BytesIO() with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_obj: for obs_file_path in list_obs_folder_files(obs_folder_path): # 获取ZIP内的相对路径 relative_path = obs_file_path.replace(obs_folder_path, '') # 读取文件内容并写入ZIP file_content = read_obs_file(obs_file_path) zip_obj.writestr(relative_path, file_content) zip_buffer.seek(0) return zip_buffer def download_data(zip_file_name): current_time = datetime.now().strftime("%Y%m%d%H%M%S") zip_stream = generate_zip_stream(folder_path) return Response( zip_stream, mimetype='application/zip', headers={ "Content-Disposition": f"attachment; filename={zip_file_name}{current_time}.zip" } )
内容的提问来源于stack exchange,提问作者Prakash
相关产品推荐
相关产品推荐

