Evgenii Legotckoi
Evgenii LegotckoiSept. 11, 2023, 4:47 p.m.

Django - Protected media content

On one of the resources that I am developing, there was a need to add protected access to media content, checking the user's right to access this media content. Simply put, can the user view the photo that nginx serves as static content.

In fact, in the case of nginx and django, everything is much simpler than it seems at first glance.

Let's look at this using the example of uploading and uploading a photo.

Algorithm for working with photography

  • Uploading a photo to a special protected directory
  • An attempt to get a photo via a direct url to a photo or in a website page (for example, an img tag)
  • Checking photo permissions
  • Creating a redirect in nginx to an internal protected directory on the server
  • Receiving a photo

Everything seems simple, but now let's see what is required for this.


Let's set up the site's django configuration file. Usually, to download media content and automatically serve it via nginx, something similar is written in the settings.

MEDIA_ROOT = BASE_DIR.parent / 'media'
MEDIA_URL = '/media/'

However, this time we are using a special protected directory, which I always call protected .

MEDIA_ROOT = BASE_DIR.parent / 'protected'
MEDIA_URL = '/media/'

Thus, we say that in the external space we have "media" , and in the dark halls of the server it is "protected" .

Such code might correspond to the following directory structure.

  • /home/www/django_project_root_folder - this is the main directory where the project, static and honey directories, as well as the virtual environment are located
  • /home/www/django_project_root_folder/protected - protected media directory
  • /home/www/django_project_root_folder/static - directory with static content
  • /home/www/django_project_root_folder/django_project - your django application
  • /home/www/django_project_root_folder/python_venv - virtual environment

I currently like this structure, because if the python version changes, that is, the virtual environment is updated, you can simply initialize the new environment and change a couple of lines in the configuration so that the site application is launched from the new virtual environment. In this case, neither the project repository, nor static files, nor media content will be affected.

Configuration nginx

