257

I want users on the site to be able to download files whose paths are obscured so they cannot be directly downloaded.

For instance, I'd like the URL to be something like this: http://example.com/download/?f=somefile.txt

And on the server, I know that all downloadable files reside in the folder /home/user/files/.

Is there a way to make Django serve that file for download as opposed to trying to find a URL and View to display it?

damon
  • 12,720
  • 14
  • 49
  • 71
  • 2
    Why aren't you simply using Apache to do this? Apache serves static content faster and more simply than Django ever could. – S.Lott Jul 20 '09 at 23:08
  • 24
    I'm not using Apache because I don't want the files accessible without permissions which are based in Django. – damon Jul 21 '09 at 13:47
  • 3
    If you want to take into account user permissions you have to serve file through Django's view – Łukasz Jul 21 '09 at 13:51
  • 133
    Exactly, which is why I'm asking this question. – damon Jul 21 '09 at 14:34

15 Answers15

193

For the "best of both worlds" you could combine S.Lott's solution with the xsendfile module: django generates the path to the file (or the file itself), but the actual file serving is handled by Apache/Lighttpd. Once you've set up mod_xsendfile, integrating with your view takes a few lines of code:

from django.utils.encoding import smart_str

response = HttpResponse(mimetype='application/force-download') # mimetype is replaced by content_type for django 1.7
response['Content-Disposition'] = 'attachment; filename=%s' % smart_str(file_name)
response['X-Sendfile'] = smart_str(path_to_file)
# It's usually a good idea to set the 'Content-Length' header too.
# You can also set any other required headers: Cache-Control, etc.
return response

Of course, this will only work if you have control over your server, or your hosting company has mod_xsendfile already set up.

EDIT:

mimetype is replaced by content_type for django 1.7

response = HttpResponse(content_type='application/force-download')  

EDIT: For nginx check this, it uses X-Accel-Redirect instead of apache X-Sendfile header.

Kyrol
  • 3,146
  • 7
  • 29
  • 42
elo80ka
  • 11,979
  • 3
  • 33
  • 43
  • 6
    If your filename, or path_to_file includes non-ascii characters such as "ä" or "ö", the `smart_str` does not work as intended since apache module X-Sendfile cannot decode the smart_str encoded string. Thus for example "Örinää.mp3" file cannot be served. And if one omits the smart_str, the Django itself throws ascii encoding error because all *headers* are encoded to ascii format before sending. Only way that I know of to circumvent this problem is to reduce X-sendfile filenames to ones that consists only ascii. – Ciantic May 31 '10 at 16:13
  • 3
    To be more clear: S.Lott has the simple example, just serving files straight from django, no other setup needed. elo80ka has the more efficient example, where the web-server handles static files and django doesn't have to. The latter has better performance, but may require more setup. Both have their places. – rocketmonkeys Dec 30 '10 at 19:12
  • 1
    @Ciantic, see btimby's answer for what looks like a solution to the encoding problem. – mlissner Feb 14 '12 at 07:36
  • Does this solution work with the following web server config? Back-end: 2 or more Apache+mod_wsgi individual (VPS) servers set up to replicate each other. Front-end: 1 nginx proxy (VPS) server using upstream load balancing, doing round-robin. – Daniel Jul 06 '15 at 15:13
  • 12
    mimetype is replaced by content_type for django 1.7 – ismailsunni Aug 04 '15 at 04:04
  • @elo80ka should your solution still work with django's server though? Or is this exclusively for webservers like apache? – BoJack Horseman Apr 04 '16 at 10:03
  • if anyone looking for a general solution check my answer https://stackoverflow.com/a/41107317/3872951 – Renjith Thankachan Sep 15 '17 at 09:30
87

A "download" is simply an HTTP header change.

See http://docs.djangoproject.com/en/dev/ref/request-response/#telling-the-browser-to-treat-the-response-as-a-file-attachment for how to respond with a download.

You only need one URL definition for "/download".

The request's GET or POST dictionary will have the "f=somefile.txt" information.

