Serving Static Files

This document provides information about serving static files with TurboGears. Static files can either be served directly by the application server (CherryPy), or through a traditional web server such as Apache.

When using CherryPy to serve static files there are two main methods:

  • The static filter configures the server to serve static files for a given URL (or URl prefix) from a local directory or file.
  • The serve_file function allows static files to be served from your controller methods, controlling the content type dynamically.

Using the Static Filter

When you quickstart a TurboGears project, there are several static filters already set up for you in your application’s configuration in <yourpackage>/config/app.cfg:

  • All URLs below “/static” are served from files below the directory <yourproject>/<yourpackage>/static/.
  • The URL “/favicon.ico” is served from the file <yourproject>/<yourpackage>/static/images/favicon.ico.

In a quickstarted project, the static directory also contains the three sub-directories css, images, and javascript. If, for example, you put a CSS stylesheet file named mystyles.css in the css directory, it can be accessed by the URL “/static/css/mystyles.css”.

Let’s look at the static filter configuration in detail:

[/static]
static_filter.on = True
static_filter.dir = "%(package_dir)s/static"

[/favicon.ico]
static_filter.on = True
static_filter.file = "%(package_dir)s/static/images/favicon.ico"

The string in square brackets [] denotes the preferred web url. For example, the first entry [/static] allows us to access static files at http://localhost:8080/static.

static_filter.on = True

The second line “static_filter.on = True” is necessary for all static filter sections.

static_filter.dir = "%(package_dir)s/static"

You can use “static_filter.file” or “static_filter.dir” to locate the directory or file. You can also use a regular expression with “static_filter.match”.

The static_filter requires all paths to be absolute. You can use %(top_level_dir)s to denote the top level directory of this project or %(package_dir)s to denote the directory containing the config package for this project (these directories are usually the same unless your project package is part of a greater package).

For example, to publish a file that is in the the top level directory of your project use something like:

[/sitemap.xml]
static_filter.on = True
static_filter.file = "%(package_dir)s/sitemap.xml"

You can also specify what content-type to set depending on the extension of each file being served (e.g. rss file as [/rss], atom files as [/atom]).

[/rss]
static_filter.on = True
static_filter.content_types = {'rss': 'application/rss+xml'}
static_filter.dir = '/full/path/to/feed'

[/atom]
static_filter.on = True
static_filter.content_types = {'atom': 'application/atom+xml'}
static_filter.dir = '/full/path/to/feed'

Using the serve_file() Function

You might want to have a particular way to serve static content that cannot be achieved via the static filter. In such cases, use CherryPy’s serve_file function in your exposed method instead.

from cherrypy.lib.cptools import serve_file
return serve_file("/path/to/file")

You can also optionally specify the mime type of the file to return in the Content-type header of the response with the content_type argument:

from cherrypy.lib.cptools import serve_file
return serve_file("/path/to/documents/document.pdf",
    content_type="application/pdf")

Please note that serve_file takes the filename of the file to be served, not the URL of the file. If you want to serve files from the "static" directory of your application, you can use the turbogears.config module to find out the filesystem path:

from os.path import join, normpath
from turbogears import config
from cherrypy.lib.cptools import serve_file

static_dir = config.get('static_filter.dir', path="/static")
filename = join(normpath(static_dir), 'images', 'users', 'joe.jpg')

return serve_file(filename, content_type="image/jpeg")

For more information, refer to the CherryPy documentation about Serving static content.

Protecting Static Files via Identity

Files served via CherryPy’s static filter are not protected by the TurboGears identity framework, because your application’s controllers are bypassed completely.

This following example shows a controller class, named StaticFilesController, which allows to serve static files from below a given directory and which supports protecting these files via the identity permission system.

Use this controller by mounting it somewhere in your controller tree, for example on your root controller, like this:

class Root(controller.RootController)
    private = StaticFilesController('/private')

Note

