简体   繁体   English

如何在 FastAPI 中读取 UploadFile?

[英]How can I read UploadFile in FastAPI?

I have a FastAPI endpoint that receives a file, uploads it to s3, and then processes it.我有一个 FastAPI 端点,它接收文件,将其上传到 s3,然后对其进行处理。 Everything works fine except for the processing, that fails with this message:一切正常,除了处理失败并显示此消息:

  File "/usr/local/lib/python3.9/site-packages/starlette/datastructures.py", line 441, in read
    return self.file.read(size)
  File "/usr/local/lib/python3.9/tempfile.py", line 735, in read
    return self._file.read(*args)
ValueError: I/O operation on closed file.

My simplified code looks like this:我的简化代码如下所示:

async def process(file: UploadFile):
    reader = csv.reader(iterdecode(file.file.read(), "utf-8"), dialect="excel")  # This fails!
    datarows = []
    for row in reader:
        datarows.append(row)
    return datarows

How can I read the contents of the uploaded file?如何读取上传文件的内容?

UPDATE更新

I managed to isolate the problem a bit more.我设法将问题进一步隔离开来。 Here's my simplified endpoint:这是我的简化端点:

import boto3
from loguru import logger
from botocore.exceptions import ClientError


UPLOAD = True

@router.post("/")
async def upload(file: UploadFile = File(...)):
    if UPLOAD:
        # Upload the file
        s3_client = boto3.client("s3", endpoint_url="http://localstack:4566")
        try:
            s3_client.upload_fileobj(file.file, "local", "myfile.txt")
        except ClientError as e:
            logger.error(e)
    contents = await file.read()
    return JSONResponse({"message": "Success!"})

If UPLOAD is True, I get the error.如果UPLOAD为 True,我会收到错误消息。 If it's not, everything works fine.如果不是,一切正常。 It seems boto3 is closing the file after uploading it.上传后似乎 boto3 正在关闭文件。 Is there any way I can reopen the file?有什么办法可以重新打开文件吗? Or send a copy to upload_fileobj ?或者发送一份副本到upload_fileobj

From FastAPI ImportFile :从 FastAPI ImportFile

Import File and UploadFile from fastapi:从 fastapi 导入文件和上传文件:

from fastapi import FastAPI, File, UploadFile

app = FastAPI()


@app.post("/files/")
async def create_file(file: bytes = File(...)):
    return {"file_size": len(file)}


@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
    return {"filename": file.filename}

From FastAPIUploadFile :从 FastAPIUploadFile

For example, inside of an async path operation function you can get the contents with:例如,在异步路径操作 function 中,您可以通过以下方式获取内容:

contents = await myfile.read()

with your code you should have something like this:使用您的代码,您应该具有以下内容:

async def process(file: UploadFile = File(...)):
    content = await file.read()
    reader = csv.reader(iterdecode(content, "utf-8"), dialect="excel")
    datarows = []
    for row in reader:
        datarows.append(row)
    return datarows

As perFastAPI's documentation , UploadFile uses Python's SpooledTemporaryFile , a "file stored in memory up to a maximum size limit, and after passing this limit it will be stored in disk.".根据FastAPI 的文档,UploadFile 使用 Python 的SpooledTemporaryFile ,“存储在 memory 中的文件达到最大大小限制,超过此限制后它将存储在磁盘中。”。 It "operates exactly as TemporaryFile ", which "is destroyed as soon as it is closed (including an implicit close when the object is garbage collected)".它“与TemporaryFile完全一样运行”,“一旦关闭就会被销毁(包括当 object 被垃圾回收时的隐式关闭)”。 It seems that, once the contents of the file have been read, the file gets closed, which, in turn, causes the file to be deleted.似乎,一旦读取了文件的内容,文件就会关闭,这反过来又会导致文件被删除。

So, option 1 would be to read the file contents, as you already do (ie, contents = await file.read() ), and then upload these bytes to your server, instead of file object (if that's possible).因此,选项 1是读取文件内容,就像您已经做的那样(即contents = await file.read() ),然后将这些字节上传到您的服务器,而不是文件 object (如果可能的话)。

Option 2 would be to copy the contents of the file into a NamedTemporaryFile , which, unlike TemporaryFile , "has a visible name in the file system" that "can be used to open the file".选项 2是将文件的内容复制到NamedTemporaryFile中,与TemporaryFile不同,它“在文件系统中有一个可见的名称”,“可用于打开文件”。 Additionally, it can remain accesible after it is closed , by setting the delete parameter to False ;此外,通过将delete参数设置为False ,它可以在关闭后保持可访问性 thus, allowing the file to reopen when needed.因此,允许文件在需要时重新打开。 Once you have finished with it, you can manually delete it using the remove() or unlink() method.完成后,您可以使用 remove() 或 unlink() 方法手动删除它。 Below is a working example (inspired by this answer ):下面是一个工作示例(受此答案启发):

import uvicorn
from fastapi import FastAPI, File, UploadFile
from tempfile import NamedTemporaryFile
import os


app = FastAPI()

@app.post("/uploadfile/")
async def upload_file(file: UploadFile = File(...)):
    contents = await file.read()

    file_copy = NamedTemporaryFile(delete=False)
    try:
        file_copy.write(contents);  # copy the received file data into a new temp file. 
        file_copy.seek(0)  # move to the beginning of the file
        print(file_copy.read(10))
        
        # Here, upload the file to your S3 service

    finally:
        file_copy.close()  # Remember to close any file instances before removing the temp file
        os.unlink(file_copy.name)  # unlink (remove) the file from the system's Temp folder
    
    # print(contents)  # Handle file contents as desired
    return {"filename": file.filename}
    

if __name__ == '__main__':
    uvicorn.run(app, host='127.0.0.1', port=8000, debug=True)

Update更新

In case the file needs to reopen, before it is closed (while it is being uploaded to your server), and your platform does not allow that (as described here ), then use the following code isntead:如果文件需要在关闭之前重新打开(当它被上传到您的服务器时),并且您的平台不允许这样做(如此所述),请使用以下代码:

@app.post("/uploadfile/")
async def upload_file(file: UploadFile = File(...)):
    contents = await file.read()

    file_copy = NamedTemporaryFile('wb', delete=False)
    f = None
    try:
        # The 'with' block ensures that the file closes and data are stored
        with file_copy as f:
            f.write(contents);
        
        # Here, upload the file to your S3 service
        # You can reopen the file as many times as desired. 
        f = open(file_copy.name, 'rb')
        print(f.read(10))

    finally:
        if f is not None:
            f.close() # Remember to close any file instances before removing the temp file
        os.unlink(file_copy.name)  # unlink (remove) the file from the system's Temp folder
    
    # Handle file contents as desired
    # print(contents)
    return {"filename": file.filename}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM