Как использовать Celery для загрузки файлов в Django

Мне стало интересно, как я могу использовать Celery workers для обработки загрузки файлов. Поэтому я попробовал реализовать это на простом классе. Я переопределил класс create в своем ModelViewSet. Но, видимо, стандартный json-кодер Django не сериализует ImageFields (отстой). Я буду очень признателен, если вы, ребята, подскажете мне, как я могу это исправить. Вот что я придумал:

serializers.py:

class ProductImageSerializer(serializers.ModelSerializer):
    class Meta:
        model = ProductImage
        fields = ['id', 'image']

tasks.py:

from time import sleep
from celery import shared_task
from .models import ProductImage

@shared_task:
def upload_image(product_id, image):
    print('Uploading image...')
    sleep(10)
    product = ProductImage(product_id=product_id, image=image)
    product.save()

views.py:

class ProductImageViewSet(ModelViewSet):
    serializer_class = ProductImageSerializer

    def get_queryset(self):
        return ProductImage.objects.filter(product_id=self.kwargs['product_pk'])

    def create(self, request, *args, **kwargs):
        product_id = self.kwargs['product_pk']
        image = self.request.FILES['image']
        image.open()
        image_data = Image.open(image)
        upload_image.delay(product_id, image_data)

        return Response('Thanks')

и вот моя модель, содержащая мое поле ImageField:

class ProductImage(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='images')
    image = models.ImageField(upload_to='store/images', validators=[validate_image_size])

Поэтому я придумал, как это сделать. Вот мое решение:

Проблема в том, что стандартный json-кодер celery не может сериализовать Images, InMemoryUploadedFile, ModelObjects и.... Поэтому нам нужно передать ему значение, которое можно сериализовать в json. В данном случае мы хотим сериализовать изображение. Поэтому мы можем преобразовать наше изображение в байты, затем преобразовать этот байтовый объект в строку, чтобы мы могли отправить ее в нашу задачу celery. После получения строки в нашей задаче мы можем преобразовать ее обратно в изображение и загрузить его с помощью celery. Многие люди в интернете предлагали такое решение, но никто из них не предоставил никакого кода. Итак, вот код для примера выше, если вы хотите увидеть его в действии:

В моем views.py я использовал ModelViewSet и переопределил метод create:

def create(self, request, *args, **kwargs):

        image = self.request.FILES['image'].read()

        byte = base64.b64encode(image)
        
        data = {
            'product_id': self.kwargs['product_pk'],
            'image': byte.decode('utf-8'),
            "name": self.request.FILES['image'].name
        }

        upload_image.delay(data=data)

        return Response('Uploading...')

А вот мой tasks.py:

from time import sleep
from celery import shared_task
from .models import ProductImage
import PIL.Image as Image
import io
import base64
import os
from django.core.files import File

@shared_task
def upload_image(data):
    
    print('Uploading image...')
    
    sleep(10)
    
    product_id = data['product_id']

    byte_data = data['image'].encode(encoding='utf-8')
    b = base64.b64decode(byte_data)
    img = Image.open(io.BytesIO(b))
    img.save(data['name'], format=img.format)
    
    with open(data['name'], 'rb') as file:
        picture = File(file)

        instance = ProductImage(product_id=product_id, image=picture)
        instance.save()
    
    os.remove(data['name'])

    print('Uploaded!')

Надеюсь, кому-то это будет полезно. И если у кого-то есть какие-либо предложения, пожалуйста, сообщите мне об этом в комментариях. Хорошего дня;)

Здравствуйте, ранее я опубликовал решение этого вопроса, и хотя это решение работало правильно, я нашел лучшее решение. Кодирование и декодирование бинарных файлов с использованием base64 делает их больше, а это не то, чего мы хотим. Поэтому лучшим решением является временное сохранение загруженного файла на диске, передача пути к нему нашему celery worker для загрузки и создания экземпляра ProductImage в нашей базе данных, а затем удаление файла, который мы сохранили на диске .

Вот как это реализовать:

tasks.py:

from time import sleep
from celery import shared_task
from .models import ProductImage
from django.core.files import File
from django.core.files.storage import FileSystemStorage
from pathlib import Path

@shared_task
def upload(product_id, path, file_name):

    print('Uploading image...')

    sleep(10)
    
    storage = FileSystemStorage()

    path_object = Path(path)

    with path_object.open(mode='rb') as file:
        
        picture = File(file, name=path_object.name)

        instance = ProductImage(product_id=product_id, image=picture)

        instance.save()


    storage.delete(file_name)

    print('Uploaded!')

В файле serializers.py необходимо переопределить метод create сериализатора ProductImage следующим образом:

    def create(self, validated_data):
        product_id = self.context['product_id']
        image_file = self.context['image_file']
        storage = FileSystemStorage()
        
        storage.save(image_file.name, File(image_file))

        return upload.delay(product_id=product_id, path=storage.path(image_file.name), file_name=image_file.name)

Вы также должны переопределить метод create в ViewSet'е ProductImage, чтобы предоставить файл изображения для контекста сериализатора:

    def create(self, request, *args, **kwargs):
        product_id = self.kwargs['product_pk']
        image_file = self.request.FILES['image']
        serializer = ProductImageSerializer(
            data=request.data,
            context={
                'product_id': product_id,
                'image_file': image_file
            }
        )
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response('Upload Started...')
Вернуться на верх