Your view function will simply merge the base path with the "f" value, open the file, create and return a response object. It should be less than 12 lines of code.

S.Lott
  • 359,791
  • 75
  • 487
  • 757
  • 51
    This is essentially the correct (simple) answer, but one caution - passing the filename as a parameter means that the user can potentially download *any* file (ie. what if you pass "f=/etc/passwd" ?) There are a lot of things that help prevent this (user permissions, etc), but just be aware of this obvious but common security risk. It's basically just a subset of validating input: If you pass in a filename to a view, check the filename in that view! – rocketmonkeys Dec 30 '10 at 19:10
  • 9
    A _very simple_ fix for this security concern: `filepath = filepath.replace('..', '').replace('/', '')` – duality_ Jul 23 '13 at 20:24
  • 7
    If you use a table to store file information, including which users should be able to download it, then all you need to send is the primary key, not the filename, and the app decides what to do. – Edward Newell May 05 '15 at 04:25
34

For a very simple but not efficient or scalable solution, you can just use the built in django serve view. This is excellent for quick prototypes or one-off work, but as has been mentioned throughout this question, you should use something like apache or nginx in production.

from django.views.static import serve
filepath = '/some/path/to/local/file.txt'
return serve(request, os.path.basename(filepath), os.path.dirname(filepath))
Cory
  • 18,463
  • 16
  • 85
  • 82
  • Also very useful for providing a fallback for testing on Windows. – Amir Ali Akbari Jan 21 '14 at 09:58
  • I'm doing a standalone django project, intended to work kind of like a a desktop client, and this worked perfectly. Thanks! – daigorocub Jan 23 '14 at 11:30
  • 1
    why it is not efficient ? – zinking Mar 08 '14 at 16:02
  • 2
    @zinking because files should generally be served via something like apache rather than through the django process – Cory Mar 08 '14 at 21:04
  • 1
    What sort of performance drawbacks are we talking about here? Do files get loaded into RAM or something of the sort if they are served through django? Why isn't django capable of serving with the same efficiency as nginx? – Gershy Jul 06 '15 at 15:05
  • 1
    @GershomMaes Not a real explanation, but the official documentation says it is `grossly inefficient and probably insecure`, I imagine they know what they are talking about https://docs.djangoproject.com/en/1.8/howto/static-files/ – Mark Oct 25 '15 at 09:58
  • I'll try this - it's perfect for my needs. We use Apache to serve the file in the the live system but while developing with Django's devserver we can use this to still provide the ability to download files. This means we can test our s/w properly during development before going live. We just change the delivery mechanism. Perfect. – Mike Stoddart Oct 21 '16 at 19:18
27

S.Lott has the "good"/simple solution, and elo80ka has the "best"/efficient solution. Here is a "better"/middle solution - no server setup, but more efficient for large files than the naive fix:

http://djangosnippets.org/snippets/365/

Basically, Django still handles serving the file but does not load the whole thing into memory at once. This allows your server to (slowly) serve a big file without ramping up the memory usage.

Again, S.Lott's X-SendFile is still better for larger files. But if you can't or don't want to bother with that, then this middle solution will gain you better efficiency without the hassle.

Community
  • 1
  • 1
