Skip to content

feat: supported more file types #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions django_chunk_file_upload/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@
@admin.register(FileManager)
class FileManagerModelAdmin(admin.ModelAdmin):
form = ChunkedUploadFileAdminForm
search_fields = ("name",)
list_display = (
"name",
"id",
"status",
"created_at",
"updated_at",
Expand Down
81 changes: 60 additions & 21 deletions django_chunk_file_upload/app_settings.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,71 @@
from __future__ import annotations

from dataclasses import dataclass, fields

from django.conf import settings

from .constants import StatusChoices
from .permissions import BasePermission, IsAuthenticated


@dataclass(kw_only=True)
class _Settings:

class LazySettings:
def __init__(self):
self.css = ("css/upload.chunk.css", "css/toastr.min.css")
self.js = (
"js/jquery-3.7.1.min.js",
"js/spark-md5.min.js",
"js/toastr.min.js",
"js/upload.chunk.js",
)
self.upload_to = "%Y/%m/%d"
self.chunk_size = 1024 * 1024 * 2 # 2MB
self.is_metadata_storage = True
@classmethod
def get_kwargs(cls, **kwargs) -> dict:
model_fields = {field.name for field in fields(cls)}
return {k: v for k, v in kwargs.items() if k in model_fields}

@classmethod
def from_kwargs(cls, **kwargs) -> "LazySettings":
ret = cls()
for k, v in kwargs.items():
if hasattr(ret, k) and v is not None:
if k == "js":
v = list(set(list(ret.js) + ["js/upload.chunk.js"]))
setattr(ret, k, v)
return ret
def from_kwargs(cls, **kwargs) -> "_Settings":
return cls(**cls.get_kwargs(**kwargs))


@dataclass(kw_only=True)
class _ImageSettings(_Settings):
quality: int = 82
compress_level: int = 9
max_width: int = 1280
max_height: int = 720
to_webp: bool = True


@dataclass(kw_only=True)
class _LazySettings(_Settings):
css: tuple | list | set = (
"css/upload.chunk.css",
"css/toastr.min.css",
"css/sweetalert2.min.css",
)
js: tuple | list | set = (
"js/jquery-3.7.1.min.js",
"js/spark-md5.min.js",
"js/toastr.min.js",
"js/sweetalert2.min.js",
"js/upload.chunk.js",
)
upload_to: str = "%Y/%m/%d"
chunk_size: int = 1024 * 1024 * 2 # 2MB
is_metadata_storage: bool = False
remove_file_on_update: bool = True
status: StatusChoices = StatusChoices.PENDING
permission_classes: tuple[BasePermission] = (IsAuthenticated,)
optimize: bool = True
image_optimizer: _ImageSettings = _ImageSettings()

@classmethod
def from_kwargs(cls, **kwargs) -> "_LazySettings":
kwargs = cls.get_kwargs(**kwargs)
js = kwargs.pop("js", None) or []
if js and isinstance(js, list):
kwargs["js"] = list(set(js + ["js/upload.chunk.js"]))

image_optimizer = kwargs.pop("image_optimizer", {}) or {}
if image_optimizer and isinstance(image_optimizer, dict):
kwargs["image_optimizer"] = _ImageSettings.from_kwargs(**image_optimizer)
return cls(**kwargs)


app_settings = LazySettings.from_kwargs(
app_settings = _LazySettings.from_kwargs(
**getattr(settings, "DJANGO_CHUNK_FILE_UPLOAD", {})
)
37 changes: 37 additions & 0 deletions django_chunk_file_upload/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from __future__ import annotations

from django.db.models import TextChoices
from django.utils.translation import gettext_lazy as _


class TypeChoices(TextChoices):
ARCHIVE = "ARCHIVE", _("ARCHIVE")
AUDIO = "AUDIO", _("AUDIO")
BINARY = "BINARY", _("BINARY")
DOCUMENT = "DOCUMENT", _("DOCUMENT")
FONT = "FONT", _("FONT")
HYPERTEXT = "HYPERTEXT", _("HYPERTEXT")
IMAGE = "IMAGE", _("IMAGE")
JSON = "JSON", _("JSON")
MICROSOFT_EXCEL = "MICROSOFT_EXCEL", _("MICROSOFT_EXCEL")
MICROSOFT_POWERPOINT = "MICROSOFT_POWERPOINT", _("MICROSOFT_POWERPOINT")
MICROSOFT_WORD = "MICROSOFT_WORD", _("MICROSOFT_WORD")
SOURCE_CODE = "SOURCE_CODE", _("SOURCE_CODE")
SEPARATED = "SEPARATED", _("SEPARATED")
TEXT = "TEXT", _("TEXT")
VIDEO = "VIDEO", _("VIDEO")
XML = "XML", _("XML")
__empty__ = _("Unknown")


class StatusChoices(TextChoices):
COMPLETED = "COMPLETED", _("Completed")
ERROR = "ERROR", _("Error")
PENDING = "PENDING", _("Pending")
PROCESSING = "PROCESSING", _("Processing")


class ActionChoices(TextChoices):
CREATE = "_add", _("Add")
UPDATE = "_save", _("Save")
DELETE = "_delete", _("Delete")
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Generated by Django 5.0.7 on 2024-08-19 20:46

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("django_chunk_file_upload", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.AlterModelOptions(
name="filemanager",
options={
"ordering": ("-created_at",),
"verbose_name": "File Manager",
"verbose_name_plural": "File Manager",
},
),
migrations.AddField(
model_name="filemanager",
name="type",
field=models.CharField(
choices=[
(None, "Unknown"),
("ARCHIVE", "ARCHIVE"),
("AUDIO", "AUDIO"),
("BINARY", "BINARY"),
("DOCUMENT", "DOCUMENT"),
("FONT", "FONT"),
("HYPERTEXT", "HYPERTEXT"),
("IMAGE", "IMAGE"),
("JSON", "JSON"),
("MICROSOFT_EXCEL", "MICROSOFT_EXCEL"),
("MICROSOFT_POWERPOINT", "MICROSOFT_POWERPOINT"),
("MICROSOFT_WORD", "MICROSOFT_WORD"),
("SOURCE_CODE", "SOURCE_CODE"),
("SEPARATED", "SEPARATED"),
("TEXT", "TEXT"),
("VIDEO", "VIDEO"),
("XML", "XML"),
],
default="Unknown",
max_length=255,
verbose_name="Type",
),
),
migrations.AddField(
model_name="filemanager",
name="user",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="files",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="filemanager",
name="checksum",
field=models.CharField(max_length=255),
),
migrations.RemoveField(
model_name="filemanager",
name="name",
),
migrations.AlterUniqueTogether(
name="filemanager",
unique_together={("user", "checksum")},
),
]
31 changes: 23 additions & 8 deletions django_chunk_file_upload/models.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,46 @@
from __future__ import annotations

from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _

from .typed import StatusChoices
from .constants import StatusChoices, TypeChoices


class FileManager(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
name = models.CharField(_("Name"), max_length=255)
file = models.FileField()
status = models.CharField(
_("Status"),
max_length=255,
choices=StatusChoices.choices,
default=StatusChoices.PENDING,
)
checksum = models.CharField(max_length=255, unique=True)
type = models.CharField(
_("Type"),
max_length=255,
choices=TypeChoices.choices,
default=TypeChoices.__empty__,
)
checksum = models.CharField(max_length=255)
eof = models.BooleanField(default=False)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="files",
)
metadata = models.JSONField(default=dict)

class Meta:
db_table = "django_chunk_file_upload"
indexes = [models.Index(fields=["checksum"], name="file_manager_checksum_idx")]
verbose_name = _("Django Chunk File Upload")
verbose_name_plural = _("Django Chunk File Upload")

def __str__(self):
return self.name
ordering = ("-created_at",)
unique_together = (
"user",
"checksum",
)
verbose_name = _("File Manager")
verbose_name_plural = _("File Manager")
111 changes: 111 additions & 0 deletions django_chunk_file_upload/optimize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from PIL import Image, UnidentifiedImageError
from PIL.JpegImagePlugin import JpegImageFile
from PIL.PngImagePlugin import PngImageFile
from PIL.WebPImagePlugin import WebPImageFile

from .app_settings import app_settings
from .constants import TypeChoices
from .utils import get_file_path


class BaseOptimizer:
"""Base Optimizer"""

def __init__(self, instance, file, *args, **kwargs):
self._instance = instance
self._file = file

@property
def instance(self):
return self._instance

@property
def file(self):
return self._file

def optimize(self):
pass


class ImageOptimizer(BaseOptimizer):
"""Image Optimizer"""

_supported_file_types = (".jpg", ".jpeg", ".png", ".webp")

def __init__(
self,
instance,
file,
*args,
**kwargs,
):
super().__init__(instance, file, *args, **kwargs)

def open(self) -> Image.Image | None:
try:
image = Image.open(self.file.file)
return image
except UnidentifiedImageError:
pass

def close(self, image: Image.Image) -> None:
if isinstance(image, Image.Image):
image.close()

def optimize(self) -> None:
resized_img = self.open()
if not resized_img:
return

fm, ext = None, None
if isinstance(resized_img, PngImageFile):
fm, ext = "PNG", ".png"
resized_img = resized_img.convert("P", palette=Image.ADAPTIVE)
elif isinstance(resized_img, JpegImageFile):
fm, ext = "JPEG", ".jpg"
elif isinstance(resized_img, WebPImageFile):
fm, ext = "WEBP", ".webp"

if app_settings.image_optimizer.to_webp:
fm, ext = "WEBP", ".webp"

if str(ext) in self._supported_file_types:
resized_img = self.resize(resized_img)
orig_save_path = self.file.save_path
filename = self.file.repl_filename + ext
self.file.path = get_file_path(filename, self.file._upload_to)
resized_img.save(
self.file.save_path,
fm,
optimize=True,
quality=app_settings.image_optimizer.quality,
compress_level=app_settings.image_optimizer.compress_level,
)
if orig_save_path != self.file.save_path:
self.instance.file.delete()
self.file.extension = ext

self.close(resized_img)

def resize(self, image: Image.Image) -> Image.Image:
"""Resize image to fit with max width and max height"""

w, h = image.size
aspect_ratio = w / h

if (
w > app_settings.image_optimizer.max_width
or h > app_settings.image_optimizer.max_height
):
if aspect_ratio > 1:
nw = app_settings.image_optimizer.max_width
nh = int(app_settings.image_optimizer.max_width / aspect_ratio)
else:
nh = app_settings.image_optimizer.max_height
nw = int(app_settings.image_optimizer.max_height * aspect_ratio)

return image.resize((nw, nh), Image.LANCZOS)
return image


MapOptimizer = {TypeChoices.IMAGE: ImageOptimizer}
Loading