Skip to content

Commit fd0f6fc

Browse files
authored
Merge pull request #136 from HackSoftware/files/improvements
Files: Improvements
2 parents dabaa4f + ea02aa5 commit fd0f6fc

File tree

9 files changed

+71
-39
lines changed

9 files changed

+71
-39
lines changed

config/settings/files_and_storages.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77

88
FILE_UPLOAD_STRATEGY = env_to_enum(
99
FileUploadStrategy,
10-
env("FILE_UPLOAD_STRATEGY", default="direct")
10+
env("FILE_UPLOAD_STRATEGY", default="standard")
1111
)
1212
FILE_UPLOAD_STORAGE = env_to_enum(
1313
FileUploadStorage,
1414
env("FILE_UPLOAD_STORAGE", default="local")
1515
)
1616

17+
FILE_MAX_SIZE = env.int("FILE_MAX_SIZE", default=10485760) # 10 MiB
18+
1719
if FILE_UPLOAD_STORAGE == FileUploadStorage.LOCAL:
1820
MEDIA_ROOT_NAME = "media"
1921
MEDIA_ROOT = os.path.join(BASE_DIR, MEDIA_ROOT_NAME)

styleguide_example/files/admin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from styleguide_example.files.models import File
77
from styleguide_example.files.services import (
8-
FileDirectUploadService
8+
FileStandardUploadService
99
)
1010

1111

@@ -64,7 +64,7 @@ def save_model(self, request, obj, form, change):
6464
try:
6565
cleaned_data = form.cleaned_data
6666

