SVN: r20963 - in trunk/quixote: . doc
Andrew Kuchling <akuchlin-fVcApmY9cLvQ3/1i3zOLAti2O/[email protected]> Wed, 05 Mar 2003 13:20:01 -0500
| Newsgroups | gmane.comp.web.quixote.cvs |
|---|---|
| Message-ID | <[email protected]> |
Author: akuchlin
Date: 2003-03-05 13:20:00 -0500 (Wed, 05 Mar 2003)
New Revision: 20963
Added:
trunk/quixote/doc/static-files.txt
Modified:
trunk/quixote/ACKS
trunk/quixote/CHANGES
trunk/quixote/README
trunk/quixote/util.py
Log:
Add Hamish Lawson's classes for serving static files and CGI scripts.
I still need to read through the code more carefully, and also add an
example to the demo.
Modified: trunk/quixote/util.py
==============================================================================
--- trunk/quixote/util.py (original)
+++ trunk/quixote/util.py 2003-03-05 13:20:01.000000000 -0500
@@ -1,14 +1,24 @@
"""quixote.util
-Contains various useful functions:
+Contains various useful functions and classes:
xmlrpc(request, func) : Processes the body of an XML-RPC request, and calls
'func' with the method name and parameters.
+ StaticFile : Wraps a file from a filesystem as a
+ Quixote resource.
+ StaticFilesFolder : Wraps a filesystem folder containing static
+ files as a Quixote namespace.
+ CGIScript : Wraps a Python CGI script as a Quixote resource.
+
+StaticFile, StaticFilesFolder, and CGIScript were contributed by
+Hamish Lawson. See doc/static_files.txt for examples of their use.
"""
__revision__ = "$Id$"
import sys, xmlrpclib
+import quixote, os, mimetypes, cgi, email, urllib
+from cStringIO import StringIO
def xmlrpc (request, func):
"""xmlrpc(request:Request, func:callable) : string
@@ -45,3 +55,226 @@
request.response.set_header('content-type', 'text/xml')
return result
+
+
+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 = '<a href="%s">%s</a>%s'
+ print >>out, "<h1>%s</h1>" % request.environ['REQUEST_URI']
+ print >>out, "<pre>"
+ 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, "</pre>"
+ else:
+ print >>out, "<h1>Folder listing denied</h1>"
+ print >>out, \
+ "<p>This folder does not allow its contents to be listed.</p>"
+ 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())
Added: trunk/quixote/doc/static-files.txt
==============================================================================
--- trunk/quixote/doc/static-files.txt (original)
+++ trunk/quixote/doc/static-files.txt 2003-03-05 13:20:01.000000000 -0500
@@ -0,0 +1,49 @@
+Examples of serving static files
+================================
+
+The ``quixote.util`` module includes classes for making files,
+directories, and even CGI scripts available as Quixote resources.
+Here are some examples.
+
+
+A single file
+-------------
+
+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
+ )
+ )
+
+
+A directory
+-----------
+
+Map a complete filesytem folder containing static files, by default
+not caching their contents.
+
+::
+ notes = StaticFilesFolder("/htdocs/legacy_app/notes")
+
+A CGI script
+------------
+
+The ``use_cache=1`` requests that the compiled script be cached.
+
+::
+ this_module = sys.modules[__name__]
+ setattr(
+ this_module,
+ "results.cgi",
+ CGIScript("/htdocs/legacy_app/results.cgi", use_cache=1)
+ )
Modified: trunk/quixote/CHANGES
==============================================================================
--- trunk/quixote/CHANGES (original)
+++ trunk/quixote/CHANGES 2003-03-05 13:20:01.000000000 -0500
@@ -8,6 +8,10 @@
Note that this means HTML templates will not work with Python 2.0
unless you compile the C extension.
+ * Added StaticFile, StaticFilesFolder, and CGIScript classes
+ to quixote.util. Consult doc/static_files.txt for examples.
+ (Contributed and documented by Hamish Lawson.)
+
0.6b2 (27 Jan 2003):
Modified: trunk/quixote/ACKS
==============================================================================
--- trunk/quixote/ACKS (original)
+++ trunk/quixote/ACKS 2003-03-05 13:20:01.000000000 -0500
@@ -26,6 +26,7 @@
A.M. Kuchling <akuchlin-fVcApmY9cLvQ3/1i3zOLAti2O/[email protected]>
Erno Kuusela
Nicola Larosa
+Hamish Lawson
Patrick K. O'Brien
Ed Overly
Jeff Rush
Modified: trunk/quixote/README
==============================================================================
--- trunk/quixote/README (original)
+++ trunk/quixote/README 2003-03-05 13:20:01.000000000 -0500
@@ -115,6 +115,7 @@
session-mgmt.txt session management: how to track information
across requests
+ static-files.txt making static files and CGI scripts available
upload.txt how to handle HTTP uploads with Quixote
upgrading.txt info on backward-incompatible changes that may
affect applications written with earlier versions