""" SUMMARY This module provides these classes for mapping filesystem files into Quixote: StaticFile Wraps a static filesystem file as a Quixore resource. StaticFilesFolder Wraps a filesystem folder containing static files as a Quixote namespace. CGIScript Wraps a Python CGI script as a Quixote resource. EXAMPLES 1. Map an individual filesystem file (possibly a symbolic link) and cache its contents. (Because 'stylesheet.css' isn't a valid identifier name, we need to use setattr to set it as an attribute of the current module.) this_module = sys.modules[__name__] setattr( this_module, "stylesheet.css", StaticFile( "/htdocs/legacy_app/stylesheet.css", use_cache=1, follow_symlinks=1 ) ) 2. Map a complete filesytem folder containing static files, by default not caching their contents. notes = StaticFilesFolder("/htdocs/legacy_app/notes") 3. Map a CGI script and cache the compiled script. this_module = sys.modules[__name__] setattr( this_module, "results.cgi", CGIScript("/htdocs/legacy_app/results.cgi", use_cache=1) ) """ import quixote, os, sys, mimetypes, cgi, email, urllib from cStringIO import StringIO class StaticFile: """ Wrapper for a static file on the filesystem. An instance is initialized with the absolute path to the file and optionally flags indicating whether the file's content should be cached and whether a symbolic link should be followed. The instance can then be called with a request object, so behaving like any Quixote object that models a resource. """ def __init__(self, path, use_cache=0, follow_symlinks=0): # Check that the supplied path is absolute and (if a symbolic link) may # be followed self.path = path assert os.path.isabs(path) if os.path.islink(path) and not follow_symlinks: raise quixote.errors.TraversalError 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 StaticFilesFolder: """ Wrap a filesystem folder containing static files as a Quixote namespace. An instance is initialized with the absolute path to the folder and optionally flags indicating whether items within the folder should be cached and whether symbolic links should be followed. """ _q_exports = [] def __init__(self, path, use_cache=0, list_folder=0, follow_symlinks=0): # Check that the supplied path is absolute self.path = path assert os.path.isabs(path) self.use_cache = use_cache self.cache = {} self.list_folder = list_folder self.follow_symlinks = follow_symlinks def _q_index(self, request): """ If folder listing is allowed, generate a simple HTML listing of the folder's contents with each item hyperlinked; if the item is a folder, place a '/' after it. If not allowed, return a page to that effect. """ out = StringIO() if self.list_folder: template = '%s%s' 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, "
" else: print >>out, "

Folder listing denied

" print >>out, \ "

This folder does not allow its contents to be listed.

" return out.getvalue() def _q_getname(self, request, name): """ Get a file from the filesystem folder and return the StaticFile or StaticFilesFolder wrapper of it; use caching if that is in use. """ if name in ('.', '..'): raise quixote.errors.TraversalError 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.islink(item_filepath) and not self.follow_symlinks: raise quixote.errors.TraversalError if os.path.isdir(item_filepath): item = StaticFilesFolder(item_filepath, self.use_cache, self.list_folder, self.follow_symlinks) elif os.path.isfile(item_filepath): item = StaticFile(item_filepath, self.use_cache, self.follow_symlinks) else: raise quixote.errors.TraversalError if self.use_cache: self.cache[name] = item if isinstance(item, StaticFilesFolder): 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. An instance is initialized with the absolute path to the script and optionally flags indicating whether the compiled code should be cached and whether a symbolic link should be followed. """ def __init__(self, filepath, use_cache=0, follow_symlinks=0): self.filepath = filepath self.folder, self.filename = os.path.split(filepath) self.use_cache = use_cache self.follow_symlinks = follow_symlinks 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: try: assert os.path.isabs(self.filepath) assert os.path.isfile(self.filepath) assert not os.path.islink(self.filepath) or self.follow_symlinks scriptfile = open(self.filepath) except AssertionError, IOError: raise quixote.errors.TraversalError 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. # # 1. 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. # # 2. We capture the script's stdout in order to return it to Quixote. # # 3. 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. # # 4. 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) try: # Execute the compiled CGI script and collect its output as a MIME # message (but parsing only the headers). exec code parser = email.Parser.HeaderParser() mime_message = parser.parsestr(sys.stdout.getvalue()) finally: # 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())