67-
service = FileDirectUploadService(
67+
service = FileStandardUploadService(
6868
file_obj=cleaned_data["file"],
6969
user=cleaned_data["uploaded_by"]
7070
)

styleguide_example/files/apis.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@
66

77
from styleguide_example.files.models import File
88
from styleguide_example.files.services import (
9-
FileDirectUploadService,
10-
FilePassThruUploadService
9+
FileStandardUploadService,
10+
FileDirectUploadService
1111
)
1212

1313
from styleguide_example.api.mixins import ApiAuthMixin
1414

1515

16-
class FileDirectUploadApi(ApiAuthMixin, APIView):
16+
class FileStandardUploadApi(ApiAuthMixin, APIView):
1717
def post(self, request):
18-
service = FileDirectUploadService(
18+
service = FileStandardUploadService(
1919
user=request.user,
2020
file_obj=request.FILES["file"]
2121
)
@@ -24,7 +24,7 @@ def post(self, request):
2424
return Response(data={"id": file.id}, status=status.HTTP_201_CREATED)
2525

2626

27-
class FilePassThruUploadStartApi(ApiAuthMixin, APIView):
27+
class FileDirectUploadStartApi(ApiAuthMixin, APIView):
2828
class InputSerializer(serializers.Serializer):
2929
file_name = serializers.CharField()
3030
file_type = serializers.CharField()
@@ -33,25 +33,25 @@ def post(self, request, *args, **kwargs):
3333
serializer = self.InputSerializer(data=request.data)
3434
serializer.is_valid(raise_exception=True)
3535

36-
service = FilePassThruUploadService(request.user)
36+
service = FileDirectUploadService(request.user)
3737
presigned_data = service.start(**serializer.validated_data)
3838

3939
return Response(data=presigned_data)
4040

4141

42-
class FilePassThruUploadLocalApi(ApiAuthMixin, APIView):
42+
class FileDirectUploadLocalApi(ApiAuthMixin, APIView):
4343
def post(self, request, file_id):
4444
file = get_object_or_404(File, id=file_id)
4545

46-
file_object = request.FILES["file"]
46+
file_obj = request.FILES["file"]
4747

48-
service = FilePassThruUploadService(request.user)
49-
file = service.upload_local(file=file, file_object=file_object)
48+
service = FileDirectUploadService(request.user)
49+
file = service.upload_local(file=file, file_obj=file_obj)
5050

5151
return Response({"id": file.id})
5252

5353

54-
class FilePassThruUploadFinishApi(ApiAuthMixin, APIView):
54+
class FileDirectUploadFinishApi(ApiAuthMixin, APIView):
5555
class InputSerializer(serializers.Serializer):
5656
file_id = serializers.CharField()
5757

@@ -63,7 +63,7 @@ def post(self, request):
6363

6464
file = get_object_or_404(File, id=file_id)
6565

66-
service = FilePassThruUploadService(request.user)
66+
service = FileDirectUploadService(request.user)
6767
service.finish(file=file)
6868

6969
return Response({"id": file.id})

styleguide_example/files/enums.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33

44
class FileUploadStrategy(Enum):
5+
STANDARD = "standard"
56
DIRECT = "direct"
6-
PASS_THRU = "pass-thru"
77

88

99
class FileUploadStorage(Enum):

styleguide_example/files/models.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from django.db import models
32
from django.conf import settings
43

@@ -13,7 +12,11 @@
1312

1413

1514
class File(BaseModel):
16-
file = models.FileField(upload_to=file_generate_upload_path, null=True, blank=True)
15+
file = models.FileField(
16+
upload_to=file_generate_upload_path,
17+
blank=True,
18+
null=True
19+
)
1720

1821
original_file_name = models.TextField()
1922

styleguide_example/files/services.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
from django.conf import settings
66
from django.db import transaction
77
from django.utils import timezone
8+
from django.core.exceptions import ValidationError
89

910
from styleguide_example.files.models import File
1011
from styleguide_example.files.utils import (
1112
file_generate_upload_path,
1213
file_generate_local_upload_url,
13-
file_generate_name
14+
file_generate_name,
15+
bytes_to_mib
1416
)
1517
from styleguide_example.files.enums import FileUploadStorage
1618

@@ -19,7 +21,14 @@
1921
from styleguide_example.users.models import BaseUser
2022

2123

22-
class FileDirectUploadService:
24+
def _validate_file_size(file_obj):
25+
max_size = settings.FILE_MAX_SIZE
26+
27+
if file_obj.size > max_size:
28+
raise ValidationError(f"File is too large. It should not exceed {bytes_to_mib(max_size)} MiB")
29+
30+
31+
class FileStandardUploadService:
2332
"""
2433
This also serves as an example of a service class,
2534
which encapsulates 2 different behaviors (create & update) under a namespace.
@@ -49,6 +58,8 @@ def _infer_file_name_and_type(self, file_name: str = "", file_type: str = "") ->
4958

5059
@transaction.atomic
5160
def create(self, file_name: str = "", file_type: str = "") -> File:
61+
_validate_file_size(self.file_obj)
62+
5263
file_name, file_type = self._infer_file_name_and_type(file_name, file_type)
5364

5465
obj = File(
@@ -67,6 +78,8 @@ def create(self, file_name: str = "", file_type: str = "") -> File:
6778

6879
@transaction.atomic
6980
def update(self, file: File, file_name: str = "", file_type: str = "") -> File:
81+
_validate_file_size(self.file_obj)
82+
7083
file_name, file_type = self._infer_file_name_and_type(file_name, file_type)
7184

7285
file.file = self.file_obj
@@ -82,7 +95,7 @@ def update(self, file: File, file_name: str = "", file_type: str = "") -> File:
8295
return file
8396

8497

85-
class FilePassThruUploadService:
98+
class FileDirectUploadService:
8699
"""
87100
This also serves as an example of a service class,
88101
which encapsulates a flow (start & finish) + one-off action (upload_local) into a namespace.
@@ -138,9 +151,11 @@ def finish(self, *, file: File) -> File:
138151
return file
139152

140153
@transaction.atomic
141-
def upload_local(self, *, file: File, file_object) -> File:
154+
def upload_local(self, *, file: File, file_obj) -> File:
155+
_validate_file_size(file_obj)
156+
142157
# Potentially, check against user
143-
file.file = file_object
158+
file.file = file_obj
144159
file.full_clean()
145160
file.save()
146161

styleguide_example/files/urls.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from django.urls import path, include
22

33
from styleguide_example.files.apis import (
4-
FileDirectUploadApi,
4+
FileStandardUploadApi,
55

6-
FilePassThruUploadStartApi,
7-
FilePassThruUploadFinishApi,
8-
FilePassThruUploadLocalApi,
6+
FileDirectUploadStartApi,
7+
FileDirectUploadFinishApi,
8+
FileDirectUploadLocalApi,
99
)
1010

1111

@@ -14,29 +14,29 @@
1414
"upload/",
1515
include(([
1616
path(
17-
"direct/",
18-
FileDirectUploadApi.as_view(),
19-
name="direct"
17+
"standard/",
18+
FileStandardUploadApi.as_view(),
19+
name="standard"
2020
),
2121
path(
22-
"pass-thru/",
22+
"direct/",
2323
include(([
2424
path(
2525
"start/",
26-
FilePassThruUploadStartApi.as_view(),
26+
FileDirectUploadStartApi.as_view(),
2727
name="start"
2828
),
2929
path(
3030
"finish/",
31-
FilePassThruUploadFinishApi.as_view(),
31+
FileDirectUploadFinishApi.as_view(),
3232
name="finish"
3333
),
3434
path(
3535
"local/<str:file_id>/",
36-
FilePassThruUploadLocalApi.as_view(),
36+
FileDirectUploadLocalApi.as_view(),
3737
name="local"
3838
)
39-
], "pass-thru"))
39+
], "direct"))
4040
)
4141
], "upload"))
4242
)

