SVN: r25341 - in trunk/quixote: . form2

Neil Schemenauer <nascheme-fVcApmY9cLvQ3/1i3zOLAti2O/[email protected]> Thu, 14 Oct 2004 13:22:15 -0400
Newsgroups gmane.comp.web.quixote.cvs
Message-ID <[email protected]>
Author: nascheme
Date: 2004-10-14 13:22:05 -0400 (Thu, 14 Oct 2004)
New Revision: 25341

Removed:
   trunk/quixote/upload.py
Modified:
   trunk/quixote/config.py
   trunk/quixote/form2/widget.py
   trunk/quixote/http_request.py
   trunk/quixote/publish.py
Log:
Add support to HTTPRequest for multipart/form-data type requests.
Enhance MIME parsing so that it doesn't read huge lines into memory.
Also implement parsing of application/x-www-form-urlencoded data rather
than using the standard cgi.py module.  Remove UPLOAD_DIR and
UPLOAD_DIR_MODE config options.  Uploaded files are now created using
the tempfile module.


Modified: trunk/quixote/config.py
===================================================================
--- trunk/quixote/config.py	2004-10-14 17:19:15 UTC (rev 25340)
+++ trunk/quixote/config.py	2004-10-14 17:22:05 UTC (rev 25341)
@@ -142,19 +142,6 @@
 MAIL_DEBUG_ADDR = None   # eg. "[email protected]"
 
 
-# HTTP file upload variables
-# ==========================
-
-# Any files upload via HTTP will be written to temporary files
-# in UPLOAD_DIR.  If UPLOAD_DIR is not defined, any attempts to
-# upload via HTTP will crash (ie. uncaught exception).
-UPLOAD_DIR = None
-
-# If UPLOAD_DIR does not exist, Quixote will create it with
-# mode UPLOAD_DIR_MODE.  No idea what this should be on Windows.
-UPLOAD_DIR_MODE = 0755
-
-
 # -- End config variables ----------------------------------------------
 # (no user serviceable parts after this point)
 
@@ -205,8 +192,6 @@
         'mail_from',
         'mail_server',
         'mail_debug_addr',
-        'upload_dir',
-        'upload_dir_mode',
         ]
 
 

Modified: trunk/quixote/form2/widget.py
===================================================================
--- trunk/quixote/form2/widget.py	2004-10-14 17:19:15 UTC (rev 25340)
+++ trunk/quixote/form2/widget.py	2004-10-14 17:22:05 UTC (rev 25341)
@@ -8,7 +8,7 @@
 import struct
 from quixote import get_request
 from quixote.html import htmltext, htmlescape, htmltag, TemplateIO, stringify
-from quixote.upload import Upload
+from quixote.http_request import Upload
 
 def subname(prefix, name):
     """Create a unique name for a sub-widget or sub-component."""

Modified: trunk/quixote/http_request.py
===================================================================
--- trunk/quixote/http_request.py	2004-10-14 17:19:15 UTC (rev 25340)
+++ trunk/quixote/http_request.py	2004-10-14 17:22:05 UTC (rev 25341)
@@ -28,8 +28,10 @@
 import time
 import urlparse, urllib
 from cgi import FieldStorage
+from types import ListType
 
 from quixote.http_response import HTTPResponse
+from quixote.errors import RequestError
 
 
 # Various regexes for parsing specific bits of HTTP, all from RFC 2616.
@@ -62,7 +64,60 @@
     else:
         return None
 
