如何使用FastAPI下载大文件?

5

我想从FastAPI后端下载一个大文件(.tar.gz)。在服务器端,我只是验证了文件路径,然后使用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

如果我返回一个带有.tar文件的StreamingResponse,可能会出现编码问题。

使用 requests 时,请记得设置相同的媒体类型,这里是 media_type='application/octet-stream'。这样就可以正常工作了!


1
这个回答解决了你的问题吗?如何使大文件可供外部API访问? - Chris
我检查了这个答案并使用了StreamingResponse。由于文件类型不同,我没有设置特定的media_type。代码就像这样 return StreamingResponse(iterfile()) 但是当下载tar文件时,我仍然收到错误消息:“无法解码任何json对象”。 - Yuqi Wang
2
你尝试过为StreamingResponse设置media_type='application/octet-stream'来指示它是二进制数据吗?你有失败的示例代码吗? - MatsLindh
这只是我放在数据主体中的某些内容。实际名称为绝对文件路径,例如 /opt/123.tar。我尝试使用一些其他文件,如系统日志或 JSON 文件,它们都可以工作。 - Yuqi Wang
yield from f 中,我发现这可能会使用大量的CPU。我该怎么解决呢?也许原因是块大小太小,导致大量文件操作?我能在这里增加块大小吗? - Yuqi Wang
2个回答

7

如果您发现在使用类似文件对象的StreamingResponse时,yield from f操作相当缓慢:

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/await 的标准 open() 方法,则必须使用普通的 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-使用带有aiofilesasync生成器。
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')

0

我会使用app.mount("/static", StaticFiles(directory="static"), name="static")来挂载一个静态文件夹,并将这个大文件放入该文件夹中,以便用户可以直接下载该大文件的链接。

这样,您就不需要编写代码来读取文件并将文件提供给用户了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接