Here is a revised version of my module for mapping filesystem files into
Quixote. New in this version is the ability to wrap CGI scripts. Also, for
better performance (but at the cost of memory) the module can now
optionally cache the contents of static files or the compiled code for CGI
scripts. I've also documented the module more fully.
Hamish Lawson
=== filesystem.py =============================================================
"""
SUMMARY
This module provides these classes for mapping filesystem files into Quixote:
FilesystemFile
Wraps a static filesystem file as a Quixore resource.
FilesystemFolder
Wraps a filesystem folder containing static files as a Quixote
namespace.
CGIScript
Wraps a Python CGI script as a Quixote resource.
EXAMPLES
1. Mapping an individual filesystem file. (Because 'stylesheet.css' isn't a
valid identifier name, we need to use setattr to set it as an attribute of
the current module.)
filepath = ("/htdocs/legacy_app/stylesheet.css")
setattr(sys.modules[__name__], "stylesheet.css", FilesystemFile(filepath))
2. Mapping a complete filesytem folder containing static files, but not
caching their contents.
notes = FilesystemFolder("/htdocs/legacy_app/notes", use_cache=0)
3. Mapping a CGI script.
filepath = CGIScript("/htdocs/legacy_app/results.cgi"
setattr(sys.modules[__name__], "results.cgi", CGIScript(filepath))
"""
import quixote, os, sys, mimetypes, cgi, email, urllib
from cStringIO import StringIO
class FilesystemFile:
"""
Wrapper for a static file on the filesystem.
An instance is initialized with the path to the file and optionally a flag
indicating whether the file's content should be cached. The instance can
then be called with a request, so behaving like any Quixote object that
models a resource.
"""
def __init__(self, path, use_cache=1):
self.path = path
self.use_cache = use_cache
self.cache = None
# Decide the Content-Type of the file
self.mimetype = \
mimetypes.guess_type(os.path.basename(path), strict=0)[0] \
or 'text/plain'
def __call__(self, request):
# Set the Content-Type for the response and return the file's
contents;
# use caching if enabled.
request.response.set_header('Content-Type', self.mimetype)
if self.cache:
contents = self.cache
else:
fsfile = open(self.path)
contents = fsfile.read()
fsfile.close()
if self.use_cache:
self.cache = contents
return contents
class FilesystemFolder:
"""
Wrap a filesystem folder containing static files as a Quixote namespace.
An instance is initialized with the path to the folder and optionally
a flag
indicating whether items within the folder should be cached.
"""
_q_exports = []
def __init__(self, path, use_cache=1):
self.path = path
self.use_cache = use_cache
self.cache = {}
def _q_index(self, request):
"""
Generate a simple HTML listing of the folder's contents with each item
hyperlinked. If the item is a folder, place a '/' after it.
"""
template = '%s%s'
out = StringIO()
print >>out, "%s
" % request.environ['REQUEST_URI']
print >>out, ""
print >>out, template % ('..', '..', '')
for filename in os.listdir(self.path):
filepath = os.path.join(self.path, filename)
marker = os.path.isdir(filepath) and "/" or ""
print >>out, template % (urllib.quote(filename), filename, marker)
print >>out, "
"
return out.getvalue()
def _q_getname(self, request, name):
"""
Get a file from the filesystem folder and return the FilesystemFile
or FilesystemFolder wrapper of it; use caching if that is in use.
"""
if name in ('.', '..'):
raise quixote.errors.TraversalError(name)
if self.cache.has_key(name):
# Get item from cache
item = self.cache[name]
else:
# Get item from filesystem; cache it if caching is in use
item_filepath = os.path.join(self.path, name)
if os.path.isdir(item_filepath):
item = FilesystemFolder(item_filepath, self.use_cache)
elif os.path.isfile(item_filepath):
item = FilesystemFile(item_filepath, self.use_cache)
else:
raise quixote.errors.TraversalError(name + "***")
if self.use_cache:
self.cache[name] = item
if isinstance(item, FilesystemFolder):
return item
else:
return item(request)
class SimulatedCGIStandardInput:
"""
Provides a simulated stdin to CGI scripts. The data is obtained from
the request.form object already created by Quixote.
"""
def __init__(self, request):
self.request = request
def read(self, length):
if self.request.environ['REQUEST_METHOD'] == 'POST':
return urllib.urlencode(self.request.form, doseq=1)
else:
return None
class CGIScript:
"""
Wraps a Python CGI script as a Quixote resource.
"""
def __init__(self, filepath, use_cache=1):
self.filepath = filepath
self.folder, self.filename = os.path.split(filepath)
self.use_cache = use_cache
self.cache = None
def __call__(self, request):
# If the compiled script is cached, get it from there. Otherwise
# read the script file and compile it; if caching is being used,
# cache the compiled code.
if self.cache:
code = self.cache
else:
scriptfile = open(self.filepath)
code = compile(scriptfile.read(), self.filepath, 'exec')
scriptfile.close()
if self.use_cache:
self.cache = code
# Set up the context a conventional CGI script may expect.
#
# If the request is a POST, Quixote will already have consumed stdin,
# so we provide the CGI script with a simulated stdin that uses
# the form object created by Quixote.
#
# We capture the script's stdout in order to return it to Quixote.
#
# We update os.environ to cater for the fact that a CGI script will
# look for HTTP/CGI environment variables there, but Quixote stores
# them in request.environ.
#
# We provide for two assumptions that a Python CGI script might make
# about directories. First, in a conventional CGI context the web
# server would set the current directory to the CGI script's location.
# Second, this directory would be at the start of Python's module
# search path, due to the fact that a new Python interpreter would
# be started up to run the script.
original_stdin = sys.stdin
original_stdout = sys.stdout
sys.stdin = SimulatedCGIStandardInput(request)
sys.stdout = StringIO()
os.environ.update(request.environ)
original_cwd = os.getcwd()
os.chdir(self.folder)
original_sys_path = sys.path
sys.path.insert(0, self.folder)
# Execute the compiled CGI script and collect its output as a MIME
# message (but parse only the headers).
exec code
parser = email.Parser.HeaderParser()
mime_message = parser.parsestr(sys.stdout.getvalue())
# Restore the context that was in effect before running the script.
sys.stdout = original_stdout
sys.stdin = original_stdin
sys.path = original_sys_path
os.chdir(original_cwd)
# Copy the generated headers to Quixote's response and return the
body.
for header, value in mime_message.items():
request.response.set_header(header, value)
return str(mime_message.get_payload())