How to serve private media files with Django
Tags: django
Published: 2020-03-13 17:35:56

In this tutorial, we will create a barebones document manager with these features:

* Logged in users can upload files.
* Superusers can view all files.
* Regular users can view only the files they uploaded.

These steps were performed on Ubuntu 18.04 LTS.

## Getting Started

First lets add the universe repository and install python3-venv.

```
sudo add-apt-repository universe
sudo apt install -y python3-venv
```

## The Project and App

Now create the project.
```
mkdir ~/.virtualenvs
python3 -m venv ~/.virtualenvs/docman
source ~/.virtualenvs/docman/bin/activate
pip install django
django-admin startproject docman
cd docman
```

Now create the database and tables by running migrate.

```
./manage.py migrate
```

After the migration process completes create an app 

```
./manage.py startapp core
```

## The model 

Each document is associated with the user.
We also track the date and time that the document was uploaded.
By default documents will be sorted in reverse chronological order.

```
# core/models.py

from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.template.defaultfilters import slugify


class Document(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField()
    file = models.FileField()
    created_by = models.ForeignKey(User, on_delete=models.PROTECT)
    created_at = models.DateTimeField(default=timezone.now)
    slug = models.SlugField(max_length=255, editable=False)

    class Meta:
        ordering = ['-created_at']
    
    def save(self, *args, **kwargs):
        if not self.id:
            self.slug = slugify(self.title)

        return super(Document, self).save(*args, **kwargs)
```

Now add the app to the INSTALLED_APPS list in `settings.py`

```
# docman/settings.py

INSTALLED_APPS = [
...
    'core.apps.CoreConfig',
]
```

Generate migrations for the model.

```
./manage makemigrations core
```

Then update the database to add the table for the model.
```
./manage migrate
```


## The views

The application functionality consists of 3 views:

* The default view displays a list of documents.
* A view to upload a document.
* A view to download a document.

All views are restricted to logged-in users.

```
# core/views.py

import os

from django.shortcuts import render, get_object_or_404
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView
from django.views.generic.edit import CreateView
from django.http import FileResponse, HttpResponseForbidden, HttpResponse
from django.views import View
from django.urls import reverse
from django.conf import settings

from .models import Document


class DocumentList(LoginRequiredMixin, ListView):
    model = Document
    
    def get_queryset(self):
        queryset = Document.objects.all()
        user = self.request.user

        if not user.is_superuser:
            queryset = queryset.filter(
                created_by=user
            )

        return queryset


class DocumentCreate(LoginRequiredMixin, CreateView):
    model = Document
    fields = ['title', 'description', 'file']

    def get_success_url(self):
        return reverse('home')

    def form_valid(self, form):
        form.instance.created_by = self.request.user
        return super().form_valid(form)


class DocumentDownload(View):
    def get(self, request, relative_path):
        document = get_object_or_404(Document, file=relative_path)
        if not request.user.is_superuser and document.created_by != request.user:
            return HttpResponseForbidden()
        absolute_path = '{}/{}'.format(settings.MEDIA_ROOT, relative_path)
        response = FileResponse(open(absolute_path, 'rb'), as_attachment=True)
        return response
```

## The URLs

Lets wire up the urls for the views.

```
# docman/urls.py

from django.contrib import admin
from django.urls import path
from django.contrib.auth import views as auth_views

from core.views import DocumentList, DocumentCreate, DocumentDownload

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', DocumentList.as_view(), name='home'),
    path('document-add/', DocumentCreate.as_view(), name='document-add'),
    path('media/<path:relative_path>', DocumentDownload.as_view(), name='document-download'),
    
    path('accounts/login/', auth_views.LoginView.as_view()),
    path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'),
]
```

## The templates

Create a login template.
```
# core/templates/registration/login.html

<form method="POST">
    {% csrf_token %}
    {{form.as_p}}
    <button type="submit">Login</button>
</form>
```

