diff --git a/lectures/models.py b/lectures/models.py
index 02314de4689025f2756db8abf900e15b25d2df29..66a2d65919e28f9b0dc2c0e01894b766b18c1813 100644
--- a/lectures/models.py
+++ b/lectures/models.py
@@ -1,9 +1,15 @@
-from datetime import timedelta
+import mimetypes
+import uuid
+
+from datetime import datetime, timedelta
 
 from django.contrib.auth.models import Group
 from django.core.exceptions import ValidationError
 from django.db import models
+from django.db.models.fields.files import FieldFile
+from django.urls import reverse
 from django.utils import timezone
+from django.utils.safestring import mark_safe
 from markdownx.models import MarkdownxField
 
 from shared.models import NameStrMixin
@@ -35,7 +41,7 @@ class LectureGroup(NameStrMixin, models.Model):
         Group,
         blank=True,
         verbose_name="Uživatelské skupiny",
-        help_text="Pokud žádné nedefinuješ, školení ve skupině jsou dostupné všem.",
+        help_text="Pokud žádné nedefinuješ, školení ve skupině jsou dostupná všem.",
     )
 
     class Meta:
@@ -112,6 +118,7 @@ class LectureLector(NameStrMixin, models.Model):
         blank=True,
         null=True,
         verbose_name="Odkaz",
+        help_text=mark_safe("Běžně na <a href=\"https://lide.pirati.cz\">aplikaci Lidé</a>.")
     )
 
     username = models.CharField(
@@ -148,6 +155,7 @@ class LectureRecording(NameStrMixin, models.Model):
         blank=True,
         null=True,
         verbose_name="Odkaz",
+        help_text=mark_safe("Běžně na <a href=\"https://tv.pirati.cz\">Pirátskou TV</a>.")
     )
 
     class Meta:
@@ -155,6 +163,46 @@ class LectureRecording(NameStrMixin, models.Model):
         verbose_name_plural = "Nahrávky"
 
 
+def get_lecture_material_file_location(instance, filename):
+    mimetypes_instance = mimetypes.MimeTypes()
+
+    current_time = datetime.today()
+    guessed_type = mimetypes_instance.guess_type(filename, strict=False)[0]
+
+    extension = ""
+
+    if guessed_type is not None:
+        for mapper in mimetypes_instance.types_map_inv:
+            if guessed_type not in mapper:
+                continue
+
+            extension = mapper[guessed_type]
+
+            if isinstance(extension, list):
+                extension = extension[0]
+
+            break
+
+    return (
+        "_private/"
+        f"{current_time.year}/{current_time.month}/{current_time.day}/"
+        f"{uuid.uuid4()}{extension}"
+    )
+
+
+class LectureMaterialFileProxy(FieldFile):
+    @property
+    def url(self) -> str:
+        return reverse(
+            "lectures:download_material_file",
+            args=(str(self.instance.id),)
+        )
+
+
+class LectureMaterialFileField(models.FileField):
+    attr_class = LectureMaterialFileProxy
+
+
 class LectureMaterial(NameStrMixin, models.Model):
     lecture = models.ForeignKey(
         "Lecture",
@@ -176,13 +224,21 @@ class LectureMaterial(NameStrMixin, models.Model):
         help_text="Pokud máš zadaný odkaz, nemůžeš definovat soubor.",
     )
 
