attachment.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import os
  2. from hashlib import md5
  3. from io import BytesIO
  4. from PIL import Image
  5. from django.conf import settings
  6. from django.core.files import File
  7. from django.core.files.base import ContentFile
  8. from django.db import models
  9. from django.urls import reverse
  10. from django.utils import timezone
  11. from django.utils.crypto import get_random_string
  12. from django.utils.encoding import python_2_unicode_compatible
  13. from misago.core.utils import slugify
  14. def upload_to(instance, filename):
  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],
  23. extension
  24. ))
  25. return os.path.join(
  26. 'attachments', spread_path[:2], spread_path[2:4], secret, filename_clean)
  27. @python_2_unicode_compatible
  28. class Attachment(models.Model):
  29. secret = models.CharField(max_length=64)
  30. filetype = models.ForeignKey('AttachmentType')
  31. post = models.ForeignKey(
  32. 'Post',
  33. blank=True,
  34. null=True,
  35. on_delete=models.SET_NULL
  36. )
  37. uploaded_on = models.DateTimeField(default=timezone.now, db_index=True)
  38. uploader = models.ForeignKey(
  39. settings.AUTH_USER_MODEL,
  40. blank=True,
  41. null=True,
  42. on_delete=models.SET_NULL
  43. )
  44. uploader_name = models.CharField(max_length=255)
  45. uploader_slug = models.CharField(max_length=255, db_index=True)
  46. uploader_ip = models.GenericIPAddressField()
  47. filename = models.CharField(max_length=255, db_index=True)
  48. size = models.PositiveIntegerField(default=0, db_index=True)
  49. thumbnail = models.ImageField(
  50. max_length=255,
  51. blank=True,
  52. null=True,
  53. upload_to=upload_to
  54. )
  55. image = models.ImageField(
  56. max_length=255,
  57. blank=True,
  58. null=True,
  59. upload_to=upload_to
  60. )
  61. file = models.FileField(
  62. max_length=255,
  63. blank=True,
  64. null=True,
  65. upload_to=upload_to
  66. )
  67. def __str__(self):
  68. return self.filename
  69. def delete(self, *args, **kwargs):
  70. self.delete_files()
  71. return super(Attachment, self).delete(*args, **kwargs)
  72. def delete_files(self):
  73. if self.thumbnail:
  74. self.thumbnail.delete(save=False)
  75. if self.image:
  76. self.image.delete(save=False)
  77. if self.file:
  78. self.file.delete(save=False)
  79. @classmethod
  80. def generate_new_secret(cls):
  81. return get_random_string(settings.MISAGO_ATTACHMENT_SECRET_LENGTH)
  82. @property
  83. def is_image(self):
  84. return bool(self.image)
  85. @property
  86. def is_file(self):
  87. return not self.is_image
  88. def get_absolute_url(self):
  89. return reverse('misago:attachment', kwargs={
  90. 'pk': self.pk,
  91. 'secret': self.secret,
  92. })
  93. def get_thumbnail_url(self):
  94. if self.thumbnail:
  95. return reverse('misago:attachment-thumbnail', kwargs={
  96. 'pk': self.pk,
  97. 'secret': self.secret,
  98. })
  99. else:
  100. return None
  101. def set_file(self, upload):
  102. self.file = File(upload, upload.name)
  103. def set_image(self, upload):
  104. fileformat = self.filetype.extensions_list[0]
  105. self.image = File(upload, upload.name)
  106. thumbnail = Image.open(upload)
  107. downscale_image = (
  108. thumbnail.size[0] > settings.MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT[0] or
  109. thumbnail.size[1] > settings.MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT[1]
  110. )
  111. strip_animation = fileformat == 'gif'
  112. thumb_stream = BytesIO()
  113. if downscale_image:
  114. thumbnail.thumbnail(settings.MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT)
  115. if fileformat == 'jpg':
  116. # normalize jpg to jpeg for Pillow
  117. thumbnail.save(thumb_stream, 'jpeg')
  118. else:
  119. thumbnail.save(thumb_stream, fileformat)
  120. elif strip_animation:
  121. thumbnail.save(thumb_stream, fileformat)
  122. if downscale_image or strip_animation:
  123. self.thumbnail = ContentFile(thumb_stream.getvalue(), upload.name)