Quixote Extras
hist

<root> / rex / StaticFile.py

"""StaticFile.py

Wrap a static file or directory so it can be served as a Quixote callable.

CHANGELOG:
2005-06-01: Copied from quixote.util module.  Removed functions and imports
  not needed by StaticFile or StaticDirectory.  The only material difference in
  our Static File is the "Last-Modified" header is not generated; this is what
  causes the mod_scgi Refresh bug.  THIS VERSION DISABLES BROWSER CACHING;
  that's the tradeoff.

The official download location for this module is:
http://cafepy.com/quixote_extras/rex/StaticFile.py

Derived from quixote.util module at
http://www.mems-exchange.org/software/quixote/
and carries the same license.
"""

import sys
import os
import mimetypes
import urllib
from rfc822 import formatdate
import quixote
from quixote import errors
from quixote.directory import Directory
from quixote.html import htmltext, TemplateIO

from quixote.util import FileStream
from quixote.util import StaticDirectory as BaseStaticDirectory

class StaticFile:

    """
    Wrapper for a static file on the filesystem.
    """

    def __init__(self, path, follow_symlinks=False,
                 mime_type=None, encoding=None, cache_time=None):
        """StaticFile(path:string, follow_symlinks:bool)

        Initialize instance with the absolute path to the file.  If
        'follow_symlinks' is true, symbolic links will be followed.
        'mime_type' specifies the MIME type, and 'encoding' the
        encoding; if omitted, the MIME type will be guessed,
        defaulting to text/plain.

        Optional cache_time parameter indicates the number of
        seconds a response is considered to be valid, and will
        be used to set the Expires header in the response when
        quixote gets to that part.  If the value is None then
        the Expires header will not be set.
        """

        # Check that the supplied path is absolute and (if a symbolic link) may
        # be followed
        self.path = path
        if not os.path.isabs(path):
            raise ValueError, "Path %r is not absolute" % path
        # Decide the Content-Type of the file
        guess_mime, guess_enc = mimetypes.guess_type(os.path.basename(path),
                                                     strict=False)
        self.mime_type = mime_type or guess_mime or 'text/plain'
        self.encoding = encoding or guess_enc or None
        self.cache_time = cache_time
        self.follow_symlinks = follow_symlinks

    def __call__(self):
        if not self.follow_symlinks and os.path.islink(self.path):
            raise errors.TraversalError(private_msg="Path %r is a symlink"
                                        % self.path)
        request = quixote.get_request()
        response = quixote.get_response()

        if self.cache_time is None:
            response.set_expires(None) # don't set the Expires header
        else:
            # explicitly allow client to cache page by setting the Expires
            # header, this is even more efficient than the using
            # Last-Modified/If-Modified-Since since the browser does not need
            # to contact the server
            response.set_expires(seconds=self.cache_time)

        stat = os.stat(self.path)
        last_modified = formatdate(stat.st_mtime)
        # XXX Commented to test the following code.
        #if last_modified == request.get_header('If-Modified-Since'):
        #    # handle exact match of If-Modified-Since header
        #    response.set_status(304)
        #    return ''

        # Set the Content-Type for the response and return the file's contents.
        response.set_content_type(self.mime_type)
        if self.encoding:
            response.set_header("Content-Encoding", self.encoding)

        # @@MO: This line causes the SCGI Refresh bug.
        #response.set_header('Last-Modified', last_modified)

        return FileStream(open(self.path, 'rb'), stat.st_size)


class StaticDirectory(BaseStaticDirectory):
    FILE_CLASS = StaticFile