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