Create a base template.
```
# core/templates/base.html

<h1>Document Manager</h1>
<p>Logged in as {{user.get_full_name}}</p>
<p><a href="{% url "logout" %}">Logout</a></p>
{% block content %}
{% endblock %}
```

The document list displays documents in reverse chronological order.
```
# core/templates/core/document_list.html

{% extends "base.html" %}

{% block content %}
    <h2>Documents</h2>
    <a href="{% url "document-add" %}">Add Document</a>
    <table width="50%">
        <thead>
            <tr>
                <th>Title</th>
                <th>Description</th>
                <th>Created by</th>
                <th>Created at</th>
                <th>File</th>
            </tr>
        </thead>
        {% for document in object_list %}
            <tr>
            <td>{{ document.title }}</td>
            <td>{{document.description}}</td>
            <td>{{document.created_by}}</td>
            <td>{{document.created_at}}</td>
            <td><a href="{{document.file.url}}">{{document.file.name}}</a></td>
            </tr>
        {% endfor %}
    </table>
{% endblock %}
```
Files are downloaded by clicking on the filename link.
The `Add Document` button links to the document upload page.

```
# core/templates/core/document_form.html

{% extends 'base.html' %}
{% block content %}
<h1>Add Document</h1>
<form method="POST" enctype="multipart/form-data">
    {% csrf_token %}
    {{form.as_p}}
    <button type="submit">Submit</button>
</form>
{% endblock %}
```

Django stores uploaded files on the local file system using paths relative to `MEDIA_ROOT`
Lets define media root in `settings.py`

```
# docman/settings.py

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
```

Also add a line at the bottom of `settings.py` that sets the URL to redirect to when a user logs out
```
# docman/settings.py

LOGOUT_REDIRECT_URL = '/'

```

## The users
We will create two users:

* A superuser who can view all uploaded documents.
* A regular user who can view only the documents they uploaded.

We will use the  [django-createuser](http://) app to add a convenient management command for creating users.

Install django-createuser
```
pip install django-createuser
```

Add django_createuser to your installed apps in `settings.py`

```
# docman/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'core.apps.CoreConfig',
    'django_createuser',  #new
]
```

Now create the superuser.
```
python manage.py createuser --email admin@example.com --first_name Administrator --is-superuser --password test1234 admin
```

Then the regular user.
```
python manage.py createuser --email user@example.com --first_name Test --last_name User --password test1234 user
```

## Run the application

We will use the Django the dev server.
```
python manage.py runserver
```

Navigate to http://127.0.0.1:8000 in your browser.

You will get prompted to login using username and password.

Login as the superuser using the username _admin_ and password _test1234_.
You should see the homepage.

![docman_home.PNG](https://l2earn.nyc3.digitaloceanspaces.com/l2earn/images/uploads/2020/03/11/4544a6905b-docman_home.PNG) 

Click the _Add Document_ link and upload a document
![docman_upload.PNG](https://l2earn.nyc3.digitaloceanspaces.com/l2earn/images/uploads/2020/03/11/0ba89fa89c-docman_upload.PNG) 
Then:
Logout from the superuser account.
Login as the regular user using the username _user_ and password _test1234_.
The document uploaded by the superuser will not be visible in the list.
Upload a document as the regular user.

If you login as the superuser both documents are displayed in the list.
You should be able to download both documents as the superuser by clicking on the links.

## Testing access control

Copy the both document links and paste them in a text editor

#### Test access for logged out users
Logout and try downloading the files using the links you copied.
Access will be denied
![docman_accessdenied.PNG](https://l2earn.nyc3.digitaloceanspaces.com/l2earn/images/uploads/2020/03/11/a4b7a0d2e8-docman_accessdenied.PNG) 

#### Test access to superuser file for regular user.
Login as the regular user then try downloading the superuser file using the link you copied.
Access will be denied


## Next Steps

The setup described in this article is not recommended for production.
Serving media files is best handled by a web server like NGINX or a CDN backed Object Storage service like AWS S3.

A future article will evolve our document manager to use NGINX and Object Storage.