(Nie)codzienna obsługa plików graficznych i nie tylko

Dodając pliki graficzne jako załączniki, zazwyczaj chcemy je przed przesłaniem na serwer przeprocesować. Zmienić ich rozmiar, bądź format. Sam framework (Django) daje w prosty sposób możliwość nadpisania metody save() i tym samym daje nam pełną kontrolę, nad zapisywanym plikiem.


Od czegoś trzeba zacząć

W poniższym przykładzie, wykorzystamy bibliotekę Pillow, którą instalujemy po przez:

pip install Pillow

A więc zacznijmy programować

Nasza klasa będzie całościowo zajmowała się obslugą oraz zapisywaniem obrazka. Zastosujemy w niej podejście podobne do tego które pozwala nam udostępniać bezpiecznie materiały przez googledrive.


1. Zapisywanie obrazka

def save(self, *args, **kwargs):
"""Save file to disk."""
if not self.id:
super().save()

image_extension = ["jpg", "jpeg"]
if self.extension() in image_extension:
img = Image.open(self.file.path)

if img.height > 1024 or img.width > 1024:
new_img = (1024, 1024)
img.thumbnail(new_img)
img.save(self.file.path)

Poniższa metoda:

  1. Sprawdza czy obiekt jest już stworzony.
  2. Jeśli nie, wywołuje metodę nadrzędną save.
  3. Po czym upewnia się czy rozszerzenie jest na liście dozwolonych
  4. Dodatkowo sprawdza czy obrazek ma któryś bok większy niż 1024px.

Tylko w przypadku spełnienia powyższych warunków, tworzy jego mniejszą wersję.

Dla wtajemniczonych polecam również sprawdzić rodzaj pliku bazując na znajdujących się tam danych np poleceniem file.


2. Bezpieczny adres

Dodatkowo celem stworzenia bezpieczniejszego adresu, do danego zasobu który użytkownicy bedą mogli sobie wzajemnie udostępniać, zastosujemy technikę dostępu do pliku przez wysoce skomplikowany adres url (jak np w google drive).

def get_file_path(instance, filename) -> str:
"""Generate secure path for filename.

:param instance: The model instance
:param filename: The name of the file that was uploaded
:return: path to file
:rtype: str
"""
_now = datetime.now()
filename = "{year}/{month}/{day}/{password}/{filename}".format(
year=_now.strftime("%Y"),
month=_now.strftime("%m"),
day=_now.strftime("%d"),
password=get_random_string(20),
filename=filename,
)
return path.join("zalaczniki/", filename)

Sam adres będzie się składał z daty / skomplikowanego hasła dostępu / nazwy pliku. Pozwoli nam to wdrożyć prostą ale i skuteczną metodę dzielenia się plikami, nawet dla użytkowników spoza serwisu.


Załączam również pełny kod całej klasy:

from datetime import datetime
from os import path

from django.db import models
from django.utils.crypto import get_random_string
from django.utils.translation import ugettext_lazy as _
from PIL import Image


class AttachmentsFiles(models.Model):
"""This class is used to store the files that are uploaded to the system"""

title = models.CharField(_("Title"), max_length=80, blank=True)
file = models.FileField(
upload_to=get_file_path,
verbose_name=_("File"),
null=True,
blank=True,
)
visible = models.BooleanField(_("Visible"), default=False)
createdDate = models.DateTimeField(
_("Created date"), default=datetime.now, blank=True
)

def __str__(self):
"""Return file title."""
return self.title

def get_file_path(instance, filename) -> str:
"""Generate secure path for filename.

:param instance: The model instance
:param filename: The name of the file that was uploaded
:return: path to file
:rtype: str
"""
_now = datetime.now()
filename = "{year}/{month}/{day}/{password}/{filename}".format(
year=_now.strftime("%Y"),
month=_now.strftime("%m"),
day=_now.strftime("%d"),
password=get_random_string(20),
filename=filename,
)
return path.join("zalaczniki/", filename)

def extension(self) -> str:
"""Return file extension.

:raises Exception: Cant extract the extension
:return: file extension
:rtype: str
"""
try:
name, extension = path.splitext(self.file.name)
return extension[1:].lower()
except Exception:
return None

def save(self, *args, **kwargs):
"""Save file to disk."""
if not self.id:
super().save()

image_extension = ["jpg", "jpeg"]
if self.extension() in image_extension:
img = Image.open(self.file.path)

if img.height > 1024 or img.width > 1024:
new_img = (1024, 1024)
img.thumbnail(new_img)
img.save(self.file.path)

class Meta:
verbose_name = _("Attachment")
verbose_name_plural = _("Attachments")

3. Dla wytrwałych

w ramach małego bonusu, przestawiam sposób jak w panelu administracyjnym, wyświetlić miniaturkę zdjęcia, filmu czy też pliku pdf. 

class AttachmentsFilesAdmin(SuperInlineModelAdmin, admin.StackedInline):
model = AttachmentsFiles
extra = 1
readonly_fields = ('file_image',)

def file_image(self, obj):
image_extension = ['jpg', 'png', 'jpeg', 'gif', 'ico']
movie_extension = ['mp4', 'flv', 'avi']
sound_extension = ['mp3', 'ogv', 'vma', 'aac', 'ogg', 'wav']

if obj.extension() is not None:
if obj.extension() in image_extension:
return mark_safe(
'<a href="{url}" data-lightbox="roadtrip"><img src="{url}" loading="lazy" class="img-attachment" width="100" height="100" /></a>'.format(
url=obj.file.url, ))
elif obj.extension() in movie_extension:
return mark_safe(
'<video controls><source src="{url}" type="video/{ext}">'.format(
url=obj.file.url, ext=obj.extension()))
elif obj.extension() in sound_extension:
return mark_safe(
'<audio controls><source src="{url}" type="audio/{ext}">'.format(
url=obj.file.url, ext=obj.extension()))
elif obj.extension() == 'pdf':
return mark_safe(
"<object data='{url}' type='application/pdf' width='100%' height='700px'></object>".format(
url=obj.file.url, ext=obj.extension()))
else:
return mark_safe(
'File format unrecognized<br<a href="{url}">Click to download</a>>'.format(
url=obj.file.url, ))
else:
return ("")

file_image.short_description = ''
file_image.admin_order_field = ''

W tym przykładzie, dodajemy pole o nazwie file_image, w trybie tylko do odczytu. W momencie renderowania go (w zależności jaki ma format załącznik), ładujemy zupełnie inny tag html, umożliwiający wyświetlenie nam odpowiedniego znacznika.

Kamil Mirończuk

I kiedy czegoś gorąco pragniesz, to cały wszechświat sprzyja potajemnie twojemu pragnieniu
~Paulo Coelho

Komentarze

Zostaw komentarz

Twój adres mailowy NIE zostanie opublikowany. W razie otrzymania zapytania, otrzymasz na niego odpowiedź.
Wymagane pola są oznaczone jako *