styleguide_example/files/utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,13 @@ def file_generate_upload_path(instance, filename):
1818

1919
def file_generate_local_upload_url(*, file_id: str):
2020
url = reverse(
21-
"api:files:upload:pass-thru:local",
21+
"api:files:upload:direct:local",
2222
kwargs={"file_id": file_id}
2323
)
2424

2525
return f"{settings.APP_DOMAIN}{url}"
26+
27+
28+
def bytes_to_mib(value: int) -> float:
29+
# 1 bytes = 9.5367431640625E-7 mebibytes
30+
return value * 9.5367431640625E-7

styleguide_example/integrations/aws/client.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class S3Credentials:
1717
bucket_name: str
1818
default_acl: str
1919
presigned_expiry: int
20+
max_size: int
2021

2122

2223
@lru_cache
@@ -28,7 +29,8 @@ def s3_get_credentials() -> S3Credentials:
2829
"AWS_S3_REGION_NAME",
2930
"AWS_STORAGE_BUCKET_NAME",
3031
"AWS_DEFAULT_ACL",
31-
"AWS_PRESIGNED_EXPIRY"
32+
"AWS_PRESIGNED_EXPIRY",
33+
"FILE_MAX_SIZE"
3234
],
3335
"S3 credentials not found."
3436
)
@@ -39,7 +41,8 @@ def s3_get_credentials() -> S3Credentials:
3941
region_name=required_config["AWS_S3_REGION_NAME"],
4042
bucket_name=required_config["AWS_STORAGE_BUCKET_NAME"],
4143
default_acl=required_config["AWS_DEFAULT_ACL"],
42-
presigned_expiry=required_config["AWS_PRESIGNED_EXPIRY"]
44+
presigned_expiry=required_config["AWS_PRESIGNED_EXPIRY"],
45+
max_size=required_config["FILE_MAX_SIZE"]
4346
)
4447

4548

@@ -88,7 +91,11 @@ def s3_generate_presigned_post(*, file_path: str, file_type: str) -> Dict[str, A
8891
},
8992
Conditions=[
9093
{"acl": acl},
91-
{"Content-Type": file_type}
94+
{"Content-Type": file_type},
95+
# As an example, allow file size up to 10 MiB
96+
# More on conditions, here:
97+
# https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
98+
["content-length-range", 1, credentials.max_size]
9299
],
93100
ExpiresIn=expires_in,
94101
)

0 commit comments

Comments
 (0)