rocketmonkeys
  • 4,755
  • 5
  • 26
  • 21
  • 4
    That snippet is not good. That snipped relies on the `django.core.servers.httpbase` undocumented private module, which has a big warning sign at the top of the code "[DON'T USE FOR PRODUCTION USE!!!](https://github.com/django/django/blob/master/django/core/servers/basehttp.py)", which has been in the file [since it was first created](https://github.com/django/django/blob/b68c478aa5d890e76aae6e2f695220505618c8e0/django/core/servers/basehttp.py). In any event, the `FileWrapper` functionality this snippet relies on has been removed in django 1.9. – eykanal Dec 04 '15 at 15:18
17

Just mentioning the FileResponse object available in Django 1.10

Edit: Just ran into my own answer while searching for an easy way to stream files via Django, so here is a more complete example (to future me). It assumes that the FileField name is imported_file

views.py

from django.views.generic.detail import DetailView   
from django.http import FileResponse
class BaseFileDownloadView(DetailView):
  def get(self, request, *args, **kwargs):
    filename=self.kwargs.get('filename', None)
    if filename is None:
      raise ValueError("Found empty filename")
    some_file = self.model.objects.get(imported_file=filename)
    response = FileResponse(some_file.imported_file, content_type="text/csv")
    # https://docs.djangoproject.com/en/1.11/howto/outputting-csv/#streaming-large-csv-files
    response['Content-Disposition'] = 'attachment; filename="%s"'%filename
    return response

class SomeFileDownloadView(BaseFileDownloadView):
    model = SomeModel

urls.py

...
url(r'^somefile/(?P<filename>[-\w_\\-\\.]+)$', views.SomeFileDownloadView.as_view(), name='somefile-download'),
...
Shadi
  • 7,343
  • 3
  • 34
  • 58
16

Tried @Rocketmonkeys solution but downloaded files were being stored as *.bin and given random names. That's not fine of course. Adding another line from @elo80ka solved the problem.
Here is the code I'm using now:

from wsgiref.util import FileWrapper
from django.http import HttpResponse

filename = "/home/stackoverflow-addict/private-folder(not-porn)/image.jpg"
wrapper = FileWrapper(file(filename))
response = HttpResponse(wrapper, content_type='text/plain')
response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(filename)
response['Content-Length'] = os.path.getsize(filename)
return response

You can now store files in a private directory (not inside /media nor /public_html) and expose them via django to certain users or under certain circumstances.
Hope it helps.

Thanks to @elo80ka, @S.Lott and @Rocketmonkeys for the answers, got the perfect solution combining all of them =)

DevB2F
  • 3,450
  • 2
  • 22
  • 43
