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)