Flask SQLAlchemy: How to upload photos and render them to your webpage

Brodie Ashcraft
6 min readOct 9, 2023

--

Uploading images from your front end and saving them to your backend is something I dealt with when I was designing my first ever fullstack application while learning fullstack development at Flatiron. I originally thought that I would be able to save the photo data into my SQL database and from there access it as needed to display and render the data.
Here is what I had in mind as my first attempt:

@app.route('/upload', methods=['POST'])
def upload()
pic = request.files['pic']

if not pic:
return 'No pic uploaded', 400

filename = secure_filename(pic.filename)
mimetype = pic.mimetype

img = Image(img=pic.read(), mimetype=mimetypem name=filename)
db.session.add(img)
db.session.commit()

But the more research I did, I learned that storing images in a database this way can lead to your database being slowed down quite a bit, especially if you are planning on storing multiple pictures. Now admittedly I wasn’t sure exactly how many photos I would want to upload and store, and I didn’t know how many would have to be uploaded before my database really started to slow down, but I wanted to avoid the possibility anyways. This app was going to be my first fullstack application, and I wanted to be able to scale it up, add more features overtime, and eventually realize it into a full scale website!

So I continued my research and was pointed to a blog by Miguel Grinberg that had an amazingly detailed method for uploading files to a folder in my repo, then sending those photos to the frontend. If you want a well detailed explanation about how to perform these file uploads I highly recommend checking out his blog! I learned new things but also had to implement my own changes to ensure that the files would be uploaded and displayed in a manner that worked for my application.
So follow along if you want to get those images uploaded and displayed to your frontend!

First off lets start with our config file. I had a config.py file that I used to basically set up all the environment variables used for my Flask application.

import os
from flask import Flask
from flask_bcrypt import Bcrypt
from flask_migrate import Migrate
from flask_restful import Api
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import MetaData
from flask_cors import CORS

app = Flask(__name__)


app.config["UPLOAD_EXTENSIONS"] = [".jpg", ".png"]
app.config["UPLOAD_PATH"] = "image_uploads"

app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.json.compact = False

# setting up db, bcrypt and other configs below

The two important lines here are:

app.config["UPLOAD_EXTENSIONS"] = [".jpg", ".png"]
app.config["UPLOAD_PATH"] = "image_uploads"

I created a folder at the same level as my config.py file called “image_uploads” where I would have all uploads saved. You could name this whatever you want but make sure your app.config[“UPLOAD_PATH”] points to this folder.
We also determine what upload extensions we want to accept. This is so we don’t allow for file types that are not images for security purposes.

Now I move into my app.py file where I have defined all my Flask routes and where we are going to receive our file uploads and save them.
We will first need to import some some built in Python packages to make everything work

import os
import imghdr
import uuid
from flask import request, session, send_from_directory
from werkzeug.utils import secure_filename

# other imports needed for app

And then we are going to create a helper function that will check what type of image files we have received:

def validate_image(stream):
header = stream.read(512)
stream.seek(0)
format = imghdr.what(None, header)
if not format:
return None
return "." + (format if format != "jpeg" else "jpg")

Using the imghdr we imported, we read the first 512 bytes of the image file, and from this imghdr will tell us what image type it is. Then the function will return the format type (.png, .jpg) if it recognizes it as a image.

With this setup we can now work on processing file uploads and saving them into the folder we created for image uploads.
Here is what our code for uploading and saving files looks like:

class Images(Resource):
def post(self):
info = request.form.get("info")
image_name = request.form.get("image_name")
image = request.files.get("image")

# check if filepath already exists. append random string if it does
if secure_filename(image.filename) in [
img.file_path for img in Image.query.all()
]:
unique_str = str(uuid.uuid4())[:8]
image.filename = f"{unique_str}_{image.filename}"


# handling file uploads
filename = secure_filename(image.filename)
if filename:
file_ext = os.path.splitext(filename)[1]
if file_ext not in app.config[
"UPLOAD_EXTENSIONS"
] or file_ext != validate_image(image.stream):
return {"error": "File type not supported"}, 400

