Using fastAPI UploadFile with libraries that accept a file-like object

Viewed 1431

I would like to know the best practice for accessing the underlying file object when uploading a file with fastAPI.

When uploading a file with fastapi, the object we get is a starlette.datastructures.UploadFile. We can access the underlying file attribute which is a tempfile.SpooledTemporaryFile. We can then access the underlying private _file attribute, and pass it to libraries that expect a file-like object.

Below is an example of two routes with python-docx and pdftotext:

@router.post("/open-docx")
async def open_docx(upload_file: UploadFile = File(...)):
    mydoc = docx.Document(upload_file.file._file) # 
    # do something

@router.post("/open-pdf")
async def open_pdf(upload_file: UploadFile = File(...)):
    mypdf = pdftotext.PDF(upload_file.file._file)
    # do something

However, I don't like the idea of accessing the private _file attribute. Is there a better way to pass the file object without saving it first?

Note: to reproduce the example above, put the following code in launch.py and run uvicorn launch:app --reload:

import docx
import pdftotext
from fastapi import FastAPI, File, UploadFile


app = FastAPI()

@app.post("/open-docx")
async def open_docx(upload_file: UploadFile = File(...)):
    mydoc = docx.Document(upload_file.file._file)
    # do something
    return {"firstparagraph": mydoc.paragraphs[0].text}

@app.post("/open-pdf")
async def open_pdf(upload_file: UploadFile = File(...)):
    mypdf = pdftotext.PDF(upload_file.file._file)
    # do something
    return {"firstpage": mypdf[0]}
2 Answers

Note that this is untested. Per the docs you can use the SpooledTemporaryFile object without accessing its underlying wrapper:

The returned object is a file-like object whose _file attribute is either an io.BytesIO or io.TextIOWrapper object (depending on whether binary or text mode was specified) or a true file object, depending on whether rollover() has been called. This file-like object can be used in a with statement, just like a normal file.

https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile (Emphasis added).

Thus it would seem you can just do:

@router.post("/open-docx")
async def open_docx(upload_file: UploadFile = File(...)):
    with upload_file.file as f:
        mydoc = docx.Document(f)

Avoid the with if you don't want to close the file.

Edit: Seekable files

SpooledTemporaryFiles are not properly seekable, sadly. See this question. It boils down to the fact that they roll over and land on the disk after a certain point, at which point seeking is harder. Accessing the _file attribute is not safe because of this rollover. Thus you either need to save them to disk, or to read them explicitly into a virtual (ram) file.

If you are on Linux, a workaround might be to save the files to /dev/shm or /tmp, the former of which is in ram, the latter often a ramdisk, and let the OS handle swapping massive files to disk.

See documentation for UploadFile here: https://fastapi.tiangolo.com/tutorial/request-files/#uploadfile

It says it has an attribute named

file: A SpooledTemporaryFile (a file-like object). 
This is the actual Python file that you can pass directly to 
other functions or libraries that expect a "file-like" object.

So you dont need to access the private ._file attribute. Just pass the

upload_file.file
Related