pythondjangofirebase-storagepyrebasedjango-uploads

How can i connect my django uploads to the firebase storage?


this is my first time using firebase storage for file upload. I don't know if this is the way to structure and do it but. I've done the upload part and it works. But how can i have the deletion setup as well, whenever the admin deletes an image of a product from django-admin it should delete the specific image from the firebase storage bucket. currently I have my product models and other related models to product as given below:

#product/models.py
import os
from django.db import models
from dripshop_apps.core.abstract_models import AbstractItem
from dripshop_apps.category.models import Category
from dripshop_apps.brand.models import Brand
from django.contrib.contenttypes.fields import GenericRelation
from django.utils import timezone
from django.dispatch import receiver
from django.db.models.signals import pre_save, pre_delete
from dripshop_apps.core.receivers import slugify_pre_save, publish_state_pre_save, update_featured_on_publish, update_visibility_on_publish
from .tasks import upload_to_firebase
from .utils import product_thumbnail_upload_path, product_image_upload_path, sanitize_for_directory_name

class Product(AbstractItem):
    price = models.DecimalField(max_digits=10, decimal_places=2)
    category = models.ForeignKey(Category, on_delete=models.SET_NULL,related_name='product_category', null=True, blank=True)
    brand = models.ForeignKey(Brand, on_delete=models.SET_NULL, related_name='product_brand', null=True, blank=True)
    thumbnail = models.ImageField(upload_to=product_thumbnail_upload_path, blank=True, null=True, max_length=255)
    visible = models.BooleanField(default=True, editable=False)
    stock = models.PositiveIntegerField(default=0)

    objects=ProductManager()

    class Meta:
        verbose_name = "Product"
        verbose_name_plural = "Products"

    def __str__(self):
        return self.title
    
class ProductImage(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    image = models.ImageField(upload_to=product_image_upload_path, blank=True, null=True, max_length=255)
    updated = models.DateTimeField(auto_now=True)
    
pre_save.connect(publish_state_pre_save, sender=Product)
pre_save.connect(slugify_pre_save, sender=Product)
pre_save.connect(update_featured_on_publish, sender=Product)

and I am using the functions product_thumbnail_upload_path and product_image_upload_path to upload the image to the desired directory structure to the firebase with the use of a celery task which is also given below it

#product/utils.py
import os
from django.utils import timezone
from .tasks import upload_to_firebase
from django.conf import settings
from pyrebase import pyrebase

firebase = pyrebase.initialize_app(settings.FIREBASE_CONFIG)
storage = firebase.storage()


def sanitize_for_directory_name(value):
    # Replace spaces with underscores and remove other characters
    return ''.join(c if c.isalnum() or c in ['_', '-'] else '_' for c in value)

def product_image_upload_path(instance, filename):
    title = getattr(instance.product, 'title', 'unknown')
    product_title = sanitize_for_directory_name(title)

    timestamp = timezone.now().strftime('%Y-%m-%d_%H-%M-%S')
    extension = os.path.splitext(filename)[1]
    local_path = f'uploads/products/{product_title}/general/{timestamp}{extension}'
    firebase_path = f'products/{product_title}/general/{timestamp}{extension}'

    # Ensure the local directory exists
    os.makedirs(os.path.dirname(local_path), exist_ok=True)

    # Save the file to the local directory
    with open(local_path, 'wb') as f:
        f.write(instance.image.read())

    # Use Celery to upload to Firebase Storage asynchronously
    upload_to_firebase.delay(local_path, firebase_path, 'Image')
    return local_path

def product_thumbnail_upload_path(instance, filename):
    title = getattr(instance, 'title', 'unknown')
    product_title = sanitize_for_directory_name(title)

    timestamp = timezone.now().strftime('%Y-%m-%d_%H-%M-%S')
    extension = os.path.splitext(filename)[1]
    local_path = f'uploads/products/{product_title}/thumbnail/{timestamp}{extension}'
    firebase_path = f'products/{product_title}/thumbnail/{timestamp}{extension}'

    # Ensure the local directory exists
    os.makedirs(os.path.dirname(local_path), exist_ok=True)

    # Save the file to the local directory
    with open(local_path, 'wb') as f:
        f.write(instance.thumbnail.read())

    # Use Celery to upload to Firebase Storage asynchronously
    upload_to_firebase.delay(local_path, firebase_path, 'Thumbnail')
    return local_path

and now the tasks for celery is created as:

import os
from celery import shared_task
from django.utils import timezone
from django.conf import settings
from pyrebase import pyrebase

firebase = pyrebase.initialize_app(settings.FIREBASE_CONFIG)
storage = firebase.storage()

@shared_task(name='upload_to_firebase')
def upload_to_firebase(local_path, firebase_path, instance_type):
    # Upload to Firebase Storage
    storage.child(firebase_path).put(local_path)
    print(f"Uploaded {instance_type} to Firebase:", firebase_path)

the question is how can i make a similar stuff to delete the desired image from the firebase bucket where it had previously been saved? also the firebase config is done in the settings file and the serviceaccountkey is loaded in it as well:


FIREBASE_CONFIG = {
    "apiKey": os.environ.get('FIREBASE_API_KEY'),
    "authDomain": os.environ.get('FIREBASE_AUTH_DOMAIN'),
    'databaseURL': os.environ.get('FIREBASE_DATABASE_URL'),
    "projectId": os.environ.get('FIREBASE_PROJECT_ID'),
    "storageBucket": os.environ.get('FIREBASE_STORAGE_BUCKET'),
    "messagingSenderId": os.environ.get('FIREBASE_MESSAGING_SENDER_ID'),
    "appId": os.environ.get('FIREBASE_APP_ID'),
    'measurementId': os.environ.get('FIREBASE_MEASUREMENT_ID'),
}


FIREBASE_SERVICE_ACCOUNT_KEY = os.path.join(BASE_DIR, 'serviceAccountKey.json')

Also, this setup is saving the image uploads twice in the local.


Solution

  • You can use a post_delet signal to fire the command to delete the file in Firebase.

    post_delete.connect(remove_thumbnail_from_firebase, sender=Product)
    
    # or with a decorator
    
    @receiver(models.signals.post_delete, sender=Product)
    def remove_thumbnail_from_firebase(sender, instance, **kwargs):
        # prepare again your file path
        ...
        storage.child(firebase_path).delete(path)
    

    Feel free to move it to an async task as well. I recommend saving the files in a single place and using the default Django storage DEFAULT_FILE_STORAGE to get the ability to work with these files with the sophisticated Django file API. Check out Django Storages package for more information about this. Then you can delete your Firebase files as local files:

    @receiver(models.signals.post_delete, sender=Product)
    def remove_thumbnail_from_firebase(sender, instance, **kwargs):
        instance.thumbnail.delete(save=False)