Search code examples
pythondjangoamazon-s3image-uploadingproduction-environment

Rotate selfie image working in development but not working when saving to S3 bucket in production?


I'm trying to fix an image rotation error that I'm getting in production, but not getting in development. Been stuck on this for a few days and would really appreciate any help from anyone who has any insight into how to solve this!

==Context==

I'm writing a Django web app with a profile pic upload feature. To get over the selfie EXIF problem (incorrect orientation), I've added a function that rotates the image using a post-save receiver. It works great in development (when the images are saved and stored locally), but now that I've moved to production (Heroku server; saving images in S3 bucket), the function throws a FileNotFound error -- [Errno 2] No such file or directory: 'https://db-devsite1.s3.amazonaws.com/media/uploadImage/81c01af5-030f-42ed-b413-91eb8941675b.JPG', even though it's the correct file path for the image. Everything else still works great.

==View==

def rotateimage (request):

    if request.method == 'POST':
        form = uploadImageForm(request.POST, request.FILES)
        if form.is_valid():
            form.save()
            return redirect('rotateimage')
    else:
        form = uploadImageForm()

==Model==

import os
from django.db import models
from catalog.utilities import rotate_image
from django.urls import reverse
from io import BytesIO
from django.core.files import File
from PIL import Image, ExifTags
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.conf import settings

class uploadImage(models.Model):
    uploadImage = models.ImageField(upload_to='uploadImage', blank=True, null=True)
    thumbnail = models.ImageField(upload_to='rotateImage', blank=True, null=True)

@receiver(post_save, sender=uploadImage, dispatch_uid="update_image_profile")
def update_image(sender, instance, **kwargs):
  if instance.uploadImage:
    fullpath = settings.MEDIA_ROOT + instance.uploadImage.url
    rotate_image(fullpath)

==Utilities==

from PIL import Image, ExifTags

def rotate_image(filepath):

    dev_test = "Off"

    if dev_test == "Off":
        try:
            image = Image.open(filepath)
            for orientation in ExifTags.TAGS.keys():
              if ExifTags.TAGS[orientation] == 'Orientation':
                    break
            exif = dict(image._getexif().items())

            if exif[orientation] == 3:
                image = image.rotate(180, expand=True)
            elif exif[orientation] == 6:
                image = image.rotate(270, expand=True)
            elif exif[orientation] == 8:
                image = image.rotate(90, expand=True)
            image.save(filepath)
            image.close()
        except (AttributeError, KeyError, IndexError):
            # cases: image don't have getexif
            pass
    else:
        try:
            image = Image.open(filepath)
            image = image.rotate(180, expand=True)
            image.save(filepath)
            image.close()
        except (AttributeError, KeyError, IndexError):
            # cases: image don't have getexif
            pass

==Settings==

import os

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

AWS_LOCATION = 'static'
AWS_ACCESS_KEY_ID = CONFIG['AWS_ACCESS_KEY_ID']
AWS_SECRET_ACCESS_KEY = CONFIG['AWS_SECRET_ACCESS_KEY']
AWS_STORAGE_BUCKET_NAME = CONFIG['AWS_STORAGE_BUCKET_NAME']
AWS_S3_CUSTOM_DOMAIN='%s.s3.amazonaws.com' % AWS_STORAGE_BUCKET_NAME

AWS_S3_OBJECT_PARAMETERS = {
     'CacheControl': 'max-age=86400',
}
DEFAULT_FILE_STORAGE = 'dbDevSite.storage_backends.MediaStorage'
STATICFILES_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'catalog/static'),
]
STATIC_URL='https://%s/%s/' % (AWS_S3_CUSTOM_DOMAIN, AWS_LOCATION)
ADMIN_MEDIA_PREFIX = STATIC_URL + 'admin/'
STATICFILES_FINDERS = ('django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder',)
AWS_DEFAULT_ACL = None
AWS_PRELOAD_METADATA=True

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'catalog/static/media')

# Heroku: Update database configuration from $DATABASE_URL.
import dj_database_url
db_from_env = dj_database_url.config(conn_max_age=500)
DATABASES['default'].update(db_from_env)

***EDIT - updated with full error

Environment:


Request Method: POST
Request URL: https://db-devsite.herokuapp.com/catalog/rotateimage

Django Version: 3.0.1
Python Version: 3.8.0
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'catalog.apps.CatalogConfig',
 'storages']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']



Traceback (most recent call last):
  File "/app/.heroku/python/lib/python3.8/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "/app/.heroku/python/lib/python3.8/site-packages/django/core/handlers/base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/app/.heroku/python/lib/python3.8/site-packages/django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/app/catalog/views.py", line 18, in rotateimage
    form.save()
  File "/app/.heroku/python/lib/python3.8/site-packages/django/forms/models.py", line 459, in save
    self.instance.save()
  File "/app/.heroku/python/lib/python3.8/site-packages/django/db/models/base.py", line 745, in save
    self.save_base(using=using, force_insert=force_insert,
  File "/app/.heroku/python/lib/python3.8/site-packages/django/db/models/base.py", line 793, in save_base
    post_save.send(
  File "/app/.heroku/python/lib/python3.8/site-packages/django/dispatch/dispatcher.py", line 173, in send
    return [
  File "/app/.heroku/python/lib/python3.8/site-packages/django/dispatch/dispatcher.py", line 174, in <listcomp>
    (receiver, receiver(signal=self, sender=sender, **named))
  File "/app/catalog/models.py", line 31, in update_image
    rotate_image(fullpath)
  File "/app/catalog/utilities.py", line 9, in rotate_image
    image = Image.open(filepath)
  File "/app/.heroku/python/lib/python3.8/site-packages/PIL/Image.py", line 2766, in open
    fp = builtins.open(filename, "rb")

Exception Type: FileNotFoundError at /catalog/rotateimage
Exception Value: [Errno 2] No such file or directory: 'https://db-devsite1.s3.amazonaws.com/media/uploadImage/81c01af5-030f-42ed-b413-91eb8941675b_Kupxl37.JPG'

EDIT here is the code solution to the post-save function that works

@receiver(post_save, sender=uploadImage, dispatch_uid="update_image_profile")
def update_image(sender, instance, **kwargs):
  if instance.uploadImage:

      # Download instance.uploadImage from S3 to temp folder
      s3_client = boto3.client('s3')
      bucket_name = settings.AWS_STORAGE_BUCKET_NAME
      subfolder_name = 'media/'
      target_image = str(instance.uploadImage)
      image_path = subfolder_name + target_image
      image_name = '/tmp/image.jpg'

      s3_client.download_file(bucket_name, image_path, image_name)

      # Rotate image in temp folder
      rotate_image(image_name)

      # Upload rotated image from temp folder back to S3
      s3_client.upload_file(image_name, bucket_name, image_path)

Solution

  • If your code is successfully handling the rotation, then the only missing steps are to download/upload the image.

    To download:

    import boto3
    
    s3_client = boto3.client('s3')
    
    s3_client.download_file('mybucket', 'user1.jpg', '/tmp/image.jpg')
    

    Then, rotate the image using your existing code, and finally upload it to your desired destination:

    s3_client.upload_file('/tmp/image.jpg', 'mybucket', 'user1.jpg')