File Uploads and Downloads in FastAPI: A Comprehensive Guide

Everything you need to know about managing file uploads and downloads in your FastAPI applications

ā€¢

Data exchange is integral to web applications, and the art of handling file uploads, conversion, and downloads is crucial for developers. The application part of managing files comes heavily into play in applications that deal with user-generated content.

Grab a cup of coffee, get comfortable, and relax. This article will guide you through mastering FastAPIā€™s file-handling features. From basics to advanced techniques, weā€™ll cover everything you need to know about managing file uploads and downloads in your applications. Enjoy the learning journey! Here is a brief summary of what we are going to cover in this article.

What is FastApi?

is a high-performing web framework for building APIs with Python 3.7+ based on standard Python-type hints. It helps developers build applications quickly and efficiently. FastAPI is built on top of the Starlette web server and includes features that make building web applications easier, such as automatic data validation, error handling, and interactive API docs.

FastAPIā€™s performance is frequently likened to popular scripting languages like GO and Node.js, and the consensus is clear: itā€™s outstanding. Its speed and efficiency make coding with FastAPI smooth and enjoyable.

Enough talking let's dive deep into code and create a FastApi environment.

Open up a terminal in your system and navigate to your projects directory. For example:

cd path/to/your/project/directory

Create a Virtual Environment

python -m venv venv_name

Replace venv_name with the desired name for your virtual environment.

Activating the Virtual Environments of Different Systems

macOS or Linux

source venv_name/bin/activate

Windows

venv_name\Scripts\activate

If the virtual environment was properly configured, you will see something like this in your terminal:

(myenv) user@your-computer:~/path/to/your/project/directory$

FastApi Installation

The only requirement you need to have is a Python interpreter that is either 3.8 or greater.

Install the following packages using pip:

pip install fastapi

You will also need an ASGI server, and that's where uvicorn kicks in

pip install "uvicorn[standard]"

Testing out if our Server works by implementing a small API:

Create a file called main.py and paste the following code into that file:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

To run the server type this command in the terminal:

uvicorn main:app --reload

After clicking the port number displayed in the terminal, you should observe a similar display in your web browser.


šŸ’” Speed up your blog creation with DifferAI.

Available for free exclusively on the free and open blogging platform, Differ.


Handling File Uploads

Option one

There are two methods for uploading files, but one of them has a limitation. Letā€™s examine both, and you can choose the most suitable one for your needs.

In your main.py, letā€™s create a method called get_file which will be used to test out one of the methods

import shutil

from fastapi import FastAPI, UploadFile, File

app = FastAPI()


@app.post('/files')
def get_file(file: bytes = File(...)):
    content = file.decode('utf-8')
    lines = content.split('\n')
    return {"content": lines}

Note: You need to install a package called python-multipart before running your server again

pip install python-multipart

python-multipart is a Python library that simplifies the handling of HTTP multipart requests, which are commonly used for uploading files through APIs. This library is especially useful when working with frameworks like FastAPI or Flask, where handling file uploads is a common requirement.

Breakdown of the code above:

@app.post('/files'):This line is a FastAPI decorator indicating that the following function will handle POST requests to the ā€˜/filesā€™ endpoint.

  • def get_file(file: bytes = File(...)): This defines a function named get_file that takes a single parameter file.
  • file: bytes specifies that the file parameter is expected to be of type bytes.
  • = File(...) indicates that FastAPI will automatically parse the incoming request and extract the file data. The File(...) dependency is used for handling file uploads in FastAPI.
  • content = file.decode('utf-8'):This line decodes the binary file data (file) into a UTF-8 encoded string, converting the bytes into a human-readable format.
  • decode('utf-8') is used to decode the binary data into a string using the UTF-8 character encoding.

lines = content.split('\n'): This line splits the decoded content into a list of lines based on the newline character ('\n').

  • split('\n') is a string method that divides the string into a list of substrings at each occurrence of the specified newline character.

return {"content": lines} : This line constructs a dictionary with a key "content" and the list of lines (lines) as its value.

  • The function returns this dictionary as the response of the API endpoint.
  • Clients accessing this endpoint will receive a JSON response containing the content of the uploaded file split into lines.

FastApi Interactive Docs

To actually test it out if its working, we will be using FastApiā€™s interactive docs.

FastAPI generates a ā€œschemaā€ with all your APIs using the OpenAPI standard for defining APIs. A ā€œschemaā€ is a definition or description of something. Not the code that implements it, but just an abstract description. The OpenAPI schema is what powers the two interactive documentation systems included in FastAPI.

