diff --git a/.gitignore b/.gitignore index af52db56d..022d2c435 100644 --- a/.gitignore +++ b/.gitignore @@ -131,6 +131,7 @@ dmypy.json # Media and Static Django /media /staticfiles +/protected_media # Logs Django /logs diff --git a/apps/core/services/file_access.py b/apps/core/services/file_access.py new file mode 100644 index 000000000..26ead9a09 --- /dev/null +++ b/apps/core/services/file_access.py @@ -0,0 +1,17 @@ +"""Сервис контроля доступа к файлам для приложени django-private-storage.""" + +from pathlib import Path + +from django.conf import settings +from private_storage.models import PrivateFile + +from apps.library.models.play import Play + + +def has_download_permission(private_file: PrivateFile) -> bool: + user = private_file.request.user + if user.is_authenticated and user.is_staff: + return True + + relative_path = Path(private_file.full_path).relative_to(settings.PRIVATE_STORAGE_ROOT) + return Play.objects.filter(url_download=relative_path, published=True).exists() diff --git a/apps/library/migrations/0044_alter_play_url_download.py b/apps/library/migrations/0044_alter_play_url_download.py new file mode 100644 index 000000000..9b45b69e7 --- /dev/null +++ b/apps/library/migrations/0044_alter_play_url_download.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.23 on 2024-01-26 13:30 + +import apps.content_pages.utilities +import django.core.validators +from django.conf import settings +from django.db import migrations +import private_storage.fields +import private_storage.storage.files +from shutil import copytree, move + +REGULAR_MEDIA = settings.MEDIA_ROOT / "plays" +PROTECTED_MEDIA = settings.PRIVATE_STORAGE_ROOT / "plays" + + +def move_to_protected_media(apps, schema_editor): + if REGULAR_MEDIA.exists(): + copytree(REGULAR_MEDIA, PROTECTED_MEDIA, copy_function=move, dirs_exist_ok=True) + + +def move_from_protected_media(apps, schema_editor): + if PROTECTED_MEDIA.exists(): + copytree(PROTECTED_MEDIA, REGULAR_MEDIA, copy_function=move, dirs_exist_ok=True) + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0043_alter_socialnetworklink_name'), + ] + + operations = [ + migrations.AlterField( + model_name='play', + name='url_download', + field=private_storage.fields.PrivateFileField(blank=True, help_text="Файл пьесы должен быть в одном из следующих форматов: ('doc', 'docx', 'txt', 'odt', 'pdf')", max_length=200, null=True, storage=private_storage.storage.files.PrivateFileSystemStorage(), upload_to=apps.content_pages.utilities.path_by_media_and_class_name, validators=[django.core.validators.FileExtensionValidator(('doc', 'docx', 'txt', 'odt', 'pdf'))], verbose_name='Текст пьесы'), + ), + migrations.RunPython(move_to_protected_media, move_from_protected_media), + ] diff --git a/apps/library/models/play.py b/apps/library/models/play.py index b1a049430..71b2138c1 100644 --- a/apps/library/models/play.py +++ b/apps/library/models/play.py @@ -3,6 +3,7 @@ from django.db import models from django.db.models import UniqueConstraint from django.utils.translation import gettext_lazy as _ +from private_storage.fields import PrivateFileField from apps.content_pages.utilities import path_by_media_and_class_name from apps.core.mixins import FileCleanUpMixin @@ -65,7 +66,7 @@ class Play(FileCleanUpMixin, BaseModel): blank=True, null=True, ) - url_download = models.FileField( + url_download = PrivateFileField( validators=(FileExtensionValidator(ALLOWED_FORMATS_FILE_FOR_PLAY),), max_length=200, blank=True, diff --git a/config/settings/base.py b/config/settings/base.py index a0c477464..7adeb0c6f 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -57,6 +57,7 @@ "anymail", "easy_thumbnails", "apps.filer.apps.FilerCustomConfig", + "private_storage", ] LOCAL_APPS = [ "apps.users", @@ -385,6 +386,12 @@ LOCALE_PATHS = [Path(STATIC_ROOT) / "core" / "locale", ] +# Private storage settings +PRIVATE_STORAGE_ROOT = ROOT_DIR / "protected_media" +PRIVATE_STORAGE_AUTH_FUNCTION = "apps.core.services.file_access.has_download_permission" +PRIVATE_STORAGE_SERVER = "nginx" +PRIVATE_STORAGE_INTERNAL_URL = "/private-redirect/" + # APP SETTINGS AFISHA_REGISTRATION_OPENS_HOURS_BEFORE = 12 POSTFIX_MAIL_DOMAIN = os.environ.get("POSTFIX_MAIL_DOMAIN") diff --git a/config/urls.py b/config/urls.py index 9026d0219..a4c6fa5f8 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,3 +1,4 @@ +import private_storage.urls from django.conf import settings from django.conf.urls.static import static from django.contrib import admin @@ -79,3 +80,7 @@ view=include("filer.urls"), ), ] + static(MEDIA_URL, document_root=MEDIA_ROOT) + +urlpatterns += [ + path("private-media/", include(private_storage.urls)), +] diff --git a/infra_deploy/prod/lubimovka_backend_prod_deploy.yml b/infra_deploy/prod/lubimovka_backend_prod_deploy.yml index 1ec24d81b..1956b6a9d 100644 --- a/infra_deploy/prod/lubimovka_backend_prod_deploy.yml +++ b/infra_deploy/prod/lubimovka_backend_prod_deploy.yml @@ -18,6 +18,7 @@ services: volumes: - static_value_prod:/code/staticfiles/ - ./media:/code/media/ + - ./protected_media:/code/protected_media/ - ./logs/backend_logs/:/code/logs/ depends_on: - postgres @@ -32,6 +33,7 @@ services: volumes: - static_value_prod:/code/staticfiles/ - ./media:/code/media/ + - ./protected_media:/code/protected_media/ - ./logs/backend_logs/:/code/logs/ command: > sh -c " diff --git a/infra_deploy/prod/lubimovka_frontend_prod_deploy.yml b/infra_deploy/prod/lubimovka_frontend_prod_deploy.yml index 6680f04ec..7a19eb4a4 100644 --- a/infra_deploy/prod/lubimovka_frontend_prod_deploy.yml +++ b/infra_deploy/prod/lubimovka_frontend_prod_deploy.yml @@ -21,6 +21,7 @@ services: - ./logs/swag_logs/:/config/log/ - static_value_prod:/config/prod/static/ - ./media:/config/prod/media/ + - ./protected_media:/config/prod/protected_media/ ports: - 443:443 - 80:80 diff --git a/infra_deploy/prod/swag/swag_nginx_prod.conf b/infra_deploy/prod/swag/swag_nginx_prod.conf index 8a1c2f9e1..e9057ce02 100644 --- a/infra_deploy/prod/swag/swag_nginx_prod.conf +++ b/infra_deploy/prod/swag/swag_nginx_prod.conf @@ -34,7 +34,7 @@ server { # proxy_http_version 1.1; # proxy_set_header Connection ""; } - location ~^/(api|admin|files) { + location ~^/(api|admin|files|private-media) { include /config/nginx/proxy.conf; set $upstream_app backend_prod; set $upstream_port 8000; @@ -53,6 +53,11 @@ server { add_header Content-disposition "attachment; filename=$1"; } + location /private-redirect/ { + internal; + alias /config/prod/protected_media/; + } + location ~^/(media|static) { set $width $arg_w; if ($arg_w = ''){ diff --git a/infra_deploy/stage/lubimovka_backend_stage_deploy.yml b/infra_deploy/stage/lubimovka_backend_stage_deploy.yml index 41292d097..7745c1342 100644 --- a/infra_deploy/stage/lubimovka_backend_stage_deploy.yml +++ b/infra_deploy/stage/lubimovka_backend_stage_deploy.yml @@ -20,6 +20,7 @@ services: volumes: - static_value_stage:/code/staticfiles/ - ./media:/code/media/ + - ./protected_media:/code/protected_media/ - ./logs/backend_logs/:/code/logs/ depends_on: - postgres @@ -34,6 +35,7 @@ services: volumes: - static_value_stage:/code/staticfiles/ - ./media:/code/media/ + - ./protected_media:/code/protected_media/ - ./logs/backend_logs/:/code/logs/ command: > sh -c " diff --git a/infra_deploy/stage/lubimovka_frontend_stage_deploy.yml b/infra_deploy/stage/lubimovka_frontend_stage_deploy.yml index 44bf4534b..b3edbe95f 100644 --- a/infra_deploy/stage/lubimovka_frontend_stage_deploy.yml +++ b/infra_deploy/stage/lubimovka_frontend_stage_deploy.yml @@ -21,6 +21,7 @@ services: - ./logs/swag_logs/:/config/log/ - static_value_stage:/config/stage/static/ - ./media:/config/stage/media/ + - ./protected_media:/config/stage/protected_media/ ports: - 443:443 - 80:80 diff --git a/infra_deploy/stage/swag/swag_nginx_stage.conf b/infra_deploy/stage/swag/swag_nginx_stage.conf index 1b9f1b7c7..762c3da42 100644 --- a/infra_deploy/stage/swag/swag_nginx_stage.conf +++ b/infra_deploy/stage/swag/swag_nginx_stage.conf @@ -36,7 +36,7 @@ server { # proxy_http_version 1.1; # proxy_set_header Connection ""; } - location ~^/(api|admin|files) { + location ~^/(api|admin|files|private-media) { include /config/nginx/proxy.conf; set $upstream_app backend; set $upstream_port 8000; @@ -55,6 +55,12 @@ server { add_header Content-disposition "attachment; filename=$1"; } + + location /private-redirect/ { + internal; + alias /config/stage/protected_media/; + } + location ~^/(media|static) { set $width $arg_w; if ($arg_w = ''){ diff --git a/infra_deploy/test/lubimovka_backend_test_deploy.yml b/infra_deploy/test/lubimovka_backend_test_deploy.yml index 3cf8f0fa3..3009d80aa 100644 --- a/infra_deploy/test/lubimovka_backend_test_deploy.yml +++ b/infra_deploy/test/lubimovka_backend_test_deploy.yml @@ -20,6 +20,7 @@ services: volumes: - static_value_test:/code/staticfiles/ - ./media:/code/media/ + - ./protected_media:/code/protected_media/ - ./logs/backend_logs/:/code/logs/ depends_on: - postgres @@ -34,6 +35,7 @@ services: volumes: - static_value_test:/code/staticfiles/ - ./media:/code/media/ + - ./protected_media:/code/protected_media/ - ./logs/backend_logs/:/code/logs/ command: > sh -c "sleep 5; python manage.py migrate && diff --git a/infra_deploy/test/lubimovka_frontend_test_deploy.yml b/infra_deploy/test/lubimovka_frontend_test_deploy.yml index 40db2b042..69acde38b 100644 --- a/infra_deploy/test/lubimovka_frontend_test_deploy.yml +++ b/infra_deploy/test/lubimovka_frontend_test_deploy.yml @@ -21,6 +21,7 @@ services: - ./logs/nginx_logs/:/var/log/nginx/ - static_value_test:/config/test/static/ - ./media:/config/test/media/ + - ./protected_media:/config/test/protected_media/ ports: - 8080:80 restart: unless-stopped diff --git a/infra_deploy/test/nginx/nginx_test.conf b/infra_deploy/test/nginx/nginx_test.conf index d8df4e262..019b4e063 100644 --- a/infra_deploy/test/nginx/nginx_test.conf +++ b/infra_deploy/test/nginx/nginx_test.conf @@ -25,7 +25,7 @@ server { # proxy_http_version 1.1; # proxy_set_header Connection ""; } - location ~^/(api|admin|files|__debug__) { + location ~^/(api|admin|files|private-media|__debug__) { #include proxy_params; proxy_set_header Host $proxy_host; resolver 127.0.0.11 valid=20s; @@ -50,6 +50,11 @@ server { root /config/test/; } + location /private-redirect/ { + internal; + alias /config/test/protected_media/; + } + } diff --git a/poetry.lock b/poetry.lock index 63d20cd3a..871733631 100644 --- a/poetry.lock +++ b/poetry.lock @@ -710,6 +710,17 @@ files = [ [package.dependencies] Django = ">=2.1" +[[package]] +name = "django-private-storage" +version = "3.1.1" +description = "Private media file storage for Django projects" +optional = false +python-versions = "*" +files = [ + {file = "django-private-storage-3.1.1.tar.gz", hash = "sha256:9e8f9e88818b04895cf4be6c86f1255c4befa5466f7d25e1be3a7c2784e9b275"}, + {file = "django_private_storage-3.1.1-py3-none-any.whl", hash = "sha256:fcb14c0f56e1fb5e1984afe689e6a92d79c1347dd5329aa50b53ecec4e517a52"}, +] + [[package]] name = "django-rest-multiple-models" version = "2.1.3" @@ -2760,4 +2771,4 @@ requests = "*" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "f5bdd2deb4502a2508316b61b9af19f70c4470f49c860f6b95ce74cfd53500da" +content-hash = "2710da768fda5e47d5699b505ac8fd230e3c3c67f08dffc0c8ef2a723b0af41c" diff --git a/pyproject.toml b/pyproject.toml index 0403d240e..4df9fd612 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ sqlparse = "^0.5.0" easy-thumbnails = "^2.8.5" idna = "^3.7" black = "^24.4.0" +django-private-storage = "^3.1.1" [tool.poetry.dev-dependencies] pre-commit = "^2.15.0" diff --git a/requirements/dev.txt b/requirements/dev.txt index 426d56401..51fc7ed8e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -28,6 +28,7 @@ django-filter==21.1 ; python_version >= "3.9" and python_version < "4.0" django-js-asset==2.1.0 ; python_version >= "3.9" and python_version < "4.0" django-phonenumber-field[phonenumbers]==5.2.0 ; python_version >= "3.9" and python_version < "4.0" django-polymorphic==3.1.0 ; python_version >= "3.9" and python_version < "4.0" +django-private-storage==3.1.1 ; python_version >= "3.9" and python_version < "4.0" django-rest-multiple-models==2.1.3 ; python_version >= "3.9" and python_version < "4.0" django==3.2.25 ; python_version >= "3.9" and python_version < "4.0" djangorestframework==3.13.1 ; python_version >= "3.9" and python_version < "4.0" diff --git a/requirements/prod.txt b/requirements/prod.txt index 0b977632d..bb159340a 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -22,6 +22,7 @@ django-filter==21.1 ; python_version >= "3.9" and python_version < "4.0" django-js-asset==2.1.0 ; python_version >= "3.9" and python_version < "4.0" django-phonenumber-field[phonenumbers]==5.2.0 ; python_version >= "3.9" and python_version < "4.0" django-polymorphic==3.1.0 ; python_version >= "3.9" and python_version < "4.0" +django-private-storage==3.1.1 ; python_version >= "3.9" and python_version < "4.0" django-rest-multiple-models==2.1.3 ; python_version >= "3.9" and python_version < "4.0" django==3.2.25 ; python_version >= "3.9" and python_version < "4.0" djangorestframework==3.13.1 ; python_version >= "3.9" and python_version < "4.0"