-    file = models.FileField(
+    file = LectureMaterialFileField(
         blank=True,
         null=True,
+        upload_to=get_lecture_material_file_location,
         verbose_name="Soubor",
         help_text="Pokud máš vložený soubor, nemůžeš definovat odkaz.",
     )
 
+    @property
+    def protected_file_url(self) -> str:
+        return reverse(
+            "lectures:download_material_file",
+            args=(self.id,),
+        )
+
     def clean(self) -> None:
         BOTH_FILE_AND_LINK_DEFINED = (
             "Definuj prosím pouze odkaz, nebo soubor. Nemůžeš mít oboje najednou."
diff --git a/lectures/urls.py b/lectures/urls.py
index 27da58fcf3087d4b144f1ff2e0a54baff5a9bc04..5b082c4b1f4c12faa943f4d4f04830777548d81f 100644
--- a/lectures/urls.py
+++ b/lectures/urls.py
@@ -9,4 +9,9 @@ urlpatterns = [
         "groups/<int:group_id>", views.view_group_lectures, name="view_group_lectures"
     ),
     path("lectures/<int:lecture_id>", views.view_lecture, name="view_lecture"),
+    path(
+        "lectures/materials/<str:pk>/file",
+        views.LectureMaterialFileDownloadView.as_view(),
+        name="download_material_file",
+    )
 ]
diff --git a/lectures/views.py b/lectures/views.py
index 673ccba0a0513dc4a6ef40e275150b7cac4dadca..722b735c37059ec003305d47f16ebe8718c75b91 100644
--- a/lectures/views.py
+++ b/lectures/views.py
@@ -1,4 +1,5 @@
 # import calendar
+# import locale
 import json
 from datetime import datetime
 from itertools import chain
@@ -8,12 +9,50 @@ from django.db import models
 from django.shortcuts import get_object_or_404, render
 from django.urls import reverse
 from django.utils import timezone
+from django_downloadview import ObjectDownloadView
 from django_http_exceptions import HTTPExceptions
 from guardian.shortcuts import get_objects_for_user
 
-from .models import Lecture, LectureGroup
+from .models import Lecture, LectureGroup, LectureMaterial
+
+
+class LectureMaterialFileDownloadView(ObjectDownloadView):
+    model = LectureMaterial
+    file_field = "file"
+    attachment = False
+
+    def get_queryset(self, *args, **kwargs):
+        queryset = (
+            super()
+            .get_queryset(*args, **kwargs)
+            .filter(
+                lecture__groups__in=(
+                    get_objects_for_user(
+                        self.current_user,
+                        "lectures.view_lecturegroup"
+                    ).
+                    filter(
+                        models.Q(user_groups__in=self.current_user.groups.all())
+                        | models.Q(user_groups=None)
+                    )
+                )
+            )
+        )
 
-# import locale
+        print(
+            queryset,
+            get_objects_for_user(
+                self.current_user,
+                "lectures.view_lecturegroup"
+            )
+        )
+
+        return queryset
+
+    def get(self, request, *args, **kwargs):
+        self.current_user = request.user
+
+        return super().get(request, *args, **kwargs)
 
 
 def get_base_context(request) -> dict:
diff --git a/media_server/__init__.py b/media_server/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/media_server/admin.py b/media_server/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e
--- /dev/null
+++ b/media_server/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/media_server/apps.py b/media_server/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..4aff1ea9e92371e7e70bb043a67dd347b60b3dce
--- /dev/null
+++ b/media_server/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class MediaServerConfig(AppConfig):
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "media_server"
diff --git a/media_server/models.py b/media_server/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..71a836239075aa6e6e4ecb700e9c42c95c022d91
--- /dev/null
+++ b/media_server/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/media_server/tests.py b/media_server/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6
--- /dev/null
+++ b/media_server/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/media_server/urls.py b/media_server/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..23bb64ae39d8b5b07f98b14e44c3cff7b6532382
--- /dev/null
+++ b/media_server/urls.py
@@ -0,0 +1,12 @@
+from django.urls import path
+
+from . import views
+
+app_name = "media_server"
+urlpatterns = [
+    path(
+        "<path:path>",
+        views.view_media,
+        name="view_media",
+    ),
+]
diff --git a/media_server/views.py b/media_server/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..370803dd04a396d2ee3c1946d80d853a84796d9e
--- /dev/null
+++ b/media_server/views.py
@@ -0,0 +1,27 @@
+import os
+
+from django.core.files.storage import FileSystemStorage
+from django_downloadview import StorageDownloadView
+from django_http_exceptions import HTTPExceptions
+
+# Create your views here.
+
+storage = FileSystemStorage()
+
+
+class MediaView(StorageDownloadView):
+    attachment = False
+
+    def get_path(self, *args, **kwargs) -> str:
+        path = super().get_path(*args, **kwargs)
+
+        # Make sure path is clean
+        path = os.path.normpath(path)
+
+        if path.startswith("_"):  # Private path
+            raise HTTPExceptions.NOT_FOUND
+
+        return path
+
+
+view_media = MediaView.as_view(storage=storage)
diff --git a/requirements/base.txt b/requirements/base.txt
index 8fb7229b52aa0c083f8da7352ec6e68e425c18d6..97e5804765d5d634eba2d20228d011108b56ebd1 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -3,6 +3,7 @@ django-admin-index==2.0.2
 django-admin-interface==0.24.2
 django-database-url==1.0.3
 django-dbsettings==1.3.0
+django-downloadview==2.3.0
 django-markdownx==4.0.0b1
 django-ordered-model==3.7.1
 psycopg2-binary==2.9.5
diff --git a/ucebnice/settings/base.py b/ucebnice/settings/base.py
index 0e22973ec2664874dc53e992bb7ed417f7f2dd4f..adf529f7a87b27f1f353290151b9a4ae9da91151 100644
--- a/ucebnice/settings/base.py
+++ b/ucebnice/settings/base.py
@@ -57,6 +57,7 @@ INSTALLED_APPS = [
     "pirates",
     "webpack_loader",
     "shared",
+    "media_server",
     "oidc",
     "users",
     "lectures",
diff --git a/ucebnice/urls.py b/ucebnice/urls.py
index 4f40d785e77ebd4f1deb0c8724a729a583da5ca3..8ed67ab7b598a2ab8dce8e9b275226f6183700ff 100644
--- a/ucebnice/urls.py
+++ b/ucebnice/urls.py
@@ -27,5 +27,6 @@ urlpatterns = [
     path("", include("lectures.urls")),
     path("markdownx/", include("markdownx.urls")),
     path("settings/", include("dbsettings.urls")),
+    path("media/", include("media_server.urls")),
     path("admin/", admin.site.urls),
 ] + pirates_urlpatterns