+def parse_header(line):
+    """Parse a Content-type like header.
 
+    Return the main content-type and a dictionary of options.
+
+    """
+    plist = map(lambda x: x.strip(), line.split(';'))
+    key = plist.pop(0).lower()
+    pdict = {}
+    for p in plist:
+        i = p.find('=')
+        if i >= 0:
+            name = p[:i].strip().lower()
+            value = p[i+1:].strip()
+            if len(value) >= 2 and value[0] == value[-1] == '"':
+                value = value[1:-1]
+            pdict[name] = value
+    return key, pdict
+
+def parse_content_disposition(full_cdisp):
+    (cdisp, cdisp_params) = parse_header(full_cdisp)
+    name = cdisp_params.get('name')
+    if not (cdisp == 'form-data' and name):
+        raise RequestError('expected Content-Disposition: form-data '
+                           'with a "name" parameter: got %r' % full_cdisp)
+    return (name, cdisp_params.get('filename'))
+
+def parse_query(qs):
+    """(qs: string) -> {key:string, string|[string]}
+
+    Parse a query given as a string argument and return a dictionary.
+    """
+    fields = {}
+    for chunk in filter(None, qs.split('&')):
+        if '=' not in chunk:
+            name = chunk
+            value = ''
+        else:
+            name, value = chunk.split('=', 1)
+        name = urllib.unquote(name.replace('+', ' '))
+        value = urllib.unquote(value.replace('+', ' '))
+        _add_field_value(fields, name, value)
+    return fields
+
+def _add_field_value(fields, name, value):
+    if name in fields:
+        values = fields[name]
+        if not isinstance(values, list):
+            fields[name] = values = [values]
+        values.append(value)
+    else:
+        fields[name] = value
+
+
 class HTTPRequest:
     """
     Model a single HTTP request and all associated data: environment
@@ -89,17 +144,12 @@
     when handling an exception.
     """
 
-    def __init__(self, stdin, environ, content_type=None):
+    def __init__(self, stdin, environ):
         self.stdin = stdin
         self.environ = environ
-        if content_type is None:
-            self.content_type = get_content_type(environ)
-        else:
-            self.content_type = content_type
         self.form = {}
         self.session = None
         self.response = HTTPResponse()
-        self.start_time = None
 
         # The strange treatment of SERVER_PORT_SECURE is because IIS
         # sets this environment variable to "0" for non-SSL requests
@@ -132,7 +182,7 @@
     def add_form_value(self, key, value):
         if self.form.has_key(key):
             found = self.form[key]
-            if isinstance(found, list):
+            if type(found) is ListType:
                 found.append(value)
             else:
                 found = [found, value]
@@ -141,27 +191,73 @@
             self.form[key] = value
 
     def process_inputs(self):
-        """Process request inputs.
-        """
-        self.start_time = time.time()
-        if self.get_method() != 'GET':
-            # Avoid consuming the contents of stdin unless we're sure
-            # there's actually form data.
-            if self.content_type == "multipart/form-data":
-                raise RuntimeError(
-                    "cannot handle multipart/form-data requests")
-            elif self.content_type == "application/x-www-form-urlencoded":
-                fp = self.stdin
-            else:
-                return
+        query = self.environ.get('QUERY_STRING')
+        if query:
+            self.form.update(parse_query(query))
+        length = self.environ.get('CONTENT_LENGTH', 0)
+        try:
+            length = int(length)
+        except ValueError:
+            raise RequestError('invalid content-length header')
+        content_type = self.environ.get("CONTENT_TYPE")
+        if content_type:
+            content_type = content_type.split(';')[0]
+        if content_type == 'application/x-www-form-urlencoded':
+            self._process_urlencoded(length)
+        elif content_type == 'multipart/form-data':
+            self._process_multipart(length)
+
+    def _process_urlencoded(self, length):
+        query = self.stdin.read(length)
+        if len(query) != length:
+            raise RequestError('unexpected end of request body')
+        self.form.update(parse_query(query))
+
+    def _process_multipart(self, length):
+        full_ctype = self.environ.get('CONTENT_TYPE')
+        (ctype, ctype_params) = parse_header(full_ctype)
+        boundary = ctype_params.get('boundary')
+        assert ctype == 'multipart/form-data'
+        if not boundary:
+            raise RequestError('multipart/form-data missing boundary')
+        mimeinput = MIMEInput(self.stdin, boundary, length)
+        try:
+            for line in mimeinput.readpart():
+                pass # discard lines up to first boundary
+            while mimeinput.moreparts():
+                self._process_multipart_body(mimeinput)
+        except EOFError:
+            raise RequestError('unexpected end of multipart/form-data')
+
+    def _process_multipart_body(self, mimeinput):
+        headers = StringIO()
+        lines = mimeinput.readpart()
+        for line in lines:
+            headers.write(line)
+            if line == '\r\n':
+                break
+        headers.seek(0)
+        headers = rfc822.Message(headers)
+        full_cdisp = headers.get('content-disposition')
+        if not full_cdisp:
+            raise RequestError('expected Content-Disposition header')
+        (cdisp, cdisp_params) = parse_header(full_cdisp)
+        name = cdisp_params.get('name')
+        filename = cdisp_params.get('filename')
+        if not (cdisp == 'form-data' and name):
+            raise RequestError('expected Content-Disposition: form-data'
+                               'with a "name" parameter: got %r' % full_cdisp)
+        # FIXME: should really to handle Content-Transfer-Encoding and other
+        # MIME complexity here.  See RFC2048 for the full horror story.
+        if filename:
+            # it might be large file upload so use a temporary file
+            content_type = headers.get('content-type')
+            upload = Upload(filename, content_type)
+            upload.receive(lines)
+            _add_field_value(self.form, name, upload)
         else:
-            fp = None
+            _add_field_value(self.form, name, '\n'.join(lines))
 
-        fs = FieldStorage(fp=fp, environ=self.environ, keep_blank_values=1)
-        if fs.list:
-            for item in fs.list:
-                self.add_form_value(item.name, item.value)
-
     def get_header(self, name, default=None):
         """get_header(name : string, default : string = None) -> string
 
@@ -331,7 +427,6 @@
                     found[encoding] = q
         return found
 
-
     def dump(self):
         result=[]
         row='%-15s %s'
@@ -496,3 +591,178 @@
             result[name] = value
 
     return result
+
+
+SAFE_CHARS = string.letters + string.digits + "-@&+=_., "
+_safe_trans = None
+
+def make_safe_filename(s):
+    global _safe_trans
+    if _safe_trans is None:
+        _safe_trans = ["_"] * 256
+        for c in SAFE_CHARS:
+            _safe_trans[ord(c)] = c
+        _safe_trans = "".join(_safe_trans)
+
+    return s.translate(_safe_trans)
+
+
+class Upload:
+    r"""
+    Represents a single uploaded file.  Uploaded files live in the
+    filesystem, *not* in memory.
+
+      fp
+        an open file containing the content of the upload.  The file pointer
+        points to the beginning of the file
+      orig_filename
+        the complete filename supplied by the user-agent in the
+        request that uploaded this file.  Depending on the browser,
+        this might have the complete path of the original file
+        on the client system, in the client system's syntax -- eg.
+        "C:\foo\bar\upload_this" or "/foo/bar/upload_this" or
+        "foo:bar:upload_this".
+      base_filename
+        the base component of orig_filename, shorn of MS-DOS,
+        Mac OS, and Unix path components and with "unsafe"
+        characters neutralized (see make_safe_filename())
+      content_type
+        the content type provided by the user-agent in the request
+        that uploaded this file.
+    """
+
+    def __init__(self, orig_filename, content_type=None):
+        if orig_filename:
+            self.orig_filename = orig_filename
+            bspos = orig_filename.rfind("\\")
+            cpos = orig_filename.rfind(":")
+            spos = orig_filename.rfind("/")
+            if bspos != -1:                 # eg. "\foo\bar" or "D:\ding\dong"
+                filename = orig_filename[bspos+1:]
+            elif cpos != -1:                # eg. "C:foo" or ":ding:dong:foo"
+                filename = orig_filename[cpos+1:]
+            elif spos != -1:                # eg. "foo/bar/baz" or "/tmp/blah"
+                filename = orig_filename[spos+1:]
+            else:
+                filename = orig_filename
+
+            self.base_filename = make_safe_filename(filename)
+        else:
+            self.orig_filename = None
+            self.base_filename = None
+        self.content_type = content_type
+        self.fp = None
+
+    def receive(self, lines):
+        self.fp = tempfile.TemporaryFile("w+b")
+        for line in lines:
+            self.fp.write(line)
+        self.fp.seek(0)
+
+    def __str__(self):
+        return str(self.orig_filename)
+
+    def __repr__(self):
+        return "<%s at %x: %s>" % (self.__class__.__name__, id(self), self)
+
+    def read(self, n):
+        return self.fp.read(n)
+
+    def readline(self):
+        return self.fp.readlines()
+
+    def __iter__(self):
+        return iter(self.fp)
+
+    def close(self):
+        self.fp.close()
+
+
+class LineInput:
+    r"""
+    A wrapper for an input stream that has the following properties:
+
+        * lines are terminated by \r\n
+
+        * lines shorter than 'maxlength' are always returned unbroken
+
+        * lines longer than 'maxlength' are broken but the pair of
+          characters \r\n are never split
+
+        * no more than 'length' characters are read from the underlying
+          stream
+
+        * if the underlying stream does not produce at least 'length'
+          characters then EOFError is raised
+
+    """
+    def __init__(self, fp, length):
+        self.fp = fp
+        self.length = length
+        self.buf = ''
+
+    def readline(self, maxlength=4096):
+        # fill buffer
+        n = min(self.length, maxlength - len(self.buf))
+        if n > 0:
+            self.length -= n
+            assert self.length >= 0
+            chunk = self.fp.read(n)
+            if len(chunk) != n:
+                raise EOFError('unexpected end of input')
+            self.buf += chunk
+        # split into lines
+        buf = self.buf
+        i = buf.find('\r\n')
+        if i >= 0:
+            i += 2
+            self.buf = buf[i:]
+            return buf[:i]
+        elif buf.endswith('\r'):
+            # avoid splitting CR LF pairs
+            self.buf = '\r'
+            return buf[:-1]
+        else:
+            self.buf = ''
+            return buf
+
+class MIMEInput:
+    """
+    Split a MIME input stream into parts.  Note that this class does not
+    handle headers, transfer encoding, etc.  
+    """
+
+    def __init__(self, fp, boundary, length):
+        self.lineinput = LineInput(fp, length)
+        self.pat = re.compile(r'--%s(--)?[ \t]*\r\n' % re.escape(boundary))
+        self.done = False
+
+    def moreparts(self):
+        """Return true if there are more parts to be read."""
+        return not self.done
+
+    def readpart(self):
+        """Generate all the lines up to a MIME boundary.  Note that you
+        must exhaust the generator before calling this function again."""
+        assert not self.done
+        last_line = ''
+        while 1:
+            line = self.lineinput.readline()
+            if not line:
+                # Hit EOF -- nothing more to read.  This should *not* happen
+                # in a well-formed MIME message.
+                raise EOFError('MIME boundary not found (end of input)')
+            if last_line.endswith('\r\n') or last_line == '':
+                m = self.pat.match(line)
+                if m:
+                    # If we hit the boundary line, return now. Forget
+                    # the current line *and* the CRLF ending of the
+                    # previous line.
+                    if m.group(1):
+                        # hit final boundary
+                        self.done = True
+                    yield last_line[:-2]
+                    return
+            if last_line:
+                yield last_line
+            last_line = line

Modified: trunk/quixote/publish.py
===================================================================
--- trunk/quixote/publish.py	2004-10-14 17:19:15 UTC (rev 25340)
+++ trunk/quixote/publish.py	2004-10-14 17:22:05 UTC (rev 25341)
@@ -18,9 +18,8 @@
 from quixote import errors
 from quixote.html import htmltext
 from quixote.util import dump_request
-from quixote.http_request import HTTPRequest, get_content_type
+from quixote.http_request import HTTPRequest
 from quixote.http_response import HTTPResponse, Stream
-from quixote.upload import HTTPUploadRequest
 from quixote.sendmail import sendmail
 
 try:
@@ -206,16 +205,6 @@
 
     debug = log # backwards compatibility
 
-    def create_request(self, stdin, env):
-        ctype = get_content_type(env)
-        if ctype == "multipart/form-data":
-            req = HTTPUploadRequest(stdin, env, content_type=ctype)
-            req.set_upload_dir(self.config.upload_dir,
-                               self.config.upload_dir_mode)
-            return req
-        else:
-            return HTTPRequest(stdin, env, content_type=ctype)
-
     def parse_request(self, request):
         """Parse the request information waiting in 'request'.
         """
@@ -242,7 +231,7 @@
         """
         return self._request
 
