Skip to content

Commit 4ea7a7b

Browse files
Merge pull request #3 from thewebscraping/feat/supported-more-file-types
feat: supported more file types
2 parents 0fc6052 + a40dda4 commit 4ea7a7b

File tree

18 files changed

+1221
-206
lines changed

18 files changed

+1221
-206
lines changed

django_chunk_file_upload/admin.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@
99
@admin.register(FileManager)
1010
class FileManagerModelAdmin(admin.ModelAdmin):
1111
form = ChunkedUploadFileAdminForm
12-
search_fields = ("name",)
1312
list_display = (
14-
"name",
13+
"id",
1514
"status",
1615
"created_at",
1716
"updated_at",
Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,71 @@
11
from __future__ import annotations
22

3+
from dataclasses import dataclass, fields
4+
35
from django.conf import settings
46

7+
from .constants import StatusChoices
8+
from .permissions import BasePermission, IsAuthenticated
9+
10+
11+
@dataclass(kw_only=True)
12+
class _Settings:
513

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

1919
@classmethod
20-
def from_kwargs(cls, **kwargs) -> "LazySettings":
21-
ret = cls()
22-
for k, v in kwargs.items():
23-
if hasattr(ret, k) and v is not None:
24-
if k == "js":
25-
v = list(set(list(ret.js) + ["js/upload.chunk.js"]))
26-
setattr(ret, k, v)
27-
return ret
20+
def from_kwargs(cls, **kwargs) -> "_Settings":
21+
return cls(**cls.get_kwargs(**kwargs))
22+
23+
24+
@dataclass(kw_only=True)
25+
class _ImageSettings(_Settings):
26+
quality: int = 82
27+
compress_level: int = 9
28+
max_width: int = 1280
29+
max_height: int = 720
30+
to_webp: bool = True
31+
32+
33+
@dataclass(kw_only=True)
34+
class _LazySettings(_Settings):
35+
css: tuple | list | set = (
36+
"css/upload.chunk.css",
37+
"css/toastr.min.css",
38+
"css/sweetalert2.min.css",
39+
)
40+
js: tuple | list | set = (
41+
"js/jquery-3.7.1.min.js",
42+
"js/spark-md5.min.js",
43+
"js/toastr.min.js",
44+
"js/sweetalert2.min.js",
45+
"js/upload.chunk.js",
46+
)
47+
upload_to: str = "%Y/%m/%d"
48+
chunk_size: int = 1024 * 1024 * 2 # 2MB
49+
is_metadata_storage: bool = False
50+
remove_file_on_update: bool = True
51+
status: StatusChoices = StatusChoices.PENDING
52+
permission_classes: tuple[BasePermission] = (IsAuthenticated,)
53+
optimize: bool = True
54+
image_optimizer: _ImageSettings = _ImageSettings()
55+
56+
@classmethod
57+
def from_kwargs(cls, **kwargs) -> "_LazySettings":
58+
kwargs = cls.get_kwargs(**kwargs)
59+
js = kwargs.pop("js", None) or []
60+
if js and isinstance(js, list):
61+
kwargs["js"] = list(set(js + ["js/upload.chunk.js"]))
62+
63+
image_optimizer = kwargs.pop("image_optimizer", {}) or {}
64+
if image_optimizer and isinstance(image_optimizer, dict):
65+
kwargs["image_optimizer"] = _ImageSettings.from_kwargs(**image_optimizer)
66+
return cls(**kwargs)
2867

2968

30-
app_settings = LazySettings.from_kwargs(
69+
app_settings = _LazySettings.from_kwargs(
3170
**getattr(settings, "DJANGO_CHUNK_FILE_UPLOAD", {})
3271
)

django_chunk_file_upload/constants.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from __future__ import annotations
2+
3+
from django.db.models import TextChoices
4+
from django.utils.translation import gettext_lazy as _
5+
6+
7+
class TypeChoices(TextChoices):
8+
ARCHIVE = "ARCHIVE", _("ARCHIVE")
9+
AUDIO = "AUDIO", _("AUDIO")
10+
BINARY = "BINARY", _("BINARY")
11+
DOCUMENT = "DOCUMENT", _("DOCUMENT")
12+
FONT = "FONT", _("FONT")
13+
HYPERTEXT = "HYPERTEXT", _("HYPERTEXT")
14+
IMAGE = "IMAGE", _("IMAGE")
15+
JSON = "JSON", _("JSON")
16+
MICROSOFT_EXCEL = "MICROSOFT_EXCEL", _("MICROSOFT_EXCEL")
17+
MICROSOFT_POWERPOINT = "MICROSOFT_POWERPOINT", _("MICROSOFT_POWERPOINT")
18+
MICROSOFT_WORD = "MICROSOFT_WORD", _("MICROSOFT_WORD")
19+
SOURCE_CODE = "SOURCE_CODE", _("SOURCE_CODE")
20+
SEPARATED = "SEPARATED", _("SEPARATED")
21+
TEXT = "TEXT", _("TEXT")
22+
VIDEO = "VIDEO", _("VIDEO")
23+
XML = "XML", _("XML")
24+
__empty__ = _("Unknown")
25+
26+
27+
class StatusChoices(TextChoices):
28+
COMPLETED = "COMPLETED", _("Completed")
29+
ERROR = "ERROR", _("Error")
30+
PENDING = "PENDING", _("Pending")
31+
PROCESSING = "PROCESSING", _("Processing")
32+
33+
34+
class ActionChoices(TextChoices):
35+
CREATE = "_add", _("Add")
36+
UPDATE = "_save", _("Save")
37+
DELETE = "_delete", _("Delete")
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Generated by Django 5.0.7 on 2024-08-19 20:46
2+
3+
import django.db.models.deletion
4+
from django.conf import settings
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("django_chunk_file_upload", "0001_initial"),
12+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13+
]
14+
15+
operations = [
16+
migrations.AlterModelOptions(
17+
name="filemanager",
18+
options={
19+
"ordering": ("-created_at",),
20+
"verbose_name": "File Manager",
21+
"verbose_name_plural": "File Manager",
22+
},
23+
),
24+
migrations.AddField(
25+
model_name="filemanager",
26+
name="type",
27+
field=models.CharField(
28+
choices=[
29+
(None, "Unknown"),
30+
("ARCHIVE", "ARCHIVE"),
31+
("AUDIO", "AUDIO"),
32+
("BINARY", "BINARY"),
33+
("DOCUMENT", "DOCUMENT"),
34+
("FONT", "FONT"),
35+
("HYPERTEXT", "HYPERTEXT"),
36+
("IMAGE", "IMAGE"),
37+
("JSON", "JSON"),
38+
("MICROSOFT_EXCEL", "MICROSOFT_EXCEL"),
39+
("MICROSOFT_POWERPOINT", "MICROSOFT_POWERPOINT"),
40+
("MICROSOFT_WORD", "MICROSOFT_WORD"),
41+
("SOURCE_CODE", "SOURCE_CODE"),
42+
("SEPARATED", "SEPARATED"),
43+
("TEXT", "TEXT"),
44+
("VIDEO", "VIDEO"),
45+
("XML", "XML"),
46+
],
47+
default="Unknown",
48+
max_length=255,
49+
verbose_name="Type",
50+
),
51+
),
52+
migrations.AddField(
53+
model_name="filemanager",
54+
name="user",
55+
field=models.ForeignKey(
56+
blank=True,
57+
null=True,
58+
on_delete=django.db.models.deletion.SET_NULL,
59+
related_name="files",
60+
to=settings.AUTH_USER_MODEL,
61+
),
62+
),
63+
migrations.AlterField(
64+
model_name="filemanager",
65+
name="checksum",
66+
field=models.CharField(max_length=255),
67+
),
68+
migrations.RemoveField(
69+
model_name="filemanager",
70+
name="name",
71+
),
72+
migrations.AlterUniqueTogether(
73+
name="filemanager",
74+
unique_together={("user", "checksum")},
75+
),
76+
]

django_chunk_file_upload/models.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,46 @@
11
from __future__ import annotations
22

3+
from django.conf import settings
34
from django.db import models
45
from django.utils.translation import gettext_lazy as _
56

6-
from .typed import StatusChoices
7+
from .constants import StatusChoices, TypeChoices
78

89

910
class FileManager(models.Model):
1011
created_at = models.DateTimeField(auto_now_add=True)
1112
updated_at = models.DateTimeField(auto_now=True)
12-
name = models.CharField(_("Name"), max_length=255)
1313
file = models.FileField()
1414
status = models.CharField(
1515
_("Status"),
1616
max_length=255,
1717
choices=StatusChoices.choices,
1818
default=StatusChoices.PENDING,
1919
)
20-
checksum = models.CharField(max_length=255, unique=True)
20+
type = models.CharField(
21+
_("Type"),
22+
max_length=255,
23+
choices=TypeChoices.choices,
24+
default=TypeChoices.__empty__,
25+
)
26+
checksum = models.CharField(max_length=255)
2127
eof = models.BooleanField(default=False)
28+
user = models.ForeignKey(
29+
settings.AUTH_USER_MODEL,
30+
on_delete=models.SET_NULL,
31+
null=True,
32+
blank=True,
33+
related_name="files",
34+
)
2235
metadata = models.JSONField(default=dict)
2336

2437
class Meta:
2538
db_table = "django_chunk_file_upload"
2639
indexes = [models.Index(fields=["checksum"], name="file_manager_checksum_idx")]
27-
verbose_name = _("Django Chunk File Upload")
28-
verbose_name_plural = _("Django Chunk File Upload")
29-
30-
def __str__(self):
31-
return self.name
40+
ordering = ("-created_at",)
41+
unique_together = (
42+
"user",
43+
"checksum",
44+
)
45+
verbose_name = _("File Manager")
46+
verbose_name_plural = _("File Manager")

django_chunk_file_upload/optimize.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from PIL import Image, UnidentifiedImageError
2+
from PIL.JpegImagePlugin import JpegImageFile
3+
from PIL.PngImagePlugin import PngImageFile
4+
from PIL.WebPImagePlugin import WebPImageFile
5+
6+
from .app_settings import app_settings
7+
from .constants import TypeChoices
8+
from .utils import get_file_path
9+
10+
11+
class BaseOptimizer:
12+
"""Base Optimizer"""
13+
14+
def __init__(self, instance, file, *args, **kwargs):
15+
self._instance = instance
16+
self._file = file
17+
18+
@property
19+
def instance(self):
20+
return self._instance
21+
22+
@property
23+
def file(self):
24+
return self._file
25+
26+
def optimize(self):
27+
pass
28+
29+
30+
class ImageOptimizer(BaseOptimizer):
31+
"""Image Optimizer"""
32+
33+
_supported_file_types = (".jpg", ".jpeg", ".png", ".webp")
34+
35+
def __init__(
36+
self,
37+
instance,
38+
file,
39+
*args,
40+
**kwargs,
41+
):
42+
super().__init__(instance, file, *args, **kwargs)
43+
44+
def open(self) -> Image.Image | None:
45+
try:
46+
image = Image.open(self.file.file)
47+
return image
48+
except UnidentifiedImageError:
49+
pass
50+
51+
def close(self, image: Image.Image) -> None:
52+
if isinstance(image, Image.Image):
53+
image.close()
54+
55+
def optimize(self) -> None:
56+
resized_img = self.open()
57+
if not resized_img:
58+
return
59+
60+
fm, ext = None, None
61+
if isinstance(resized_img, PngImageFile):
62+
fm, ext = "PNG", ".png"
63+
resized_img = resized_img.convert("P", palette=Image.ADAPTIVE)
64+
elif isinstance(resized_img, JpegImageFile):
65+
fm, ext = "JPEG", ".jpg"
66+
elif isinstance(resized_img, WebPImageFile):
67+
fm, ext = "WEBP", ".webp"
68+
69+
if app_settings.image_optimizer.to_webp:
70+
fm, ext = "WEBP", ".webp"
71+
72+
if str(ext) in self._supported_file_types:
73+
resized_img = self.resize(resized_img)
74+
orig_save_path = self.file.save_path
75+
filename = self.file.repl_filename + ext
76+
self.file.path = get_file_path(filename, self.file._upload_to)
77+
resized_img.save(
78+
self.file.save_path,
79+
fm,
80+
optimize=True,
81+
quality=app_settings.image_optimizer.quality,
82+
compress_level=app_settings.image_optimizer.compress_level,
83+
)
84+
if orig_save_path != self.file.save_path:
85+
self.instance.file.delete()
86+
self.file.extension = ext
87+
88+
self.close(resized_img)
89+
90+
def resize(self, image: Image.Image) -> Image.Image:
91+
"""Resize image to fit with max width and max height"""
92+
93+
w, h = image.size
94+
aspect_ratio = w / h
95+
96+
if (
97+
w > app_settings.image_optimizer.max_width
98+
or h > app_settings.image_optimizer.max_height
99+
):
100+
if aspect_ratio > 1:
101+
nw = app_settings.image_optimizer.max_width
102+
nh = int(app_settings.image_optimizer.max_width / aspect_ratio)
103+
else:
104+
nh = app_settings.image_optimizer.max_height
105+
nw = int(app_settings.image_optimizer.max_height * aspect_ratio)
106+
107+
return image.resize((nw, nh), Image.LANCZOS)
108+
return image
109+
110+
111+
MapOptimizer = {TypeChoices.IMAGE: ImageOptimizer}

0 commit comments

Comments
 (0)