10

I'd like Django to serve some media files (e.g. user-uploaded files) only for logged-in users. Since my site is quite low-traffic, I think I will keep things simple and do not use django-sendfile to tell Nginx when to serve a file. Instead I'll let Django/Gunicorn do the job. To me this seems a lot simpler and for a low traffic site this maybe more secure.

But what is the best way to organize the file storage location? Media files are all stored below MEDIA_ROOT and this directory is served by Nginx in production. If I upload my files to MEDIA_ROOT/protected/ I have to tell Nginx not to serve the files in the subdirectory protected.

But is this a good idea? It seems a litte risky to me to allow Nginx access /media/ in the first place and then protect the subdirectory /media/protected/. Wouldn't it be better not to use a subdirectory of MEDIA_ROOT to store protected files?

But if I try something like this quick-and-dirty in my model:

upload_to='../protected/documents/%Y/%m/'

Django complains:

SuspiciousFileOperation at /admin/core/document/add/
The joined path (/home/me/projects/project/protected/documents/2016/09/test.file) is located outside of the base path component (/home/me/projects/project/media)

So I thing it is not good practice to "leave" the MEDIA_ROOT.

What is the best solution to store and serve protected media files?

user2496550
  • 573
  • 1
  • 8
  • 18

3 Answers3

13

Serving media files ( that may be large files) from view directly is not good. You can use sendfile extension available in nginx server; a sample nginx configuration is like below.

 location /projects/project/media/{
    # this path is not public
    internal;
    # absolute path
    alias /projects/project/media/;
 }

change your view to

@login_required
def serve_protected_document(request, file):
    document = get_object_or_404(ProtectedDocument, file="protected/documents/" + file)

    # Split the elements of the path
    path, file_name = os.path.split(file)

    response = HttpResponse()
    response["Content-Disposition"] = "attachment; filename=" + file_name
    # nginx uses this path to serve the file
    response["X-Accel-Redirect"] = document.name # path to file
    return response

Link: More details on configuring sendfile extension on nginx is here

Renjith Thankachan
  • 4,178
  • 1
  • 30
  • 47
  • This isn't answering the question. It is about how you show the link on the form edit page. – tread Mar 22 '18 at 13:29
  • 1
    The OP specified that the use case did not demand performance, and instead was looking for a simple implementation using the auth functions already in Django. – shanemgrey May 01 '18 at 21:46
  • @surfer190 Spend some time on how a webserver handle requests compared to a WSGI application, then you will get an idea! – Renjith Thankachan Nov 08 '19 at 08:18
11

I now came up with the following solution:

I have this in my Django settings:

MEDIA_ROOT = "/projects/project/media/"
MEDIA_URL = "/media/

In my models I do either:

document = models.FileField(upload_to="public/documents")

or

document = models.FileField(upload_to="protected/documents")

This way, I now have the two subdirectories 'public' and 'protected' in my media files directory.

Nginx or Djangos development server only serves the files in the 'public' subdirectory.

For Djangos development server:

if os.environ["ENVIRONMENT_TYPE"] == 'development':
    urlpatterns += static(settings.MEDIA_URL + "public/", document_root=settings.MEDIA_ROOT + "public/")

And for Nginx (used in production):

location /media/public/ {
    alias   /projects/project/media/public/;
}

When I want to serve a protected document, I do the following:

In urls.py:

url(r'^media/protected/documents/(?P<file>.*)$', core.views.serve_protected_document, name='serve_protected_document'),

And in views.py:

@login_required()
def serve_protected_document(request, file):
    document = get_object_or_404(ProtectedDocument, file="protected/documents/" + file)

    # Split the elements of the path
    path, file_name = os.path.split(file)

    response = FileResponse(document.file,)
    response["Content-Disposition"] = "attachment; filename=" + file_name

    return response

I would appreciate any comments! Are there better ways to implement this?

user2496550
  • 573
  • 1
  • 8
  • 18
  • I am looking for a solution to the same problem, except that I don't have any public media and that I wouldn't like misusing Django to serve files. But for the moment just one remark. Shouldn't file be opened before using File response? https://docs.djangoproject.com/en/1.10/ref/request-response/#fileresponse-objects – texnic Jan 15 '17 at 00:26
  • @texnic check my answer – Renjith Thankachan Apr 05 '17 at 06:33
  • 8
    Your views.py is not safe. If someone copy/pastes the url from source html, anyone should be able to download the file as long as they are logged in as anyone who has an account. – CyberHavenProgramming Oct 30 '19 at 22:30
0

project/urls.py

from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from .views import media_protect
urlpatterns = [
...
path('media/protect/<str:app>/<int:author>/<str:file>', media_protect)
...
]

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

project/views.py

from django.contrib.auth.decorators import login_required
from django.http import FileResponse
from project import settings

@login_required
def media_protect(request, app, author, file):
    media_root = os.path.join(settings.MEDIA_ROOT)
    if ...:
        response = FileResponse(
            open(f'{media_root}/protect/{app}/{author}/{file}', 'rb'))
        return response
    else ...:
        retun ...
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community May 16 '23 at 20:40