- 19. Sep
- 19:47
- 2010
Lighttpd's X-Sendfile
In this post I may introduce you, my geeky reader, to lighttpd's X-Sendfile HTTP Header. This will enable you to command lighttpd to send a file, obviously. You, my dear reader, may know that I absolutely love django. Therefore I may also give you a nice little snippet, which will expose the complete power of X-Sendfile to your mighty djangonic fingertips.
First I have to warn you that enabling X-Sendfile presents a security risk. By enabling allow-x-send-file in your mod_fastcgi settings, you give Lighttpd the option to send any file specified in the HTTP response header, as long as the user, which lighttpd is running as, has access to the file.
If this is no concern, you may continue absorbing every little bit of my awesomeness (hello HIMYM).
Enabling X-Sendfile
First you have to tell your fastcgi.server to allow lighttpd to parse X-Sendfile.
fastcgi.server = ( "/foobar.fcgi" => ( "main" => ( "host" => "127.0.0.1", "port" => 1337, .... "allow-x-send-file" => "enable", ) ), )
Django and X-Sendfile
Now you are going to put the power of X-Sendfile into django. I prefer keeping the following snippet inside my project's folder under utils/xsend.py. The whole piece will be initiated through the urls.py.
from django.http import HttpResponse, Http404 from django.conf import settings import os def send(request, file=None, from_media=False): if not file: raise Http404 if from_media: path = os.path.join(getattr(settings, 'PROJECT_ROOT'), 'media', file) else: path = file response = HttpResponse() # This makes sure that the file is downloaded and not loaded # into the browser's view. response['Content-Disposition'] = 'attachment' response['X-Sendfile'] = path return response
As you can see we have a view with arguments. file is obviously the path to the file you want to send. Furthermore I like to keep things inside my media folder, so I added the from_media kwarg. This frees me, and you, of the hassle to write the absolute filepath, in case the file sits inside the media directory. Note that this requires you to have a variable PROJECT_ROOT in your settings.py, which you should have anyway since it is best practice.
Let us continue by putting the following in our urls.py
(r'^iwant/myfile/$', 'myproject.utils.xsend.send', {'from_media': True, 'file': 'mywholesong.flac'})
So, if you now hit http://www.example.com/iwant/myfile/ you will be served mywholesong.flac.
What now?
Check Existence
Do not trust. Not even things you typed, there might be a typo.
What I mean by this is, that you should check if the file os.path.exists().
Implement some neat logging
Here is a full example.
models.py:
from django.db import models from datetime import datetime class Downloader(models.Model): ip = models.IPAddressField() host = models.CharField(max_length=256) date = models.DateTimeField(default=datetime.now()) request_url = models.CharField(max_length=800) referer_url = models.URLField(verify_exists=False) browser = models.TextField() def __unicode__(self): return self.ip class Meta: get_latest_by = 'date'
Then I recommend to either create a new function based on xsend.send() which will log, or add another keyword to the send() function. I did go with the last version:
from django.http import HttpResponse, Http404 from django.conf import settings import os from some.models import Downloader def send(request, file=None, from_media=False, log=False): if not file: raise Http404 if from_media: path = os.path.join(getattr(settings, 'PROJECT_ROOT'), 'media', file) else: path = file if log: if request.META.has_key('HTTP_REFERER'): referer = request.META['HTTP_REFERER'] else: referer = '' dl_log = Downloader(ip=request.META['REMOTE_ADDR'], host=request.META['REMOTE_HOST'], request_url=request.META['PATH_INFO'], referer_url=referer, browser=request.META['HTTP_USER_AGENT']) dl_log.save() response = HttpResponse() # This makes sure that the file is downloaded and not loaded # into the browser's view. response['Content-Disposition'] = 'attachment' response['X-Sendfile'] = path return response
Send under other filename
Yeah that's right. You can send your file under a different name by modifying the Content-Disposition attribute.
response['Content-Disposition'] = 'attachment; filename="%s"' % user.name
Great when creating temporal files with cryptic names, to avoid collisions, but still serving the user a nicely named file.
Detect the MIMETYPE
What is really missing is the MIMETYPE. Any browser worth it's salt will try to detect it on it's own, in case it has not been specified by the server. Luckily python comes with a mimetype module included, which will try to make a well educated guess.
import mimetype from django.http import HttpResponse mime_guess = mimetype.guess_type(file) # (type, encoding) - type will be type/subtype (ready for HTTP header) # or None if mime_guess[0]: response = HttpResonse(mimetype=mime_guess[0]) else: response = HttpResonse()
etc.
Closing Words
This was a rather lengthy post, but I hope you enjoyed every little bit of reading (or skipping) through it. I hope I did not make any mistakes.
Feel free to criticize me.