I will give a small piece of configuration that is directly responsible for setting up nginx access to the protected directory

 server {

     # Other code

     location /protected/ {
        root /home/www/django_project_root_folder/;
        expires 30d;

With this code we simply indicate where the protected directory is located and that it is internal, that is, the user will not simply receive its contents without special permission.

Uploading photos

To work with photographs, I currently use this data model, of course with modifications for a specific project, but the basis is the same

# -*- coding: utf-8 -*-

import os
import uuid

from django.contrib.gis.db import models
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _

from photo.fields import WEBPField
from photo.managers import PhotoManager

def image_folder(instance, filename):
    return 'photos/{}.webp'.format(uuid.uuid4().hex)

class Photo(models.Model):
    class Meta:
        verbose_name = _('Photo')
        verbose_name_plural = _('Photos')

    created_at = models.DateTimeField(verbose_name=_('Created at'), auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name=_('Updated at'), auto_now=True)
    height = models.IntegerField(verbose_name=_('Height'), default=0, blank=True, null=True)
    width = models.IntegerField(verbose_name=_('Width'), default=0, blank=True, null=True)
    image = WEBPField(

    def filename(self):
        return os.path.basename(self.image.name)

@receiver(post_delete, sender=Photo)
def auto_delete_image_on_delete(sender, instance, **kwargs):
    if instance.image:
        if os.path.isfile(instance.image.path):

A special field WEBPField is used here, which I have already talked about. This is a field that converts an image to webp format on the fly. There is also code for automatically deleting photos from the server. But this is not the most important thing.

The most important thing is this function

def image_folder(instance, filename):
    return 'photos/{}.webp'.format(uuid.uuid4().hex)

This function generates a file name and indicates where to save the file on the server relative to the protected directory.

As a result, the photo will be saved in the following path


In this case, the code for the img tag in the template will look like this:

<img src="{{ photo.image.url }}"/>

And the result will look like this

<img src="/media/photos/0aec484a6ff246d7ad5eb1b06c0a698e.webp"/>

## urls.py

I'll start by specifying the url manager to access the content

# -*- coding: utf-8 -*-

from django.urls import path

from photo.views import photo_access

urlpatterns = [
    path('media/photos/<str:path>', photo_access, name='photo'),

As you can see, here is a url that matches the result for the img tag.

Function photo_access

And now the most interesting thing is how exactly access to protected content is carried out

# -*- coding: utf-8 -*-

from django.http import HttpResponse, HttpResponseForbidden

from photo.models import Photo
from utils.shortcuts import get_object_or_none

def photo_access(request, path):
    def create_x_accel_redirect(path):
        response = HttpResponse()
        # Content-type will be detected by nginx
        del response['Content-Type']
        response['X-Accel-Redirect'] = '/protected/photos/' + path
        return response

    photo = get_object_or_none(Photo, image='photos/' + path)
    if photo is None:
        return HttpResponseForbidden('Not authorized to access this media.')

    # Some another check code
    if condition is True:
        return create_x_accel_redirect(path)

    if request.user.is_authenticated and request.user.is_staff:
        return create_x_accel_redirect(path)

    return HttpResponseForbidden('Not authorized to access this media.')

In this function, we check whether the user is an authorized representative of the resource administration and whether the photo object exists. You can also add any other conditions you like.

If they are executed, then using the create_x_accel_redirect function we add a directive that redirects the request inside nginx to the image.

That is, we replace /media/photos/0aec484a6ff246d7ad5eb1b06c0a698e.webp with /protected/photos/0aec484a6ff246d7ad5eb1b06c0a698e.webp and redirect the request with the X-Accel-Redirect directive.
Thus, nginx serves content from a protected directory via media url.

More details about the X-Accel-Redirect directive can be found on the official nginx wiki

We recommend hosting TIMEWEB
We recommend hosting TIMEWEB
Stable hosting, on which the social network EVILEG is located. For projects on Django we recommend VDS hosting.

Do you like it? Share on social networks!


Only authorized users can post comments.
Please, Log in or Sign up

C++ - Test 006. Enumerations

  • Result:10points,
  • Rating points-10
  • KiRi4
  • Sept. 7, 2023, 7:57 a.m.

C++ - Test 002. Constants

  • Result:41points,
  • Rating points-8
  • KiRi4
  • Sept. 7, 2023, 7:49 a.m.

C++ - Test 001. The first program and data types

  • Result:66points,
  • Rating points-1
Last comments
IscanderCheSept. 13, 2023, 9:11 a.m.
QScintilla C++ example По горячим следам (с другого форума вопрос задали, пришлось в памяти освежить всё) решил дополнить. Качаем исходники с https://riverbankcomputing.com/software/qscintilla/downlo…
Evgenii Legotckoi
Evgenii LegotckoiSept. 6, 2023, 7:18 a.m.
Qt/C++ - Lesson 048. QThread — How to work with threads using moveToThread Разве могут взаимодействовать объекты из разных нитей как-то, кроме как через сигнал-слоты?" Могут. Выполняя оператор new , Вы выделяете под объект память в куче (heap), …
Andrei CherniaevSept. 5, 2023, 3:37 a.m.
Qt/C++ - Lesson 048. QThread — How to work with threads using moveToThread Я поясню свой вопрос. Выше я писал "Почему же в методе MainWindow::on_write_1_clicked() Можно обращаться к методам exampleObject_1? Разве могут взаимодействовать объекты из разных…
nvnAug. 31, 2023, 9:47 a.m.
QML - Lesson 004. Signals and Slots in Qt QML Здравствуйте! Прекрасный сайт, отличные статьи. Не хватает только готовых проектов для скачивания. Многих комментариев типа appCore != AppCore просто бы не было )))
NSProjectAug. 24, 2023, 1:40 p.m.
Django - Tutorial 023. Like Dislike system using GenericForeignKey Ваша ошибка связана с gettext from django.utils.translation import gettext_lazy as _ Поле должно выглядеть так vote = models.SmallIntegerField(verbose_name=_("Голос"), choices=VOTES) …
Now discuss on the forum
IscanderCheSept. 17, 2023, 9:24 a.m.
Интернационализация строк в QMessageBox Странная картина... Сделал минимально работающий пример - всё работает. Попробую на другой операционке. Может, дело в этом.
NSProjectSept. 17, 2023, 8:49 a.m.
Помогите добавить Ajax в проект В принципе ничего сложного с отправкой на сервер нет. Всё что ты хочешь отобразить на странице передаётся в шаблон и рендерится. Ты просто создаёшь файл forms.py в нём описываешь свою форму и в …
BlinCTSept. 15, 2023, 12:35 p.m.
Размеры полей в TreeView Всем привет. Пытаюсь сделать дерево вот такого вида Пытаюсь организовать делегат для каждой строки в дереве. ТО есть отступ какого то размера и если при открытии есть под…
IscanderCheSept. 8, 2023, 12:07 p.m.
Кастомная QAbstractListModel и цвет фона, цвет текста и шрифт Похоже надо не абстрактный , а "реальный" типа QSqlTableModel Да, но не совсем. Решилось с помощью стайлшитов и setFont. Спасибо за отлик!
Evgenii Legotckoi
Evgenii LegotckoiSept. 6, 2023, 6:35 a.m.
Вопрос: Нужно ли в деструкторе удалять динамически созданные QT-объекты. Напр: Зависит от того, как эти объекты были созданы. Если вы передаёте указатель на parent объект, то не нужно, Ядро Qt само разрулит удаление, если нет, то нужно удалять вручную, иначе будет ут…

Follow us in social networks