如何使用 FastAPI 下载大文件?
- 2025-02-08 08:52:00
- admin 原创
- 56
问题描述:
我正在尝试.tar.gz
从 FastAPI 后端下载一个大文件 ()。在服务器端,我只需验证文件路径,然后使用Starlette.FileResponse
返回整个文件 — 就像我在 StackOverflow 上的许多相关问题中看到的那样。
服务器端:
return FileResponse(path=file_name, media_type='application/octet-stream', filename=file_name)
此后,我收到以下错误:
File "/usr/local/lib/python3.10/dist-packages/fastapi/routing.py", line 149, in serialize_response
return jsonable_encoder(response_content)
File "/usr/local/lib/python3.10/dist-packages/fastapi/encoders.py", line 130, in jsonable_encoder
return ENCODERS_BY_TYPE[type(obj)](obj)
File "pydantic/json.py", line 52, in pydantic.json.lambda
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8b in position 1: invalid start byte
我也尝试使用StreamingResponse
,但出现同样的错误。还有其他方法吗?
我的代码中StreamingResponse
:
@x.post("/download")
async def download(file_name=Body(), token: str | None = Header(default=None)):
file_name = file_name["file_name"]
# should be something like xx.tar
def iterfile():
with open(file_name,"rb") as f:
yield from f
return StreamingResponse(iterfile(),media_type='application/octet-stream')
好的,这是这个问题的更新。我发现错误不是发生在这个api上,而是api在执行这个转发请求。
@("/")
def f():
req = requests.post(url ="/download")
return req.content
如果我返回一个StreamingResponse
带有.tar
文件的文件,则可能导致编码问题。
使用请求时,请记住设置相同的媒体类型。这是media_type='application/octet-stream'
。它有效!
解决方案 1:
如果您发现使用类文件对象yield from f
时速度相当慢,例如:StreamingResponse
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
some_file_path = 'large-video-file.mp4'
app = FastAPI()
@app.get('/')
def main():
def iterfile():
with open(some_file_path, mode='rb') as f:
yield from f
return StreamingResponse(iterfile(), media_type='video/mp4')
您可以创建一个生成器,使用指定的块大小分块读取文件;从而加快该过程。示例如下。
请注意,StreamingResponse
可以使用async
生成器或普通的生成器/迭代器来流式传输响应主体。如果您使用open()
不支持async
/ 的标准方法await
,则必须使用 normal 声明生成器函数def
。无论如何,FastAPI/Starlette 仍将异步工作,因为它将检查您传递的生成器是否是异步的(如源代码中所示),如果不是,它将使用 在单独的线程中运行生成器,iterate_in_threadpool
然后等待。
Content-Disposition
您可以在响应中设置标题(如此答案中所述,以及此处和此处)以指示内容是否预计显示在inline
浏览器中(如果您正在流式传输,例如.mp4
视频,.mp3
音频文件等),或者作为下载并本地保存的内容(attachment
使用指定的filename
)。
至于media_type
(也称为 MIME 类型),有两种主要的 MIME 类型(请参阅常见的 MIME 类型):
text/plain
是文本文件的默认值。文本文件应易于阅读,且不得包含二进制数据。
application/octet-stream
是所有其他情况的默认值。未知文件类型应使用此类型。
对于带有扩展名的文件.tar
,如您的问题所示,您还可以使用不同于 的子类型,octet-stream
即x-tar
。否则,如果文件类型未知,请坚持使用application/octet-stream
。有关常见 MIME 类型的列表,请参阅上面链接的文档。
选项 1 - 使用普通发电机
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
CHUNK_SIZE = 1024 * 1024 # = 1MB - adjust the chunk size as desired
some_file_path = 'large_file.tar'
app = FastAPI()
@app.get('/')
def main():
def iterfile():
with open(some_file_path, 'rb') as f:
while chunk := f.read(CHUNK_SIZE):
yield chunk
headers = {'Content-Disposition': 'attachment; filename="large_file.tar"'}
return StreamingResponse(iterfile(), headers=headers, media_type='application/x-tar')
选项 2 - 使用async
生成器aiofiles
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import aiofiles
CHUNK_SIZE = 1024 * 1024 # = 1MB - adjust the chunk size as desired
some_file_path = 'large_file.tar'
app = FastAPI()
@app.get('/')
async def main():
async def iterfile():
async with aiofiles.open(some_file_path, 'rb') as f:
while chunk := await f.read(CHUNK_SIZE):
yield chunk
headers = {'Content-Disposition': 'attachment; filename="large_file.tar"'}
return StreamingResponse(iterfile(), headers=headers, media_type='application/x-tar')
解决方案 2:
我会用来app.mount("/static", StaticFiles(directory="static"), name="static")
挂载一个静态文件夹,并将这个大文件放入该文件夹,这样用户就可以直接下载这个大文件的链接。
这样,您就不需要代码来读取文件并将文件提供给用户。