F(backend): add public media derivatives pipeline
Some checks failed
Backend CI/CD / test (push) Has been cancelled
Backend CI/CD / deploy (push) Has been cancelled

This commit is contained in:
2026-05-20 14:26:51 +03:30
parent 88b793ed9f
commit b4903f7cb1
18 changed files with 710 additions and 53 deletions

View File

@@ -5,6 +5,7 @@ from typing import Optional
from apps.blog.api.schemas import AuthorSchema
from apps.gallery.models import Gallery
from core.media import BLUR_VARIANT, PREVIEW_VARIANT, derivative_url
class GallerySchema(ModelSchema):
@@ -12,12 +13,34 @@ class GallerySchema(ModelSchema):
uploaded_by: AuthorSchema
file_size_mb: float
markdown_url: str
absolute_image_url: Optional[str] = None
absolute_image_preview_url: Optional[str] = None
absolute_image_blur_url: Optional[str] = None
class Config:
model = Gallery
model_fields = ['id', 'title', 'description', 'image', 'alt_text',
'width', 'height', 'is_public', 'created_at']
@staticmethod
def resolve_absolute_image_url(obj, context):
request = context["request"]
if obj.image and hasattr(obj.image, "url"):
return request.build_absolute_uri(obj.image.url)
return None
@staticmethod
def resolve_absolute_image_preview_url(obj, context):
request = context["request"]
url = derivative_url(obj.image, PREVIEW_VARIANT)
return request.build_absolute_uri(url) if url else None
@staticmethod
def resolve_absolute_image_blur_url(obj, context):
request = context["request"]
url = derivative_url(obj.image, BLUR_VARIANT)
return request.build_absolute_uri(url) if url else None
class GalleryCreateSchema(Schema):
"""Payload for creating a gallery entry."""

View File

@@ -63,6 +63,7 @@ def upload_image(request, file: UploadedFile = File(...), data: GalleryCreateSch
alt_text=data.alt_text if data else "",
is_public=data.is_public if data else True
)
gallery_item._defer_image_processing = True
# Save image
filename = f"gallery/{uuid.uuid4().hex}.{file.name.split('.')[-1]}"

View File

@@ -1,13 +1,14 @@
from django.db import models
from django.conf import settings
from PIL import Image
from core.models import BaseModel
from core.media import (
delete_image_derivatives_by_name,
get_image_previous_name,
safe_process_public_image,
)
MAX_IMAGE_FILE_SIZE_BYTES = 2 * 1024 * 1024
class Gallery(BaseModel):
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
@@ -27,47 +28,39 @@ class Gallery(BaseModel):
return self.title
def save(self, *args, **kwargs):
previous_image_name = get_image_previous_name(self, "image")
current_image_name = self.image.name if self.image else None
image_changed = previous_image_name != current_image_name
super().save(*args, **kwargs)
if self.image:
# Get file size
self.file_size = self.image.size
# Get image dimensions
with Image.open(self.image.path) as img:
self.width, self.height = img.size
# Compress image if it's too large
self.compress_image()
# Update fields without triggering save again
Gallery.objects.filter(pk=self.pk).update(
file_size=self.file_size,
width=self.width,
height=self.height
if image_changed and previous_image_name:
delete_image_derivatives_by_name(
self.image.storage if self.image else None,
previous_image_name,
"gallery",
delete_original=True,
)
def compress_image(self):
"""Compress image if it's larger than 2MB or dimensions are too large"""
if not self.image:
if image_changed:
Gallery.objects.filter(pk=self.pk).update(file_size=None, width=None, height=None)
return
with Image.open(self.image.path) as img:
# Convert to RGB if necessary
if img.mode in ("RGBA", "P"):
img = img.convert("RGB")
# Resize if too large
max_size = (1920, 1080)
if img.size[0] > max_size[0] or img.size[1] > max_size[1]:
img.thumbnail(max_size, Image.Resampling.LANCZOS)
# Compress if file size is too large
quality = 85
if self.file_size and self.file_size > MAX_IMAGE_FILE_SIZE_BYTES:
quality = 70
img.save(self.image.path, "JPEG", quality=quality, optimize=True)
if getattr(self, "_defer_image_processing", False):
return
if image_changed:
result = safe_process_public_image(self.image, "gallery")
if result:
Gallery.objects.filter(pk=self.pk).update(
file_size=result.file_size,
width=result.width,
height=result.height,
)
self.file_size = result.file_size
self.width = result.width
self.height = result.height
@property
def file_size_mb(self):

View File

@@ -1,20 +1,26 @@
from celery import shared_task
from PIL import Image
import logging
from celery import shared_task
from core.media import safe_process_public_image
logger = logging.getLogger(__name__)
@shared_task
def process_uploaded_image(gallery_id):
"""Process uploaded image: compress, resize, extract metadata"""
"""Process gallery image derivatives and refresh metadata."""
try:
from .models import Gallery
gallery_item = Gallery.objects.get(id=gallery_id)
if gallery_item.image:
# This will trigger the compression and metadata extraction
gallery_item.compress_image()
result = safe_process_public_image(gallery_item.image, "gallery", force=True)
if result:
Gallery.objects.filter(pk=gallery_item.pk).update(
file_size=result.file_size,
width=result.width,
height=result.height,
)
logger.info(f"Processed image: {gallery_item.title}")
return f"Processed image: {gallery_item.title}"