The URL path passed to the constructor should match the location where the controller is mounted! It is used as the section name to look up the configuration for this controller instance.

To set the configuration for a static files controller for a particular URL path, you can either use the config.update function in your Python code:

config.update({
    '/private': {
        # The filesystem directory under which the static files are located
        'static_files.basedir': join(os.getcwd(), 'private'),
        # Give a list of permission names either as a comma-separated
        # string or a tuple/list of strings.
        'static_files.permissions': 'static_files',
    }
})

Or put the following in one of your configuration files:

[/private]
static_files.basedir = "%(current_dir_uri)s/private"
static_files.permissions = 'static_files'

Please note that you are only able to specify a set of permissions (OR-ed together), which is required to access the static files served by this controller. You can not specify arbitrary identity predicates, like identity.in_group or identity.not_anonymous. Most of these predicates can be expressed by setting up appropriate permissions and properly assigning users to groups and groups to permissions, so this shouldn’t be a real limitation in practice. If you need finer access control you can always adapt the implementation of StaticFilesController or overwrite the permissions property of the identity.current object, so that it returns the proper permissions as necessary.

With the example configuration above you can now put static files to be served by the controller in a directory named private below the directory where your application was started. For example, if you put a file test.html in the private directory, it can now accessed by the following URL in your application (provided the user has the static_files permission):

http://yourserver.tld/private/test.html

You can have a hierarchy of sub-directories below the private directory and they will map to URL paths as expected, e.g.:

http://yourserver.tld/private/foo/bar/test.html

Use the following commands (e.g. using 'tg-admin shell') to set up a user with the appropriate permission to view the files served by the example StaticFilesController instance:

from yourpkg import model

model.create_tables()
u = model.User.by_user_name(u'test')

if not u:
    model.create_default_user(u'test', password=u'test')
    g = model.Group()
    g.group_name(u'users')
    g.display_name(u'All users')
    model.session.add(g)
    p = model.Permission()
    p.permission_name = u'static_files'
    p.description = u'Can access static files'
    model.session.add(p)
    p.groups.append(g)
    g.users.append(u)
    model.session.flush()

Here’s the code for the controller, slightly abridged for the sake of brevity:

import os
from mimetypes import guess_type
from os.path import abspath, basename, commonprefix, exists, join

# third-party imports
from cherrypy import request, NotFound
from cherrypy.lib.cptools import serve_file
from turbogears import config, controllers, expose, identity


class StaticFilesController(controllers.Controller):
    def __init__(self, baseurl):
        basedir = config.get('static_files.basedir', path=baseurl)
        if not basedir:
            raise ValueError("'static_files.basedir' not configured for URL "
                "path '%s'." % baseurl)
        self.basedir = abspath(basedir)
        self.baseurl = baseurl

        # get permissions from configuration and apply them
        perms = config.get('static_files.permissions', path=baseurl)
        if perms:
            if isinstance(perms, basestring):
                perms = [p.strip() for p in perms.split(',') if p.strip()]
            if isinstance(perms, (tuple, list)):
                self.require = identity.has_any_permission(*perms)
                if not isinstance(self, identity.SecureResource):
                    self.__class__.__bases__ += (identity.SecureResource,)
        super(StaticFilesController, self).__init__()

    @expose()
    def default(self, *args, **kwargs):
        """Serve file from below self.basedir specified in first URL param."""

        if not args:
            raise NotFound()
        fullpath = abspath(join(self.basedir, *args))

        # check if fullpath exists and is below basedir
        if commonprefix([self.basedir, fullpath]) != self.basedir or \
                not exists(fullpath):
            raise NotFound()

        mimetype, enc = guess_type(fullpath)
        try:
            return serve_file(fullpath, contentType=mimetype)
        except (IOError, OSError):
            raise NotFound()

Here is a module called staticfiles.py, which you can download and copy into your application’s package directory and then import the controller in controllers.py with:

from staticfiles import StaticFilesController

Download: staticfiles.py