image.save(os.path.join(app.config["UPLOAD_PATH"], filename))

img = Image(name=image_name, file_path=filename)

db.session.add(img)
db.session.commit()

Let’s break this down into parts starting from the top


info = request.form.get("info")
image_name = request.form.get("image_name")
image = request.files.get("image")

These three lines retrieve the info being sent from the front end. The most important for our current task will be the ‘image’

if secure_filename(image.filename) in [
img.file_path for img in Image.query.all()
]:
unique_str = str(uuid.uuid4())[:8]
image.filename = f"{unique_str}_{image.filename}"

In my Image model, I have an attribute called ‘file_path’ when creating a new Image instance to store in the database, we save our secure_filename to this attribute. So what the code above does, is check whether we already have a image with this file_path, and if we do, we will add a unique string using uuid to our image.filename.
The reason we do this is so that in the case that multiple people upload an image called “cat.jpg” each subsequent image will get a new uuid string prepended to the filename. This prevents us overwriting old photos with the same filename.

Next is handling the upload:

        filename = secure_filename(image.filename)
if filename:
file_ext = os.path.splitext(filename)[1]
if file_ext not in app.config[
"UPLOAD_EXTENSIONS"
] or file_ext != validate_image(image.stream):
return {"error": "File type not supported"}, 400

image.save(os.path.join(app.config["UPLOAD_PATH"], filename))

img = Image(name=image_name, file_path=filename)

db.session.add(img)
db.session.commit()

First we use the secure_filename we imported from werkzeug.utils and use this on our image.filename. The reasoning for this is so that someone cant try to upload a malicious filename that would potentially access system information. For instance if someone was attempting to upload a file with a filename like “../../../.bashrc” it would be sanitized and changed to “…….bashrc” thus preventing potentially malicious data from entering our system.

Second we have this line:

file_ext = os.path.splitext(filename)[1]

This will split only the file extension from our filename so we can check it against our validate_image() helper function we defined earlier.
Next we have this block:

if file_ext not in app.config[
"UPLOAD_EXTENSIONS"
] or file_ext != validate_image(image.stream):
return {"error": "File type not supported"}, 400

We check that our file_ext is one of the extensions we defined in our config.py file or if the file_ext is not equal to the result of our validate_image() helper function when passing in the image.stream.
Remember, our helper function takes in the stream of bytes from our image, and returns what it detects the extension to be. Thus if both of these checks fail, we return the error that the submitted filetype is not supported.

Finally comes saving the image to our folder!

image.save(os.path.join(app.config["UPLOAD_PATH"], filename))

Lets break it down. We imported os, and use the os.path.join() function to create a path for the file. We pass in the “UPLOAD_PATH” we defined in our config, as well as the secure filename we saved as filename. So when we use image.save() we pass in the filepath returned from os.path.join() and save the image to said filepath!

The final step is adding our path to the database so that when we need to display images, we can do so by accessing our Image table in our database:

img = Image(name=image_name, file_path=filename)

db.session.add(img)
db.session.commit()

Here we create a new Image instance and give it our image name, and the file_path that we saved, and finally add and commit it to our database! Now our database only has to keep track of small strings and not large binary objects that could potentially slow our database.

Nice! We can now save the files to our images folder, with the next step being able to access the photos from our frontend:

class Images(Resource):
def get(self, id):
img = Image.query.filter(Image.id == id).first()
path = img.file_path
return send_from_directory(app.config["UPLOAD_PATH"], path)

This simple block of code is all we need to be able to access our photos. We query the database for the id corresponding to the image we want, then we use the send_from_directory() function provided by Flask, to display the image to the webpage! Now all you need to do is perform a simple fetch from your frontend to the specified location for your get requests, and you’ll be able to access photos you’ve saved!

I hope this quick tutorial on file uploads serves to help! Uploading images to a website/application is an essential part of most modern apps and is something everyone should be comfortable implementing and performing. Thanks for reading and have a wonderful day!

--

--