As a core aspect of nearly every company website, the contact form is not something that would usually make us pause for thought here at FARM. However, we recently needed to include a file upload in a contact form, and to subsequently restrict access to those uploads to the site administrators. We thought we’d share a useful little bit of Django/nginx configuration to do just that.

First, the trick of hiding these files from the public. While it’s possible to store these files out of the web root, you’re then relying on Django alone serve the files. This is not ideal as it holds up the Python thread whilst the entire transfer occurs. This could provide a trivial way for a malicious user (or even just an unfortunate set of circumstances) to deny access to your site.

The solution then is creating an area in your media folder that’s not directly web accessible.

Here’s the simple way to do this with nginx. Inside your nginx configuration file:

location /media/protected/uploads/ {
    internal;
    root /srv/www/project/src/;
}

This would suffice fine if you never actually planned to view the submissions, but we require access to the files for site administrators via the admin interface.

So in the the app’s urls.py, you need:

urlpatterns = patterns(
    ...,
    url(r'^(?P<filename>[^/]+)/$', 'file_serve', name='upload_file_serve'),
)

And the corresponding view in views.py:

def file_serve(request, filename):
    if request.user.is_superuser:
        response = HttpResponse()
        url = "/media/protected/" + filename
        response['Content-Disposition'] = 'attachment; filename=%s' % smart_str(filename)
        length = os.path.getsize(MEDIA_ROOT + "media/protected/" + filename)
        response['Content-Length'] = str(length)
        response['X-Accel-Redirect'] = url
        return response
    else:
        return HttpResponseForbidden("Restricted Access")

The view uses Django’s built-in security to handle all authentication matters and simply checks if the user is authenticated. If they’re not they get served a 403. If they are authenticated, then it uses Nginx’s X-Accel-Redirect header to pass them the file as a download. This could then easily be customised further to suggest rendering instead of saving depending the circumstances or anything else that you want to do here.

The final piece of the puzzle is simply rending the new url in the admin.

In the model in models.py:

class MyModel(models.Model):

    ...

    upload = models.FileField(upload_to='protected')

    ...

    def protected_file(self,):
        """
        Returns the url for the file using the view that ensures user is logged in
        """
        url = reverse('contact_file_serve', args=[os.path.basename(self.upload.file.name)])
        return format_html('<a href="'+url+'">File</a>')
        protected_file.short_description = 'Upload'

And in the admin.py:

class CareersApplicationAdmin(admin.ModelAdmin):
    readonly_fields = (..., 'protected_file')
    exclude = ('upload', )