Quickstart

Installation

  1. Install from PyPI with pip:

pip install django-privates
  1. Add the app to your INSTALLED_APPS for admin integration and system checks:

INSTALLED_APPS = [
    ...

    "privates",

    ...
]

Settings

We use the STORAGES setting introduced since Django 4.2, and expect a storage to be configured with the "privates" key. Additionally, the SENDFILE_ settings need to be defined. For example:

 1STORAGES = {
 2    # default and staticfiles are the Django defaults
 3    "default": {
 4        "BACKEND": "django.core.files.storage.FileSystemStorage",
 5    },
 6    "staticfiles": {
 7        "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
 8    },
 9    # add privates
10    "privates": {
11        "BACKEND": "django.core.files.storage.FileSystemStorage",
12        "OPTIONS": {
13            "location": BASE_DIR / "private_media",
14            "base_url": "/protected/",
15        },
16    },
17}
18
19SENDFILE_BACKEND = "django_sendfile.backends.nginx"
20SENDFILE_ROOT = BASE_DIR / "private_media"  # same as the storage location
21SENDFILE_URL = "/protected/"  # same as the storage base_url

The meaning of the backend options is:

location

The private equivalent of MEDIA_ROOT - the base location where private media files will be stored.

base_url

The private equivalent of MEDIA_URL. Note that your webserver must be configured appropriately for this, see the sendfile documentation. Your webserver may not directly serve these URLs, otherwise files can be downloaded without authentication.

Warning

It’s important that you specify the location and base_url options, otherwise Django will fall back to settings.MEDIA_ROOT and settings.MEDIA_URL which will typically publicly expose your files.

The sendfile settings are important to actually serve the files correctly:

  • SENDFILE_BACKEND: depends on your webserver, see the sendfile documentation.

  • SENDFILE_ROOT: should be set to the storage location, as the library will resolve files from that path.

  • SENDFILE_URL: should be set to the storage base_url, so the webserver can match the location block to serve the file.

URLs

No URLs need to be configured. However, for development, you likely want to serve the private media file uploads with Django. This can be achieved via settings:

SENDFILE_BACKEND = "django_sendfile.backends.development"

Defining model fields and admin integration

Models

The simplest way is to use the model field, which is a drop-in replacement of Django’s django.db.models.FileField.

from django.db import models

from privates.fields import PrivateMediaFileField, PrivateMediaImageField


class IDDocument(models.Model):
    pdf = PrivateMediaFileField(blank=True)
    mugshot = PrivateMediaImageField(upload_to="mugshots/%Y/")

This uses the underlying privates.storages.private_media_storage.

Admin

There is built in admin integration via a mixin. This takes care of replacing the default widget so you can open the private media files, and perform permission checks.

from django.contrib import admin

from privates.admin import PrivateMediaMixin

from .models import Invoice


@admin.register(Invoice)
class InvoiceAdmin(PrivateMediaMixin, admin.ModelAdmin):
    pass

By default, this mixin requires you to have the <applabel>.can_change_<model> permission.

Attributes:

  • privates.admin.PrivateMediaMixin.private_media_permission_required: (custom) permission to check instead of the default <applabel>.can_change_<model>

  • privates.admin.PrivateMediaMixin.private_media_view_options: optional arguments to forward to the sendfile.sendfile function.

Serving file contents

The private media files still need to be served to authorized users. The process for this is roughly:

  1. Define a view, which:

    1. Checks the user permissions

    2. Looks up the requested model instance

    3. Look up the relevant file field of the model

    4. Extract the path on-disk of the file

    5. Return a response which contains the file path information in a format the webserver understands

    6. Let the webserver (nginx, apache…) serve the file as efficiently as possible

  2. Hook up the view to a URL in your urls.py

  3. Expose the download button with a simple anchor tag URL or button action in your template(s)

Generic view

Django Privates ships with a built in permission-check view, used by the admin integration. You are encouraged to re-use this.

It’s built on top of django.contrib.auth.mixins.PermissionRequiredMixin and django.views.generic.DetailView, so the methods/attributes of these base classes are available.

from privates.views import PrivateMediaView


class InvoicePDFView(PrivateMediaView):
    queryset = Invoice.objects.all()
    file_field = "pdf"
    permission_required = "applabel.can_change_invoice"

Custom views

You can also easily serve file contents in regular views and/or djangorestframework endpoints, for example:

from django_sendfile import sendfile
from rest_framework import permissions
from rest_framework.generics import GenericAPIView

class DownloadFileView(GenericAPIView):
    queryset = IDDocument.objects.all()
    permission_classes = (permissions.IsAuthenticated,)

    def get(self, request, *args, **kwargs):
        instance = self.get_object()  # the get_object methods performs permission checks
        filepath = instance.pdf.path
        return sendfile(request, path)

Testing tools

To isolate tests, you should clean up any uploaded files generated during tests. privates.test.temp_private_root is available to facilitate this:

from privates.test import temp_private_root


@temp_private_root()
class MyTests(TestCase):
    pass

The usage is the same as override_settings, so you can use it as a class decorator, test method decorator or context manager.