How I added image support to our Django app

I tackled the challenge of adding image support to our Django app using AWS S3 and CloudFront. Here's how I made it work smoothly across platforms, enhancing the user experience. A quick guide for anyone looking to do the same.

Recently, I faced the challenge of adding image support to our Check-ins product at DailyBot, and I want to share how I tackled this challenge and walk you through the process of adding image upload functionality using Django, Amazon S3, and CloudFront.

Enhancing your Django App with image support will significantly improve user experience. In our case, images allow users to visually showcase their work or share visual updates with their team.

Why add image support?

At DailyBot, we've always known that image support would be a game-changer for us. Countless users had asked us for it, and it's easy to see why. 

When you're trying to explain yesterday's work, your workflow for the day, or a roadblock, sometimes words just aren't enough. Being able to share a screenshot of a new feature you just worked on or a malfunctioning service that keeps eluding you can give your team so much more context.

Our chatbot spans all major platforms, including Slack, Discord, Teams, and Google Chat, so needless to say, implementing this feature on DailyBot started off as a significant challenge. 

We needed a solution that would work seamlessly across all platforms without compromising UX. Plus, with the sheer volume of reports our users share daily, we had to find a storage and delivery method that was fast, secure, and scalable.

And after some consideration, we decided to leverage the power of AWS S3 and CloudFront. This combination provided the exact level of performance and reliability we were looking for.

Personally, I’m thrilled to look back knowing that we've successfully implemented this feature and made it incredibly easy to use within our Django-based chatbot. So, if you're looking to add image support to your own project, here's a quick guide to help you overcome the same challenges we faced.

Daily updates made easier with DailyBot—because staying on track shouldn't be a hassle.

1. Setting up a custom storage manager

First, let's set up a custom storage manager in Django to handle the uploading, retrieving, and deleting of images on S3.

Install Required Packages

We need to install boto3 and django-storages to interact with AWS S3 and manage our media files effectively:

pip install boto3 django-storages

Configure Django Settings

Next, we configure our Django settings to include the necessary AWS credentials and settings. This configuration tells Django how to connect to your S3 bucket:

AWS_ACCESS_KEY_ID = 'your-access-key-id'
AWS_SECRET_ACCESS_KEY = 'your-secret-access-key'
AWS_STORAGE_BUCKET_NAME = 'your-bucket-name'
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
AWS_LOCATION = 'your-custom-location'
DEFAULT_FILE_STORAGE = 'myapp.storage_backends.MediaStorageManager'

Create your Custom Storage Manager

We need a custom storage manager to handle the specifics of saving files to S3. This custom manager ensures that each file has a unique name and handles the actual upload process.

Create a file named storage_backends.py in your Django app:

import hashlib
import os

from django.conf import settings
from storages.backends.s3boto3 import S3Boto3Storage

class MediaStorageManager(S3Boto3Storage):
    location = settings.AWS_LOCATION
    bucket_name = settings.AWS_STORAGE_BUCKET_NAME

    def __init__(self, *args, **kwargs):
        kwargs['custom_domain'] = settings.AWS_S3_CUSTOM_DOMAIN
        super().__init__(*args, **kwargs)

    def _save(self, name, content):
        unique_name = hashlib.sha256((name).encode('utf-8')).hexdigest()
        path = os.path.join(self.location, unique_name)
        return super()._save(path, content)

This class extends S3Boto3Storage from django-storages to handle file uploads to S3. The _save method ensures that each file gets a unique name by hashing its original name.

2. Creating the media file model

Next, let's create a Django model to store metadata about each uploaded image. This model keeps track of each image's details, including its S3 path, organization, and owner information.

Define the Model

In your models.py, add the following code to define the MediaFile model:

import uuid
from django.db import models
from django.utils.timezone import now
from .storage_backends import MediaStorageManager

class MediaFile(models.Model):
    uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True, db_index=True)
    user = models.ForeignKey('yourapp.User', on_delete=models.CASCADE)
    file = models.FileField(storage=MediaStorageManager())
    created_at = models.DateTimeField(default=now)
    updated_at = models.DateTimeField(default=now)

This model includes fields for the image's unique identifier, organization, owner, and the file itself. It uses the custom storage manager to handle file storage.

3. Uploading and deleting files

With our storage manager and model in place, we can now handle file uploads and deletions.

Upload a File

To upload a file, we use our custom storage manager to save the file to S3 and create a MediaFile instance to store its metadata:

from io import BytesIO

def upload_file(name, content):
    file = MediaStorageManager()._save(name, BytesIO(content))
    return MediaFile.objects.create(
        user=user,
        file=file
    )

This function uploads the file to S3 and creates a new MediaFile record in the database with the file's metadata.

Delete a File

Deleting a file involves removing it from S3 and invalidating the CloudFront cache to ensure the file is no longer accessible.

from django.db.models.signals import pre_delete
from django.dispatch import receiver
import boto3
import time

@receiver(pre_delete, sender=MediaFile)
def remove_file_from_s3(sender, instance, **kwargs):
    instance.file.delete(save=False)
    cloudfront_client = boto3.client('cloudfront', region_name=settings.AWS_DEFAULT_REGION)
    paths = [f'/{settings.AWS_LOCATION}/{instance.file.name}']
    cloudfront_client.create_invalidation(
        DistributionId=settings.CLOUDFRONT_DISTRIBUTION_ID,
        InvalidationBatch={
            'Paths': {'Quantity': len(paths), 'Items': paths},
            'CallerReference': str(time.time())
        }
    )

This code listens for the pre_delete signal on the MediaFile model, ensuring that when a file is deleted, it's also removed from S3, and the CloudFront cache is invalidated.

4. Handling different image formats

Since images may come from various sources, you have to be prepared to handle images with different formats. Here’s how to handle these differences and ensure they are processed correctly:

import requests
from io import BytesIO

def get_image(url):
    response = requests.get(url)
    if response.status_code == 200:
        return BytesIO(response.content)
    else:
        raise ValueError("Failed to get file")

This function fetches an image from a URL and converts it into a byte stream that can be uploaded to S3.

Wrapping up

This guide demonstrated the core steps to implement this feature, making your project more dynamic and user-friendly. To give you a better idea, here's a demonstration of DailyBot's image support functionality:

Using images in reports—just as effortless as sharing your regular work updates with DailyBot.

Try integrating image uploads into your Django App today. Take a look at the possibilities and see how it can elevate your UX. If you have any questions or need further assistance, you can find me on LinkedIn.

Happy coding! 🧑‍💻

Conversations around
The Watercooler.
Stay connected with the voices behind DailyBot. Join our subscriber list for fresh insights on the future of work, inspiring team stories, and a peek into our creative process.
Read about our privacy policy.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.