• 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.

blog comments powered by Disqus

This is Luis’ blog. Here he posts about stuff that he encounters in everday life, both virtual and real.

Recently he wrote “Complete Facebook Profile?”, “Lighttpd's X-Sendfile”, “Modular Lighttpd Configurations”, “Fool Facebook's Like-Button” and “Calculating Battery Health”.

Contact