Salvatorelab
  • 10,675
  • 6
  • 49
  • 74
  • 1
    Thank you, this was exactly what I was looking for! – ihatecache Sep 02 '15 at 19:23
  • 1
    Add double-quotes around the file name `filename="%s"` in the Content-Disposition header, to avoid problems with spaces in file names. References: [Filenames with spaces are truncated upon download](http://kb.mozillazine.org/Filenames_with_spaces_are_truncated_upon_download), [How to encode the filename parameter of Content-Disposition header in HTTP?](http://stackoverflow.com/questions/93551/how-to-encode-the-filename-parameter-of-content-disposition-header-in-http) – Christian Long Jan 04 '16 at 18:12
  • 1
    Your solutions works for me. But I had "invalid start byte ..." error for my file. Solved it with `FileWrapper(open(path.abspath(file_name), 'rb'))` – Mark Mishyn Jan 26 '17 at 10:41
  • `FileWrapper` has been removed since Django 1.9 – freethebees Jun 21 '17 at 15:56
  • It is possible to use `from wsgiref.util import FileWrapper` – Kriss Aug 04 '17 at 11:19
13

It was mentioned above that the mod_xsendfile method does not allow for non-ASCII characters in filenames.

For this reason, I have a patch available for mod_xsendfile that will allow any file to be sent, as long as the name is url encoded, and the additional header:

X-SendFile-Encoding: url

Is sent as well.

http://ben.timby.com/?p=149

btimby
  • 1,537
  • 2
  • 10
  • 7
7

Try: https://pypi.python.org/pypi/django-sendfile/

"Abstraction to offload file uploads to web-server (e.g. Apache with mod_xsendfile) once Django has checked permissions etc."

Roberto Rosario
  • 1,628
  • 1
  • 16
  • 31
6

You should use sendfile apis given by popular servers like apache or nginx in production. Many years i was using sendfile api of these servers for protecting files. Then created a simple middleware based django app for this purpose suitable for both development & production purpose.You can access the source code here.
UPDATE: in new version python provider uses django FileResponse if available and also adds support for many server implementations from lighthttp, caddy to hiawatha

Usage

pip install django-fileprovider
  • add fileprovider app to INSTALLED_APPS settings,
  • add fileprovider.middleware.FileProviderMiddleware to MIDDLEWARE_CLASSES settings
  • set FILEPROVIDER_NAME settings to nginx or apache in production, by default it is python for development purpose.

in your classbased or function views set response header X-File value to absolute path to the file. For example,

def hello(request):  
   // code to check or protect the file from unauthorized access
   response = HttpResponse()  
   response['X-File'] = '/absolute/path/to/file'  
   return response  

django-fileprovider impemented in a way that your code will need only minimum modification.

Nginx configuration

To protect file from direct access you can set the configuration as

 location /files/ {
  internal;
  root   /home/sideffect0/secret_files/;
 }

Here nginx sets a location url /files/ only access internaly, if you are using above configuration you can set X-File as,

response['X-File'] = '/files/filename.extension' 

By doing this with nginx configuration, the file will be protected & also you can control the file from django views

Community
  • 1
  • 1
Renjith Thankachan
  • 3,638
  • 1
  • 23
  • 41
3
def qrcodesave(request): 
    import urllib2;   
    url ="http://chart.apis.google.com/chart?cht=qr&chs=300x300&chl=s&chld=H|0"; 
    opener = urllib2.urlopen(url);  
    content_type = "application/octet-stream"
    response = HttpResponse(opener.read(), content_type=content_type)
    response["Content-Disposition"]= "attachment; filename=aktel.png"
    return response 
Savad KP
  • 1,513
  • 3
  • 27
  • 34
Saurabh Chandra Patel
  • 9,983
  • 3
  • 77
  • 72
2

Django recommend that you use another server to serve static media (another server running on the same machine is fine.) They recommend the use of such servers as lighttp.

This is very simple to set up. However. if 'somefile.txt' is generated on request (content is dynamic) then you may want django to serve it.

Django Docs - Static Files

kjfletch
  • 5,118
  • 3
  • 29
  • 37
0

I have faced the same problem more then once and so implemented using xsendfile module and auth view decorators the django-filelibrary. Feel free to use it as inspiration for your own solution.

https://github.com/danielsokolowski/django-filelibrary

Daniel Sokolowski
  • 10,545
  • 3
  • 61
  • 49
0

Providing protected access to static html folder using https://github.com/johnsensible/django-sendfile: https://gist.github.com/iutinvg/9907731

iutinvg
  • 2,131
  • 1
  • 18
  • 18
0

I did a project on this. You can look at my github repo:

https://github.com/nishant-boro/django-rest-framework-download-expert

This module provides a simple way to serve files for download in django rest framework using Apache module Xsendfile. It also has an additional feature of serving downloads only to users belonging to a particular group

nishant_boro
  • 304
  • 1
  • 2
  • 8
0

Another project to have a look at: http://readthedocs.org/docs/django-private-files/en/latest/usage.html Looks promissing, haven't tested it myself yet tho.

Basically the project abstracts the mod_xsendfile configuration and allows you to do things like:

from django.db import models
from django.contrib.auth.models import User
from private_files import PrivateFileField

def is_owner(request, instance):
    return (not request.user.is_anonymous()) and request.user.is_authenticated and
                   instance.owner.pk = request.user.pk

class FileSubmission(models.Model):
    description = models.CharField("description", max_length = 200)
        owner = models.ForeignKey(User)
    uploaded_file = PrivateFileField("file", upload_to = 'uploads', condition = is_owner)
avlnx
  • 628
  • 1
  • 6
  • 19
  • 1
    request.user.is_authenticated is a method, not an attribute. (not request.user.is_anonymous()) is the exact same as request.user.is_authenticated() because is_authenticated is the inverse of is_anonymous. – explodes Aug 06 '12 at 15:07
  • @explodes Even worst, that code is right from the docs of `django-private-files`... – Armando Pérez Marqués Feb 25 '13 at 20:39