Storing Images in SQLite Database with Flask and Sending to React Frontend

flaskbase64react

Saturday, February 10, 2024

I recently finished building a full-stack application that utilized React and Flask, a micro web framework for Python that allows you to build web applications quickly and with minimal boilerplate code.

The application required me to store images in my SQLite database that were uploaded from my React frontend. I spent some time trying to figure out how to create functionality that would allow me to store these image uploads and send them back to my frontend to display.

In this article, I’ll share my process for handling the file uploads within the Flask web application and how I encoded these images into Base64 format URLs to display them in my React frontend.

Strategy for Storing Image Uploads:

Initially, I planned to store binary image data directly in the database. However, after learning about potential memory issues, I decided to store images in a designated folder and save their filenames in the database for reference. I read this blog by Brodie Ashcraft which guided me on how to accomplish this.

In my app.config file, I created an Images directory to store all of the images uploaded from the frontend. I then created a helper function encode_image_directory() to scan the Images folder and encode each image file into base64 format. This function returned a dictionary where the keys are the filenames and the values are base64-encoded strings of the corresponding images. Here’s a snippet of the code I used to achieve this:

app.config["Images"] = "Images"

def encode_imgage_directory():
    image_directory = os.path.join(app.config["Images"])
    image_files = os.listdir(image_directory)

    images = {}
    for filename in image_files:
        file_path = os.path.join(image_directory, filename)
        with open(file_path, 'rb') as f:
            encoded_image = base64.b64encode(f.read()).decode('utf-8')
            encoded_image = encoded_image.replace('\n', '')
            image_url = 'data:image/jpg;base64,' + encoded_image
            images[filename] = image_url
    return images

API Implementation:

In my Flask API, I created endpoints to handle GET and POST requests related to plant data. The POST endpoint processes multipart form data containing uploaded images, saves them to the server, and records their filenames in the database. I also included Error handlers to handle exceptions during this process.

from flask import request, make_response, jsonify
from werkzeug.utils import secure_filename
import os
import uuid

class Plants(Resource):
    def post(self):
        default_value = '0'
        image_directory = app.config["Images"]
        images = []

        uploaded_files = [
            request.files.get('image1', default_value),
            request.files.get('image2', default_value),
            request.files.get('image3', default_value)
        ]

        unique_str = str(uuid.uuid4())[:8]

        try:
            for image in uploaded_files:
                if image:
                    filename = f"{unique_str}_{secure_filename(image.filename)}"
                    images.append(filename)
                    image_path = os.path.join(image_directory, filename)
                    image.save(image_path)
                else:
                    images.append(None)
        except Exception as exc:
            return make_response({"error": f"An error occurred saving image: {str(exc)}"}, 400)

        name = request.form.get('name', default_value)
        price = request.form.get('price', default_value)
        description = request.form.get('description', default_value)
        water = request.form.get('water', default_value)
        sun = request.form.get('sun', default_value)
        qty = request.form.get('qty', default_value)

        try:
            new_plant = Plant(
                name=name,
                price=price,
                description=description,
                qty=qty,
                image1=images[0],
                image2=images[1],
                image3=images[2],
                water=water,
                sun=sun
            )
            db.session.add(new_plant)
            db.session.commit()

            encoded_images = encode_image_directory()

            new_plant_dict = {
                "id": new_plant.id,
                "name": new_plant.name,
                "description": new_plant.description,
                "price": new_plant.price,
                "qty": new_plant.qty,
                "sun": new_plant.sun,
                "water": new_plant.water,
                "image1": encoded_images.get(new_plant.image1),
                "image2": encoded_images.get(new_plant.image2),
                "image3": encoded_images.get(new_plant.image3),
            }

            return make_response(new_plant_dict, 201)
        except Exception as exc:
            db.session.rollback()
            return make_response({"error": f"An error occurred: {str(exc)}"}, 400)

There is a lot going on in this code so let’s break it down:

1. Handling File Uploads: using Flask’s request.files object, we retrieve the uploaded image files from the request.

uploaded_files = [
    request.files.get('image1', default_value),
    request.files.get('image2', default_value),
    request.files.get('image3', default_value)
]

2. Saving Uploaded Images: once we retrieve the uploaded image files, we save them to a specified directory on the server. Each image is saved with a unique filename generated using a combination of a random string and the original filename to prevent naming conflicts. I also append the original filename to the images array to retrieve the corresponding base64 data from my helper function dictionary later on.

for image in uploaded_files:
    if image:
        filename = f"{unique_str}_{secure_filename(image.filename)}"
        images.append(filename)
        image_path = os.path.join(image_directory, filename)
        image.save(image_path)
    else:
        images.append(None)

3. Database Operations: store information about the object, in my case, I created a database for house plants, so I store their names, prices, descriptions, and image filenames, to the database. I used SQLAlchemy, an ORM (Object-Relational Mapping) library, to interact with the database. Each plant record is represented by an instance of the Plant model.

new_plant = Plant(
    name=name,
    price=price,
    description=description,
    qty=qty,
    image1=images[0],
    image2=images[1],
    image3=images[2],
    water=water,
    sun=sun
)
db.session.add(new_plant)
db.session.commit()

4. Error Handling: if an error occurs, we rollback the database transaction and return an appropriate error response with a descriptive message.

try:

    ...
except Exception as exc:
    db.session.rollback()
    return make_response({"error": f"An error occurred: {str(exc)}"}, 400)

5. Response: upon successfully adding a new plant to the database, we construct a response containing information about the newly added plant. This includes its ID, name, description, price, quantity, sun requirements, water requirements, and we call upon our helper function to retrieve the corresponding base64 encoded URLs and assign them to our image keys.

new_plant_dict = {
    "id": new_plant.id,
    "name": new_plant.name,
    "description": new_plant.description,
    "price": new_plant.price,
    "qty": new_plant.qty,
    "sun": new_plant.sun,
    "water": new_plant.water,
    "image1": encoded_images.get(new_plant.image1),
    "image2": encoded_images.get(new_plant.image2),
    "image3": encoded_images.get(new_plant.image3),
}
return make_response(new_plant_dict, 201)

Lastly, I created a resource for GET requests.

def get(self):
        encoded_images = encode_imgage_directory()
        plants = Plant.query.all()
        plant_data = []

        for plant in plants:
              plant_info = {
                "id": plant.id,
                "name": plant.name,
                "description": plant.description,
                "price": plant.price,
                "qty": plant.qty,
                "sun": plant.sun,
                "water": plant.water,
                "reviews": [],  
                "image1": encoded_images[plant.image1], 
                "image2": encoded_images[plant.image2], 
                "image3": encoded_images[plant.image3],  
                }     
              plant_data.append(plant_info)
        return make_response(plant_data, 200)

When a request is received we query the database for all the records and call on the encoded_images helper function to convert the plant image data to base64 format.

We then construct and return a response containing information about each plant stored in the database, including its images encoded in base64 format.

To display the images in my frontend, I simply have to loop through the response and create an <img /> tag for each of the respective images and assign the src attribute to the corresponding value.

Conclusion:

This article reflects my experience of handling file uploads and encoding/decoding images for use in a web application, I hope I was helpful in providing insights into the process and the code used to achieve this. Please feel free to leave me comments/questions or feedback!