42

I accept the file via POST. When I save it locally, I can read the content using file.read (), but the name via file.name incorrect(16) is displayed. When I try to find it by this name, I get an error. What might be the problem?

My code:

  @router.post(
    path="/upload",
    response_model=schema.ContentUploadedResponse,
)
async def upload_file(
        background_tasks: BackgroundTasks,
        uploaded_file: UploadFile = File(...)):
    uploaded_file.file.rollover()
    uploaded_file.file.flush()
    #shutil.copy(uploaded_file.file.name, f'../api/{uploaded_file.filename}')
    background_tasks.add_task(s3_upload, uploaded_file=fp)
    return schema.ContentUploadedResponse()
Fyzzys
  • 756
  • 1
  • 7
  • 13
  • A noob to python. Can anyone please tell me the meaning of `uploaded_file.file.flush()`? Thank you. – Koushik Das Feb 24 '22 at 07:13
  • Please have a look at [**this answer**](https://stackoverflow.com/a/70657621/17865804), if you are using a `def` endpoint, as well as [**this answer**](https://stackoverflow.com/a/70667530/17865804), if you are using an `async def` endpoint. – Chris Aug 17 '22 at 11:23

8 Answers8

57

Background

UploadFile is just a wrapper around SpooledTemporaryFile, which can be accessed as UploadFile.file.

SpooledTemporaryFile() [...] function operates exactly as TemporaryFile() does

And documentation about TemporaryFile says:

Return a file-like object that can be used as a temporary storage area. [..] It will be destroyed as soon as it is closed (including an implicit close when the object is garbage collected). Under Unix, the directory entry for the file is either not created at all or is removed immediately after the file is created. Other platforms do not support this; your code should not rely on a temporary file created using this function having or not having a visible name in the file system.

async def endpoint

You should use the following async methods of UploadFile: write, read, seek and close. They are executed in a thread pool and awaited asynchronously.

For async writing files to disk you can use aiofiles. Example:

@app.post("/")
async def post_endpoint(in_file: UploadFile=File(...)):
    # ...
    async with aiofiles.open(out_file_path, 'wb') as out_file:
        content = await in_file.read()  # async read
        await out_file.write(content)  # async write

    return {"Result": "OK"}

Or in the chunked manner, so as not to load the entire file into memory:

@app.post("/")
async def post_endpoint(in_file: UploadFile=File(...)):
    # ...
    async with aiofiles.open(out_file_path, 'wb') as out_file:
        while content := await in_file.read(1024):  # async read chunk
            await out_file.write(content)  # async write chunk

    return {"Result": "OK"}

def endpoint

Also, I would like to cite several useful utility functions from this topic (all credits @dmontagu) using shutil.copyfileobj with internal UploadFile.file. This functions can be invoked from def endpoints:

import shutil
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Callable

from fastapi import UploadFile


def save_upload_file(upload_file: UploadFile, destination: Path) -> None:
    try:
        with destination.open("wb") as buffer:
            shutil.copyfileobj(upload_file.file, buffer)
    finally:
        upload_file.file.close()


def save_upload_file_tmp(upload_file: UploadFile) -> Path:
    try:
        suffix = Path(upload_file.filename).suffix
        with NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
            shutil.copyfileobj(upload_file.file, tmp)
            tmp_path = Path(tmp.name)
    finally:
        upload_file.file.close()
    return tmp_path


def handle_upload_file(
    upload_file: UploadFile, handler: Callable[[Path], None]
) -> None:
    tmp_path = save_upload_file_tmp(upload_file)
    try:
        handler(tmp_path)  # Do something with the saved temp file
    finally:
        tmp_path.unlink()  # Delete the temp file

Note: you'd want to use the above functions inside of def endpoints, not async def, since they make use of blocking APIs.

alex_noname
  • 26,459
  • 5
  • 69
  • 86
  • Why you use `async` for this task? FastApi tells that not asynchronus endpoints anyway will be runned via `async`, so as I get it, if you don't have another operations after your async call - you should NOt to use `async` https://fastapi.tiangolo.com/async/ – Давид Шико May 24 '21 at 14:39
  • you are missing a `:` behind `while content := await in_file.read(1024)` – M.Winkens Jul 08 '21 at 15:27
  • Can you please add a snippet for `aiofiles.tempfile.TemporaryFile` so we can first store file in temporary location and can raise error for various validations. If all validations are passed we can move this temp file to our storage. Regards. – JD Solanki Aug 26 '21 at 11:37
18

You can save the uploaded files this way,

from fastapi import FastAPI, File, UploadFile

app = FastAPI()


@app.post("/upload-file/")
async def create_upload_file(uploaded_file: UploadFile = File(...)):
    file_location = f"files/{uploaded_file.filename}"
    with open(file_location, "wb+") as file_object:
        file_object.write(uploaded_file.file.read())
    return {"info": f"file '{uploaded_file.filename}' saved at '{file_location}'"}

You can also use the shutil.copyfileobj(...) method (see this detailed answer to how both are working behind the scenes).

So, as an alternative way, you can write something like the below using the shutil.copyfileobj(...) to achieve the file upload functionality.

import shutil
from fastapi import FastAPI, File, UploadFile

app = FastAPI()


@app.post("/upload-file/")
async def create_upload_file(uploaded_file: UploadFile = File(...)):    
file_location = f"files/{uploaded_file.filename}"
    with open(file_location, "wb+") as file_object:
        shutil.copyfileobj(uploaded_file.file, file_object)    
return {"info": f"file '{uploaded_file.filename}' saved at '{file_location}'"}
JPG
  • 82,442
  • 19
  • 127
  • 206
  • The two functions above are **not** equivalent. In the first one, the entire contents of the file will be read into memory, whereas, in the second one, the data will be read in chunks (this is the default behaviour). Please have a look at [this answer](https://stackoverflow.com/a/73365632/17865804) for more details. – Chris Aug 17 '22 at 12:13
  • 1
    Indeed your answer is wonderful, I appreciate it. But, I didn't say they are "equivalent", but *"almost* identical" – JPG Aug 17 '22 at 15:46
  • _"almost identical"_ might be too vague for the reader, and _"the above function can be re-written"_ implies writing the same function in a diiferent way. I think it is important for future readers to know the difference between the two. – Chris Aug 17 '22 at 16:18
  • 1
    I completely get it. I just updated my answer, I hope now it's better. – JPG Aug 17 '22 at 16:42
  • As a final touch-up, you may want to replace `async def` with `def`, as both functions perform blocking I/O operations that would block the main thread. – Chris Aug 17 '22 at 18:12
3

In my case, I need to handle huge files, so I must avoid reading them all into memory. What I want is to save them to disk asynchronously, in chunks.

I'm experimenting with this and it seems to do the job (CHUNK_SIZE is quite arbitrarily chosen, further tests are needed to find an optimal size):

import os
import logging

from fastapi import FastAPI, BackgroundTasks, File, UploadFile

log = logging.getLogger(__name__)

app = FastAPI()

DESTINATION = "/"
CHUNK_SIZE = 2 ** 20  # 1MB


async def chunked_copy(src, dst):
    await src.seek(0)
    with open(dst, "wb") as buffer:
        while True:
            contents = await src.read(CHUNK_SIZE)
            if not contents:
                log.info(f"Src completely consumed\n")
                break
            log.info(f"Consumed {len(contents)} bytes from Src file\n")
            buffer.write(contents)


@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
    fullpath = os.path.join(DESTINATION, file.filename)
    await chunked_copy(file, fullpath)
    return {"File saved to disk at": fullpath}

However, I'm quickly realizing that create_upload_file is not invoked until the file has been completely received. So, if this code snippet is correct it will probably be beneficial to performance but will not enable anything like providing feedback to the client about the progress of the upload and it will perform a full data copy in the server. It seems silly to not be able to just access the original UploadFile temporary file, flush it and just move it somewhere else, thus avoiding a copy.

benelgiac
  • 941
  • 6
  • 10
  • 1
    You can access `.stream()`, which provides the byte chunks without storing the entire file into memory (even though a large file is never stored entirely into memory, as Starlette currently uses a `SpooledTemporaryFile` with the `max_size` set to 1MB, meaning that the file data is spooled in memory until the file size exceeds 1MB, at which point the contents are written to a temporary file on disk). Please have a look at [this answer](https://stackoverflow.com/a/73443824/17865804) and [this answer](https://stackoverflow.com/a/70667530/17865804) for more details and code examples. – Chris Aug 30 '22 at 05:06
1

Just did this to upload a file and works fine.

from fastapi import APIRouter, File, status, Depends, HTTPException,  UploadFile

import shutil
from pathlib import Path

from database.user_functions import *
from database.auth_functions import *
from database.form_functions import *

from model import *
from model_form import *

file_routes = APIRouter()


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


# @file_routes.post("/uploadfile/")
# async def create_upload_file(file: UploadFile):
#     return {"filename": file.filename}


@file_routes.post("/upload-file/")
async def create_upload_file(uploaded_file: UploadFile = File(...)):    

    file_location = f"./{uploaded_file.filename}"
    with open(file_location, "wb+") as file_object:
        shutil.copyfileobj(uploaded_file.file, file_object)    
    return {"info": f"file '{uploaded_file.filename}' saved at '{file_location}'"}

tbh I did found it on a medium article

Aymen Azoui
  • 369
  • 2
  • 4
0

you can save the file by copying and pasting the below code.

 fastapi import (
    FastAPI
    UploadFile,
    File,
    status
)
from fastapi.responses import JSONResponse

import aiofiles
app = FastAPI( debug = True ) 

@app.post("/upload_file/", response_description="", response_model = "")
async def result(file:UploadFile = File(...)):
     try:
        async with aiofiles.open(file.filename, 'wb') as out_file:
            content = await file.read()  # async read
            await out_file.write(content)  # async write

    except Exception as e:
        return JSONResponse(
            status_code = status.HTTP_400_BAD_REQUEST,
            content = { 'message' : str(e) }
            )
    else:
        return JSONResponse(
            status_code = status.HTTP_200_OK,
            content = {"result":'success'}
            )

If you wanted to upload the multiple file then copy paste the below code

 fastapi import (
    FastAPI
    UploadFile,
    File,
    status
)
from fastapi.responses import JSONResponse

import aiofiles
app = FastAPI( debug = True ) 
@router.post("/upload_multiple_file/", response_description="", response_model = "")

async def result(files:List[UploadFile] = File(...), secret_key: str = Depends(secretkey_middleware)):
    try:
        
        for file in files:

            async with aiofiles.open(eventid+file.filename, 'wb') as out_file:
                content = await file.read() 
                await out_file.write(content) 
                


        pass
    except Exception as e:
      
        return JSONResponse(
            status_code = status.HTTP_400_BAD_REQUEST,
            content = { 'message' : str(e) }
            )
    else:
        return JSONResponse(
            status_code = status.HTTP_200_OK,
            content = {"result":'result'}
            )

0

use this helper function to save the file

from fastapi import UploadFile

import shutil
from pathlib import Path

def save_upload_file(upload_file: UploadFile, destination: Path) -> str:
    try:
        with destination.open("wb") as buffer:
            shutil.copyfileobj(upload_file.file, buffer)
            file_name = buffer.name
            print(type(file_name))
    finally:
        upload_file.file.close()
    return file_name

use this function to give a unique name to each save file, assuming you will be saving more than one file

def unique_id():
    return str(uuid.uuid4())

def delete_file(filename):
    os.remove(filename)

in your endpoint

@router.post("/use_upload_file", response_model=dict)
async def use_uploaded_file(
    file_one: UploadFile = File(),
    file_two: UploadFile = File()
    ):


    file_one_path = save_upload_file(audio_one, Path(f"{unique_id()}"))
    file_two_path = save_upload_file(audio_two, Path(f"{unique_id()}"))

    result = YourFunctionThatUsestheSaveFile(audio_one_path, audio_two_path)

    delete_file(audio_one_path)
    delete_file(audio_two_path)

    return result
0

Code to upload file in fast-API through Endpoints (post request):

@router.post(path="/test", tags=['File Upload'])
def color_classification_predict(uploadFile: UploadFile):
    try:
        if uploadFile.filename:
            # saved_dir- directory path where we'll save the uploaded file 
            test_filename = os.path.join(saved_dir, uploadFile.filename)
            with open(test_filename, "wb+") as file_object:
                shutil.copyfileobj(uploadFile.file, file_object)
    except Exception as e:
        raise e
    print('[INFO] Uploaded file saved.')
0

the accepted answer is good, I just want to point out something that cause me an error when i try to save the file make sure the destination path either you start with ./my_folder_destination or just start with the name of the folder example my_folder_destionation

Here is an example

import shutil
from datetime import datetime, timedelta
from fastapi import UploadFile


@app.post("/upload-image")
def upload_image(image: UploadFile):
    now = str(datetime.now())[:19]
    now = now.replace(":", "_")
    #this is just to make sure the file not exist and added as a new file
    path = "./static/images/" + image.filename.split('.')[0] + now + "." + image.filename.split('.')[-1]
    with open(path, 'wb+') as buffer:
        shutil.copyfileobj(image.file, buffer)

    return {"image_destination":path}

Also last thing i want to point out if get a FileNotFoundError: [Errno 2] No such file or directory: some/directory it means that directory is not exist so check if you have / at the start of your directory path and remove it or replace it with ./

Also for those who wanted to just move the file to a new directory here is code below

import shutil
from datetime import datetime, timedelta
from fastapi import UploadFile


@app.post("/upload-image")
def upload_image(image: UploadFile):
    now = str(datetime.now())[:19]
    now = now.replace(":", "_")
    #this is just to make sure the file not exist and added as a new file
    path = "./static/images/" + image.filename.split('.')[0] + now + "." + image.filename.split('.')[-1]
    shutil.move(image.filename, path)

    return {"image_destination":path}

so basically i just not read the file location i just move it to the location i want to move it Hope it helps :D

jasjastone
  • 11
  • 3