From 608338098c5b4c22272cbf2d37de5cd93e96ff70 Mon Sep 17 00:00:00 2001 From: kovacspe Date: Tue, 21 May 2024 21:07:33 +0200 Subject: [PATCH] Add sendfile --- Pipfile | 1 + competition/models.py | 25 ++++++++++++++++++++----- competition/views.py | 13 +++++++------ downloads/__init__.py | 0 downloads/apps.py | 6 ++++++ downloads/migrations/__init__.py | 0 downloads/models.py | 3 +++ downloads/tests.py | 3 +++ downloads/urls.py | 18 ++++++++++++++++++ downloads/views.py | 29 +++++++++++++++++++++++++++++ requirements.txt | 1 + webstrom/settings.py | 5 +++-- webstrom/urls.py | 1 + 13 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 downloads/__init__.py create mode 100644 downloads/apps.py create mode 100644 downloads/migrations/__init__.py create mode 100644 downloads/models.py create mode 100644 downloads/tests.py create mode 100644 downloads/urls.py create mode 100644 downloads/views.py diff --git a/Pipfile b/Pipfile index 334e3e25..ad8d4a45 100644 --- a/Pipfile +++ b/Pipfile @@ -9,6 +9,7 @@ dj-rest-auth = "~=5.0.1" django = "~=3.2.23" django-allauth = "~=0.58.2" django-filter = "~=23.5" +django-sendfile2 = "~=0.7.1" djangorestframework = "~=3.14.0" drf-writable-nested = "~=0.7.0" pillow = "~=10.3.0" diff --git a/competition/models.py b/competition/models.py index ab49af6c..24f6107a 100644 --- a/competition/models.py +++ b/competition/models.py @@ -24,7 +24,9 @@ from personal.models import Profile, School from user.models import User -private_storage = FileSystemStorage(location=settings.PRIVATE_STORAGE_ROOT) +private_storage = FileSystemStorage(location=settings.PRIVATE_STORAGE_ROOT, + base_url='/protected/' + ) class CompetitionType(models.Model): @@ -620,11 +622,11 @@ class Vote(models.IntegerChoices): def get_solution_path(instance, filename): - return instance.get_solution_path() + return instance.get_solution_file_path() def get_corrected_solution_path(instance, filename): - return instance.get_corrected_solution_path() + return instance.get_corrected_solution_file_path() class Solution(models.Model): @@ -673,18 +675,31 @@ def get_solution_file_name(self): f'-{self.problem.id}-{self.semester_registration.id}.pdf' def get_solution_file_path(self): - return f'solutions/user_solutions/{self.get_solution_file_path()}' + return f'solutions/user_solutions/{self.get_solution_file_name()}' def get_corrected_solution_file_name(self): return f'{self.semester_registration.profile.user.get_full_name_camel_case()}'\ f'-{self.problem.id}-{self.semester_registration.id}_corrected.pdf' def get_corrected_solution_file_path(self): - return f'solutions/corrected/{self.get_corrected_solution_file_path()}' + return f'solutions/corrected/{self.get_corrected_solution_file_name()}' def can_user_modify(self, user): return self.problem.can_user_modify(user) + def can_access(self, user): + return self.semester_registration.profile.user == user or self.can_user_modify(user) + + @classmethod + def get_by_filepath(cls, path): + try: + return cls.objects.get(solution=path) + except cls.DoesNotExist: + try: + return cls.objects.get(corrected_solution=path) + except cls.DoesNotExist: + return None + @classmethod def can_user_create(cls, user: User, data: dict) -> bool: problem = Problem.objects.get(pk=data['problem']) diff --git a/competition/views.py b/competition/views.py index 09bebb94..73736e26 100644 --- a/competition/views.py +++ b/competition/views.py @@ -280,10 +280,11 @@ def upload_solution(self, request, pk=None): problem=problem, semester_registration=event_registration, late_tag=late_tag, - is_online=True + is_online=True, + solution=file ) - solution.solution.save( - solution.get_solution_file_name(), file, save=True) + # solution.solution.save( + # solution.get_solution_file_name(), file, save=True) return Response(status=status.HTTP_201_CREATED) @@ -394,7 +395,7 @@ def upload_solutions_with_points(self, request, pk=None): except (IndexError, ValueError, AssertionError): errors.append({ 'filename': filename, - 'status': 'Nedá sa prečítať názov súboru. Skontroluj, že názov súboru' + 'status': 'Nedá sa prečítať názov súboru. Skontroluj, že názov súboru' 'je v tvare BODY-MENO-ID_ULOHY-ID_REGISTRACIE_USERA.pdf' }) continue @@ -583,7 +584,7 @@ def upload_solution_file(self, request, pk=None): raise exceptions.ParseError( detail='Riešenie nie je vo formáte pdf') solution.solution.save( - solution.get_solution_file_name(), file, save=True + solution.get_solution_file_path(), file, save=True ) return Response(status=status.HTTP_201_CREATED) @@ -600,7 +601,7 @@ def upload_corrected_solution_file(self, request, pk=None): raise exceptions.ParseError( detail='Riešenie nie je vo formáte pdf') solution.corrected_solution.save( - solution.get_corrected_solution_file_name(), file, save=True + solution.get_corrected_solution_file_path(), file, save=True ) return Response(status=status.HTTP_201_CREATED) diff --git a/downloads/__init__.py b/downloads/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/downloads/apps.py b/downloads/apps.py new file mode 100644 index 00000000..62b306e9 --- /dev/null +++ b/downloads/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DownloadsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'downloads' diff --git a/downloads/migrations/__init__.py b/downloads/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/downloads/models.py b/downloads/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/downloads/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/downloads/tests.py b/downloads/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/downloads/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/downloads/urls.py b/downloads/urls.py new file mode 100644 index 00000000..d871f9c7 --- /dev/null +++ b/downloads/urls.py @@ -0,0 +1,18 @@ +from django.conf.urls import url +from django.utils.translation import ugettext_lazy as _ + +from competition.models import Solution + +from .views import download_protected_file + +urlpatterns = [ + # Include non-translated versions only since Admin ignores lang prefix + url(r'solutions/(?P.*)$', download_protected_file, + {'path_prefix': 'solutions/', 'model_class': Solution}, + name='download_solution'), + url(r'corrected_solutions/(?P.*)$', download_protected_file, + {'path_prefix': 'corrected_solutions/', + 'model_class': Solution}, + name='download_corrected_solution'), + +] diff --git a/downloads/views.py b/downloads/views.py new file mode 100644 index 00000000..1986e1b6 --- /dev/null +++ b/downloads/views.py @@ -0,0 +1,29 @@ +import os + +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.core.exceptions import PermissionDenied +from django_sendfile import sendfile +from rest_framework.request import Request + + +@login_required +def download_protected_file(request: Request, model_class, path_prefix, path): + """ + This view allows download of the file at the specified path, if the user + is allowed to. This is checked by calling the model's can_access_files + method. + """ + filepath = os.path.join(settings.PRIVATE_STORAGE_ROOT, path_prefix, path) + filepath_mediapath = path_prefix + path + + if request.user.is_authenticated: + # Superusers can access all files + if request.user.is_superuser: + return sendfile(request, filepath) + obj = model_class.get_by_filepath(filepath_mediapath) + + if obj is not None and obj.can_access(request.user): + return sendfile(request, filepath) + + raise PermissionDenied diff --git a/requirements.txt b/requirements.txt index 91c5904b..c4036f94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ Django==3.2.25 django-allauth==0.58.2 django-filter==23.5 django-rest-swagger==2.2.0 +django-sendfile2==0.7.1 django-typomatic==2.5.0 djangorestframework==3.14.0 drf-writable-nested==0.7.0 diff --git a/webstrom/settings.py b/webstrom/settings.py index 62684328..894772ef 100644 --- a/webstrom/settings.py +++ b/webstrom/settings.py @@ -178,8 +178,9 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_URL = '/media/' -PRIVATE_STORAGE_ROOT = os.path.join(BASE_DIR, 'protected_media') - +PRIVATE_STORAGE_ROOT = os.path.join(BASE_DIR, 'protected_media/') +SENDFILE_ROOT = PRIVATE_STORAGE_ROOT +SENDFILE_BACKEND = "django_sendfile.backends.simple" # Email backend EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend' diff --git a/webstrom/urls.py b/webstrom/urls.py index ed425585..177c6a56 100644 --- a/webstrom/urls.py +++ b/webstrom/urls.py @@ -10,6 +10,7 @@ path('cms/', include('cms.urls')), path('personal/', include('personal.urls')), path('base/', include('base.urls')), + path('protected/', include('downloads.urls')), # Dočasná cesta pre allauth bez rest frameworku path('accounts/', include('allauth.urls')), ]