attachment.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import os
  2. from hashlib import md5
  3. from io import BytesIO
  4. from django.core.files import File
  5. from django.core.files.base import ContentFile
  6. from django.db import models
  7. from django.urls import reverse
  8. from django.utils import timezone
  9. from django.utils.crypto import get_random_string
  10. from PIL import Image
  11. from ...conf import settings
  12. from ...core.utils import slugify
  13. def upload_to(instance, filename):
  14. # pylint: disable=undefined-loop-variable
  15. spread_path = md5(str(instance.secret[:16]).encode()).hexdigest()
  16. secret = Attachment.generate_new_secret()
  17. filename_lowered = filename.lower().strip()
  18. for extension in instance.filetype.extensions_list:
  19. if filename_lowered.endswith(extension):
  20. break
  21. filename_clean = ".".join(
  22. (slugify(filename[: (len(extension) + 1) * -1])[:16], extension)
  23. )
  24. return os.path.join(
  25. "attachments", spread_path[:2], spread_path[2:4], secret, filename_clean
  26. )
  27. class Attachment(models.Model):
  28. secret = models.CharField(max_length=64)
  29. filetype = models.ForeignKey("AttachmentType", on_delete=models.CASCADE)
  30. post = models.ForeignKey("Post", blank=True, null=True, on_delete=models.SET_NULL)
  31. uploaded_on = models.DateTimeField(default=timezone.now, db_index=True)
  32. uploader = models.ForeignKey(
  33. settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL
  34. )
  35. uploader_name = models.CharField(max_length=255)
  36. uploader_slug = models.CharField(max_length=255, db_index=True)
  37. filename = models.CharField(max_length=255, db_index=True)
  38. size = models.PositiveIntegerField(default=0, db_index=True)
  39. thumbnail = models.ImageField(
  40. max_length=255, blank=True, null=True, upload_to=upload_to
  41. )
  42. image = models.ImageField(
  43. max_length=255, blank=True, null=True, upload_to=upload_to
  44. )
  45. file = models.FileField(max_length=255, blank=True, null=True, upload_to=upload_to)
  46. def __str__(self):
  47. return self.filename
  48. def delete(self, *args, **kwargs):
  49. self.delete_files()
  50. return super().delete(*args, **kwargs)
  51. def delete_files(self):
  52. if self.thumbnail:
  53. self.thumbnail.delete(save=False)
  54. if self.image:
  55. self.image.delete(save=False)
  56. if self.file:
  57. self.file.delete(save=False)
  58. @classmethod
  59. def generate_new_secret(cls):
  60. return get_random_string(settings.MISAGO_ATTACHMENT_SECRET_LENGTH)
  61. @property
  62. def is_image(self):
  63. return bool(self.image)
  64. @property
  65. def is_file(self):
  66. return not self.is_image
  67. def get_absolute_url(self):
  68. return reverse(
  69. "misago:attachment", kwargs={"pk": self.pk, "secret": self.secret}
  70. )
  71. def get_thumbnail_url(self):
  72. if self.thumbnail:
  73. return reverse(
  74. "misago:attachment-thumbnail",
  75. kwargs={"pk": self.pk, "secret": self.secret},
  76. )
  77. def set_file(self, upload):
  78. self.file = File(upload, upload.name)
  79. def set_image(self, upload):
  80. fileformat = self.filetype.extensions_list[0]
  81. self.image = File(upload, upload.name)
  82. thumbnail = Image.open(upload)
  83. downscale_image = (
  84. thumbnail.size[0] > settings.MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT[0]
  85. or thumbnail.size[1] > settings.MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT[1]
  86. )
  87. strip_animation = fileformat == "gif"
  88. thumb_stream = BytesIO()
  89. if downscale_image:
  90. thumbnail.thumbnail(settings.MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT)
  91. if fileformat == "jpg":
  92. # normalize jpg to jpeg for Pillow
  93. thumbnail.save(thumb_stream, "jpeg")
  94. else:
  95. thumbnail.save(thumb_stream, "png")
  96. elif strip_animation:
  97. thumbnail.save(thumb_stream, "png")
  98. if downscale_image or strip_animation:
  99. self.thumbnail = ContentFile(thumb_stream.getvalue(), upload.name)