-    def log_request(self, request):
+    def log_request(self, request, start_time):
         """Log a request in the access_log file.
         """
         if self.access_log is not None:
@@ -251,7 +240,7 @@
             else:
                 user = "-"
             now = time.time()
-            seconds = now - request.start_time
+            seconds = now - start_time
             timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(now))
 
             env = request.environ
@@ -520,6 +509,7 @@
         exceptions will be handled here.
         """
         self._set_request(request)
+        start_time = time.time()
         try:
             self.parse_request(request)
             output = self.try_publish(request, env.get('PATH_INFO', ''))
@@ -530,7 +520,7 @@
             # Some other exception, generate error messages to the logs, etc.
             output = self.finish_failed_request(request)
         output = self.filter_output(request, output)
-        self.log_request(request)
+        self.log_request(request, start_time)
         return output
 
     def publish(self, stdin, stdout, stderr, env):
@@ -540,7 +530,7 @@
         standard input, process it, and write the response to standard
         output.
         """
-        request = self.create_request(stdin, env)
+        request = HTTPRequest(stdin, env)
         output = self.process_request(request, env)
 
         # Output results from Response object

Deleted: trunk/quixote/upload.py
===================================================================
--- trunk/quixote/upload.py	2004-10-14 17:19:15 UTC (rev 25340)
+++ trunk/quixote/upload.py	2004-10-14 17:22:05 UTC (rev 25341)
@@ -1,377 +0,0 @@
-"""quixote.upload
-$HeadURL$
-$Id$
-
-Code for handling HTTP upload requests.  Provides HTTPUploadRequest, a
-subclass of HTTPRequest that is created when handling an HTTP request
-whose Content-Type is "multipart/form-data".  Also provides the Upload
-class, which is used as the form value for "file upload" variables.
-"""
-
-__revision__ = "$Id$"
-
-import os, string
-import errno
-from cgi import parse_header
-from rfc822 import Message
-from time import time, strftime, localtime
-
-from quixote.http_request import HTTPRequest
-from quixote.errors import RequestError
-from quixote.config import ConfigError
-
-CRLF = "\r\n"
-LF = "\n"
-
-
-def read_mime_part(file, boundary, lines=None, ofile=None):
-    """
-    Read lines from 'file' up to and including a MIME message boundary
-    derived from 'boundary'.  Return true if there is no more data to be
-    read from 'file', ie. we hit either a MIME outer boundary or EOF.
-
-    If 'lines' is supplied, each line is stripped of line-endings and
-    appended to 'lines'.  If 'ofile' is supplied, each line is written
-    to 'ofile' as-is (ie. with line-endings intact).  Neither the
-    boundary line nor a blank line preceding it (if any) will be
-    saved/written.  If neither 'lines' nor 'ofile' is supplied, the data
-    read is discarded.
-    """
-    # Algorithm based on read_lines_to_outerboundary() in cgi.py
-
-    next = "--" + boundary
-    last = "--" + boundary + "--"
-
-    # XXX reading arbitrary binary data (which is possible in a file
-    # upload) a line-at-a-time might be problematic.  Eg.  I have
-    # observed one .GDS file in the wild where the longest "line" was
-    # around 1 MB.  Most binary files that I looked at have reasonable
-    # "line" lengths though -- maximum 5-10k.  However, reading in
-    # fixed-size chunks would make spotting the MIME boundary tricky.
-    # One more reason why HTTP upload is stupid.
-
-    prev_delim = ""
-    while 1:
-        line = file.readline()
-
-        # Hit EOF -- nothing more to read.  (This should *not* happen
-        # in a well-formed MIME message, but let's assume the worst.)
-        if not line:
-            return True
-
-        # Strip (but remember) line ending.
-        if line[-2:] == CRLF:
-            line = line[:-2]
-            delim = CRLF
-        elif line[-1:] == LF:
-            line = line[:-1]
-            delim = LF
-        else:
-            delim = ""
-
-        # If we hit the boundary line, return now.  Forget the current
-        # line *and* the delimiter of the previous line -- in
-        # particular, we do not want to preserve the blank line that
-        # comes after an uploaded file's contents and the following
-        # boundary line.
-        if line == next:           # hit boundary, but more to come
-            return False
-        elif line == last:         # final boundary -- no more to read
-            return True
-
-        if lines is not None:
-            lines.append(line)
-        if ofile is not None:
-            ofile.write(prev_delim + line)
-        prev_delim = delim
-
-
-SAFE_CHARS = string.letters + string.digits + "-@&+=_., "
-_safe_trans = None
-
-def make_safe(s):
-    global _safe_trans
-    if _safe_trans is None:
-        _safe_trans = ["_"] * 256
-        for c in SAFE_CHARS:
-            _safe_trans[ord(c)] = c
-        _safe_trans = "".join(_safe_trans)
-
-    return s.translate(_safe_trans)
-
-
-class Upload:
-    """
-    Represents a single uploaded file.  Uploaded files live in the
-    filesystem, *not* in memory -- this is not a file-like object!  It's
-    just a place to store a couple of filenames.  Specifically, feel
-    free to access the following instance attributes:
-
-      orig_filename
-        the complete filename supplied by the user-agent in the
-        request that uploaded this file.  Depending on the browser,
-        this might have the complete path of the original file
-        on the client system, in the client system's syntax -- eg.
-        "C:\foo\bar\upload_this" or "/foo/bar/upload_this" or
-        "foo:bar:upload_this".
-      base_filename
-        the base component of orig_filename, shorn of MS-DOS,
-        Mac OS, and Unix path components and with "unsafe"
-        characters neutralized (see make_safe())
-      tmp_filename
-        where you'll actually find the file on the current system
-      content_type
-        the content type provided by the user-agent in the request
-        that uploaded this file.
-    """
-
-    def __init__(self, orig_filename, content_type=None):
-        if orig_filename:
-            self.orig_filename = orig_filename
-            bspos = orig_filename.rfind("\\")
-            cpos = orig_filename.rfind(":")
-            spos = orig_filename.rfind("/")
-            if bspos != -1:                 # eg. "\foo\bar" or "D:\ding\dong"
-                filename = orig_filename[bspos+1:]
-            elif cpos != -1:                # eg. "C:foo" or ":ding:dong:foo"
-                filename = orig_filename[cpos+1:]
-            elif spos != -1:                # eg. "foo/bar/baz" or "/tmp/blah"
-                filename = orig_filename[spos+1:]
-            else:
-                filename = orig_filename
-
-            self.base_filename = make_safe(filename)
-        else:
-            self.orig_filename = None
-            self.base_filename = None
-
-        self.content_type = content_type
-        self.tmp_filename = None
-
-    def __str__(self):
-        return str(self.orig_filename)
-
-    def __repr__(self):
-        return "<%s at %x: %s>" % (self.__class__.__name__, id(self), self)
-
-    def _open(self, dir):
-        """
-        Generate a unique filename in 'dir'.  Open and return a
-        writeable file object from it.
-        """
-        flags = os.O_WRONLY|os.O_CREAT|os.O_EXCL
-        try:
-            flags |= os.O_BINARY    # for Windows
-        except AttributeError:
-            pass
-        tstamp = strftime("%Y%m%d.%H%M%S", localtime(time()))
-        counter = 0
-        while 1:
-            filename = "upload.%s.%s" % (tstamp, counter)
-            filename = os.path.join(dir, filename)
-            try:
-                fd = os.open(filename, flags)
-            except OSError, err:
-                if err.errno == errno.EEXIST:
-                    # Filename collision -- try again
-                    counter += 1
-                else:
-                    # Bomb on any other error.
-                    raise
-            else:
-                # Opened the file just fine; it now exists so no other
-                # process or thread will be able to grab that filename.
-                break
-
-        # Wrap a file object around the file descriptor.
-        return (os.fdopen(fd, "wb"), filename)
-
-    def receive(self, file, boundary, dir):
-        (ofile, filename) = self._open(dir)
-        done = read_mime_part(file, boundary, ofile=ofile)
-        ofile.close()
-        self.tmp_filename = filename
-        return done
-
-    def get_size(self):
-        """get_size() : int
-        Return the size of the file, measured in bytes, or None if
-        the file doesn't exist.
-        """
-        stats = os.stat(self.tmp_filename)
-        return stats.st_size
-
-class CountingFile:
-    """A file-like object that records the number of bytes read
-    from the underlying file.  Ignores seek(), because it's only
-    used by HTTPUploadRequest on an unseekable file (stdin).
-    """
-
-    def __init__(self, file):
-        self.__file = file
-        self.__bytesread = 0
-
-    def read(self, nbytes):
-        data = self.__file.read(nbytes)
-        self.__bytesread += len(data)
-        return data
-
-    def readline(self):
-        line = self.__file.readline()
-        self.__bytesread += len(line)
-        return line
-
-    def get_bytesread(self):
-        return self.__bytesread
-
-
-class HTTPUploadRequest(HTTPRequest):
-    """
-    Represents a single HTTP request with Content-Type
-    "multipart/form-data", which is used for HTTP uploads.  (It's
-    actually possible for any HTML form to specify an encoding type of
-    "multipart/form-data", even if there are no file uploads in that
-    form.  In that case, you'll still get an HTTPUploadRequest object --
-    but since this is a subclass of HTTPRequest, that shouldn't cause
-    you any problems.)
-
-    When processing the upload request, any uploaded files are stored
-    under a temporary filename in the directory specified by the
-    'upload_dir' instance attribute (which is normally set, by
-    Publisher, from the UPLOAD_DIR configuration variable).
-    HTTPUploadRequest then creates an Upload object which contains the
-    various filenames for this upload.
-
-    Other form variables are stored as usual in the 'form' dictionary,
-    to be fetched later with get_form_var().  Uploaded files can also be
-    accessed via get_form_var(), which returns the Upload object created
-    at upload-time, rather than a string.
-
-    Eg. if your upload form contains this:
-      <input type="file" name="upload">
-
-    then, when processing the form, you might do this:
-      upload = request.get_form_var("upload")
-
-    after which you could open the uploaded file immediately:
-      file = open(upload.tmp_filename)
-
-    or move it to a more permanent home before doing anything with it:
-      permanent_name = os.path.join(permanent_upload_dir,
-                                    upload.base_filename)
-      os.rename(upload.tmp_filename, permanent_name)
-    """
-
-    def __init__(self, stdin, environ, content_type=None):
-        HTTPRequest.__init__(self, stdin, environ, content_type)
-
-        self.upload_dir = None
-        self.upload_dir_mode = 0775
-
-    def set_upload_dir(self, dir, mode=None):
-        self.upload_dir = dir
-        if mode is not None:
-            self.upload_dir_mode = mode
-
-    def parse_content_type(self):
-        full_ctype = self.get_header('Content-Type')
-        if full_ctype is None:
-            raise RequestError("no Content-Type header")
-
-        (ctype, ctype_params) = parse_header(full_ctype)
-        boundary = ctype_params.get('boundary')
-
-        if not (ctype == "multipart/form-data" and boundary):
-            raise RequestError("expected Content-Type: multipart/form-data "
-                               "with a 'boundary' parameter: got %r"
-                               % full_ctype)
-
-        return (ctype, boundary)
-
-    def parse_content_disposition(self, full_cdisp):
-        (cdisp, cdisp_params) = parse_header(full_cdisp)
-        name = cdisp_params.get("name")
-
-        if not (cdisp == "form-data" and name):
-            raise RequestError("expected Content-Disposition: form-data "
-                               "with a 'name' parameter: got %r" % full_cdisp)
-
-        return (name, cdisp_params.get("filename"))
-
-    def check_upload_dir(self):
-        if not os.path.isdir(self.upload_dir):
-            print "creating %s with mode %o" % (self.upload_dir,
-                                                self.upload_dir_mode)
-            os.mkdir(self.upload_dir, self.upload_dir_mode)
-
-    def handle_upload(self, name, filename, file, boundary, content_type):
-        if self.upload_dir is None:
-            raise ConfigError("upload_dir not set")
-        upload = Upload(filename, content_type)
-        self.check_upload_dir()
-        done = upload.receive(file, boundary, self.upload_dir)
-        self.add_form_value(name, upload)
-        return done
-
-    def handle_regular_var(self, name, file, boundary):
-        lines = []
-        done = read_mime_part(file, boundary, lines=lines)
-        if len(lines) == 1:
-            value = lines[0]
-        else:
-            value = "\n".join(lines)
-        self.add_form_value(name, value)
-        #form_vars.append((name, value))
-        return done
-
-    def parse_body(self, file, boundary):
-        total_bytes = 0                 # total bytes read from 'file'
-        done = False
-        while not done:
-            headers = Message(file)
-            cdisp = headers.get('content-disposition')
-            if not cdisp:
-                raise RequestError("expected Content-Disposition header "
-                                   "in body sub-part")
-            (name, filename) = self.parse_content_disposition(cdisp)
-            if filename:
-                content_type = headers.get('content-type')
-                done = self.handle_upload(name, filename, file,
-                                          boundary, content_type)
-            else:
-                done = self.handle_regular_var(name, file, boundary)
-
-    def check_length_read(self, file):
-        # Parse Content-Length header.
-        # XXX if we want to worry about disk free space, this should
-        # be done *before* parsing the body!
-        clen = self.get_header("Content-Length")
-        if clen is not None:
-            clen = int(clen)
-
-        total_bytes = file.get_bytesread()
-        if total_bytes != clen:
-            raise RequestError(
-                "upload request length mismatch: expected %d bytes, got %d"
-                % (clen, total_bytes))
-
-    def process_inputs(self):
-        self.start_time = time()
-
-        # Parse Content-Type header -- mainly to get the 'boundary'
-        # parameter.  Barf if not there or unexpected type.
-        (ctype, boundary) = self.parse_content_type()
-
-        # The meat of the body starts after the first occurrence of
-        # the boundary, so read up to that point.
-        file = CountingFile(self.stdin)
-        read_mime_part(file, boundary)
-
-        # Parse the parts of the message, ie. the form variables.  Some of
-        # these will presumably be "file upload" variables, so need to be
-        # treated specially.
-        self.parse_body(file, boundary)
-
-        # Ensure that we read exactly as many bytes as were promised
-        # by the Content-Length header.
-        self.check_length_read(file)