To see the documentation, just add /docs to the url ([**http://127.0.0.1:8000/docs**](http://127.0.0.1:8000/docs)). This link will show automatic interactive API documentation.

Click on the try it out button and you should be prompted with something like this:

Go ahead and choose a file from your computer and execute it. Essentially this is what you expect to see:

The main drawback of handling file uploads as bytes and then decoding them into a string, as shown in the provided code snippet, is that it may not be suitable for large files. When you decode a large file from bytes to a string, it can consume a significant amount of memory, potentially leading to memory issues, especially in applications where multiple large files are uploaded simultaneously.

Option Two

@app.post('/upload')
def upload_file(uploaded_file: UploadFile = File(...)):
    path = f"files/{uploaded_file.filename}"
    with open(path, 'w+b') as file:
        shutil.copyfileobj(uploaded_file.file, file)

    return {
        'file': uploaded_file.filename,
        'content': uploaded_file.content_type,
        'path': path,
    }

The code above, does the following:

  • @app.post('/upload'): This line is a FastAPI decorator indicating that the following function will handle POST requests to the ā€˜/uploadā€™ endpoint.
  • def upload_file(uploaded_file: UploadFile = File(...)): This defines a function named upload_file that takes a single parameter uploaded_file.
  • uploaded_file: UploadFile specifies that the uploaded_file parameter is expected to be of type UploadFile. FastAPI's UploadFile class is used to handle uploaded files.
  • path = f"files/{uploaded_file.filename}" This line constructs the path where the uploaded file will be saved. It uses an f-string to create a path in the ā€˜filesā€™ directory with the original filename of the uploaded file.
  • with open(path, 'w+b') as file:This line opens a new file with write and binary mode (ā€˜w+bā€™) at the specified path.
  • 'w+b' mode ensures that the file is opened in binary mode, suitable for writing binary data.
  • shutil.copyfileobj(uploaded_file.file, file) This line uses the shutil.copyfileobj() function to copy the contents of the uploaded file (uploaded_file.file) to the newly created file (file). It efficiently copies the binary data from the uploaded file to the local file on the server.
  • shutil.copyfileobj(uploaded_file.file, file) This line uses the shutil.copyfileobj() function to copy the contents of the uploaded file (uploaded_file.file) to the newly created file (file).
  • It efficiently copies the binary data from the uploaded file to the local file on the server.

When you go back to the docs, a new endpoint should be displayed and ready for testing.

If the configuration is correct, you should be able to upload a file from your system, and the uploaded file will be stored inside your projectā€™s directory, similar to the following example:

Accessing Static Files

In most cases, serving static files is a common use case in most web applications and FastApi provides a really simple way to make files statically available. Here is how you can achieve this:

Importing the necessary module

from fastapi.staticfiles import StaticFiles

Mount static files directory

app.mount('/files', StaticFiles(directory='files'),'files')

Testing out

It works!!!

File Integrity: Validating File Types Based on Content

Content-based file validation is a crucial aspect of handling file uploads in web applications. Unlike relying solely on file extensions, content-based validation ensures that uploaded files are of the expected type by examining their actual content. This approach adds an extra layer of security and accuracy, preventing malicious uploads and enhancing user experience.

For this example, we will validate that the only file we can be able to upload is that of a valid JSON file.

import shutil
import json
from fastapi import FastAPI, UploadFile, File, status
from fastapi.staticfiles import StaticFiles
from fastapi.exceptions import HTTPException

@app.post('/upload/file')
def upload_json(file:UploadFile = File(...)):
    if file.content_type != 'application/json':
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Wow, That's not allowed")
    data = json.loads(file.file.read())
    return {
        "filename":file.filename,
        "content":data
    }

Breakdown of the code above:

  • @app.post('/upload/file'): This line is a FastAPI decorator indicating that the following function will handle POST requests to the ā€˜/upload/fileā€™ endpoint.
  • def upload_json(file: UploadFile = File(...)):
  • This defines a function named upload_json that takes a single parameter file.
  • file: UploadFile = File(...) specifies that the file parameter is expected to be of type UploadFile, which is used to handle uploaded files
  • = File(...) indicates that FastAPI will automatically parse the incoming request and extract the file data. The File(...) dependency is used for handling file uploads in FastAPI.
  • Content Type Validation:if file.content_type != 'application/json': This line checks if the uploaded fileā€™s content type is not ā€˜application/jsonā€™. If the uploaded file is not a JSON file, it raises an HTTPException with a 400 Bad Request status code and a custom error message: ā€œWow, Thatā€™s not allowedā€.
  • Loading JSON Data:data = json.loads(file.file.read())
  • If the uploaded file is determined to be a JSON file, this line reads the file content and loads it as a JSON object using json.loads(). file.file.read() reads the binary data of the uploaded file. json.loads() parses the JSON data into a Python dictionary.
  • Response: return {"filename": file.filename, "content": data} If the file is a valid JSON file, the function returns a dictionary containing the filename and the loaded JSON content. This response will be sent back to the client.

Testing out

That works as expected!!

Uploading and handling File uploads

Weā€™ve explored methods for uploading files and ensuring their static availability. Now, letā€™s focus on enabling interactive downloads in specific formats. In this case, letā€™s examine how to facilitate the conversion from a JSON file to a YAML file.

import shutil
import json
import time

import yaml
from fastapi import FastAPI, UploadFile, File, status
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.exceptions import HTTPException
import os

app = FastAPI()


time_str = time.strftime('%Y-%m%d - %H%M%S')
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
UPLOAD_DIR = os.path.join(BASE_DIR, "uploads")


@app.post('/upload/download')
def upload_download(file:UploadFile = File(...)):
    if file.content_type != 'application/json':
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Wow, that's not allowed")
    data = json.loads(file.file.read())
    new_filename =  "{}_{}.yaml".format(os.path.splitext(file.filename)[0], time_str)
    SAVE_F = os.path.join(UPLOAD_DIR, new_filename)
    with open(SAVE_F, "w") as f:
        yaml.dump(data,f)
    return FileResponse(path=SAVE_F, media_type="application/octet-stream", filename=new_filename)

Break down of the code above:

  • This code defines a POST request endpoint at the path ā€˜/upload/downloadā€™. It expects an uploaded file of type UploadFile.
  • This part checks whether the uploaded file is of JSON type. If the fileā€™s content type is not ā€˜application/jsonā€™, it raises a Bad Request HTTPException with a message indicating that the uploaded file type is not allowed.
  • This line reads the content of the uploaded JSON file and loads it into a Python dictionary using json.loads(). The file.file attribute represents the file object.
  • new_filename is created by combining the original filename (without extension) with the timestamp generated earlier. This new filename has a '.yaml' extension.
  • SAVE_F represents the complete path where the converted YAML file will be saved, including the 'uploads' directory.
  • The code then opens the file specified by SAVE_F in write mode and uses yaml.dump() to write the Python dictionary data into the YAML file.
  • Finally, the function returns a FileResponse object. It specifies the path to the saved YAML file (path=SAVE_F), sets the media type to "application/octet-stream", indicating a binary file, and provides the desired filename for the downloaded file (filename=new_filename).

Note: you should create a uploads directory before testing out whether it works

After an instance of running your server, and testing it out with a JSON file, this is what you should see.

Everything to the output will work as expected.

Conclusion

This article delved into the intricacies of handling file uploads and downloads in FastAPI, a modern and efficient Python web framework. Here are the key takeaways:

  • File Handling: We explored different methods of handling file uploads and downloads, covering various scenarios such as validating file types, processing content-based validation, and dealing with large files.
  • Content-Based Validation: Content-based validation emerged as a crucial technique, allowing developers to validate files based on their actual content rather than relying solely on file extensions. This enhances security and ensures the integrity of uploaded files.
  • Integration of Python Libraries: We discussed how to integrate Python libraries like shutil for efficient file copying, and streamlining the file handling process.
  • Static File Serving: Additionally, we covered serving static files using FastAPI, enabling developers to make images, stylesheets, and scripts directly accessible to clients, enhancing the user experience.
  • Robust Error Handling: Proper error handling, including HTTP status codes and meaningful error messages.

As you wrap up this insightful article, I want to share something extraordinary with you. Beyond crafting compelling content, I lead a freelance group specializing in innovative software solutions. šŸŒāœØ

Imagine having a website that not only looks stunning but functions seamlessly, APIs that enhance your operations, and the power of data visualization and machine learning at your fingertips. Thatā€™s what we bring to the table! šŸ’»šŸš€

Whether youā€™re a visionary entrepreneur or a growing SME, our team is here to turn your tech dreams into reality. Letā€™s redefine whatā€™s possible. Connect with us on Gmail, LinkedIn, or WhatsApp, and letā€™s embark on a journey of digital transformation together. Your success story begins now!

Reach out to us at:

Gmail: mbs.team.general@gmail.com

LinkedIn: https://www.linkedin.com/in/nourmibrahimmbs/

Enjoyed this article?

Share it with your network to help others discover it

Continue Learning

Discover more articles on similar topics