Как использовать 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...')