How to serve private media files with Django
Joshua Masiko
Posted on March 13, 2020
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 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.
Click the Add Document link and upload a document
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
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.
Posted on March 13, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.