Instant File Provider

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Instant File Provider
=====================

Browse and serve local files via HTTP.

Sole requirements are Python 2.5 and Werkzeug_ 0.4 as of this version.  The
first release (0.1, 24-Sep-2007) was based on Paste, Paste Deploy, Paste
Script, and Genshi.

Version 0.2.1 introduced the use of unicode objects for most paths.  This
results in unicode objects being returned from path functions and allows for
handling files with non-ASCII characters.

Usage::

    python instantfileprovider.py runserver

.. _Werkzeug: http://werkzeug.pocoo.org/

:Copyright: 2007-2009 Jochen Kupperschmidt
:Date: 05-Jan-2009
:License: GNU General Public License
:Version: 0.2.1
"""

from fnmatch import fnmatch
from itertools import starmap
import os

from werkzeug.exceptions import abort, HTTPException
from werkzeug import script
from werkzeug.utils import escape, SharedDataMiddleware, url_fix, xhtml
from werkzeug.wrappers import BaseResponse as Response


# The path you want to publish.
DOCUMENT_ROOT = '/path/to/publish'

# File patterns to exclude.
HIDE = [u'.hg', u'.svn', u'*.pyc', u'*.pyo']


class Application(object):
    """The WSGI application"""

    def __init__(self, document_root, hide):
        self.document_root = os.path.abspath(document_root)
        self.hide = hide

    def __call__(self, environ, start_response):
        try:
            # Make sure the document root exists
            # (yes, on *every* request).
            if not os.path.isdir(self.document_root):
                abort(503, 'Document root does not exist,'
                    'please correct your setup.')

            path_info = environ.get('PATH_INFO', u'')
            path = os.path.abspath(
                os.path.join(self.document_root, path_info[1:]))
            if not os.path.isdir(path):
                # Directory doesn't exist.
                abort(404)

            # Fetch directory entries and return them as XHTML.
            entries = list(get_entries(path, self.hide))
            xhtml = xhtmlize(path_info, entries)
            response = Response(xhtml, mimetype='text/html')
        except HTTPException, exc:
            response = exc
        return response(environ, start_response)


def get_entries(path, hide):
    """Yield directory entries."""
    for entry in os.listdir(path):
        # Check if the entry should be hidden.
        if not any(fnmatch(entry, pattern) for pattern in hide):
            # Categorize entry as directory or file.
            yield entry, os.path.isdir(os.path.join(path, entry))

def xhtmlize(path, entries):
    """Build a XHTML document to browse a directory."""
    def generate_path_anchors():
        stack = []
        for segment in filter(None, path.split('/')):
            stack.append(segment)
            yield xhtml.a(stack[-1],
                href=u'/'.join([''] + map(url_fix, stack)))

    if not path.endswith(u'/'):
        path += u'/'

    # Add an entry point towards the higher directory.
    if path != u'/':
        entries.insert(0, (u'..', True))

    # Sort directory entries by type (reversed), then by name.
    entries.sort(key=lambda entry: (not entry[1], entry[0]))

    def itemize(item, is_dir):
        label = escape(item)
        if is_dir:
            label = xhtml.strong(label + u'/')
            item += u'/'
        return xhtml.li(xhtml.a(label, href=url_fix(path + item)))

    return xhtml.html(
        xhtml.body(
            xhtml.h1(u'/' + u'/'.join(generate_path_anchors())),
            xhtml.ul(*list(starmap(itemize, entries))),
        )
    )

def make_shared_app():
    # The application needs a unicode path.
    app = Application(unicode(DOCUMENT_ROOT), HIDE)
    # The middleware is buggy so feed it a non-unicode string.
    app = SharedDataMiddleware(app, {'/': DOCUMENT_ROOT})
    return app

action_runserver = script.make_runserver(make_shared_app)

if __name__ == '__main__':
    script.run()