source: ceda_http_fileserver/trunk/ceda_http_fileserver/ceda/server/wsgi/fileserver/app.py @ 7727

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/ceda_http_fileserver/trunk/ceda_http_fileserver/ceda/server/wsgi/fileserver/app.py@7727
Revision 7727, 27.4 KB checked in by pjkersha, 11 years ago (diff)

0.4.0 Release

  • Adds trailing slash to sub-dir links in dir listing.
  • Filled out epydoc
  • Property svn:keywords set to Id
Line 
1"""CEDA (Centre for Environmental Data Archival) File Server WSGI Application
2module
3"""
4__author__ = "P J Kershaw"
5__date__ = "11/06/10"
6__copyright__ = "(C) 2010 Science and Technology Facilities Council"
7__license__ = """http://www.apache.org/licenses/LICENSE-2.0"""
8__contact__ = "Philip.Kershaw@stfc.ac.uk"
9__revision__ = '$Id$'
10#   Copyright (c) 2006-2007 Open Source Applications Foundation
11#
12#   Licensed under the Apache License, Version 2.0 (the "License");
13#   you may not use this file except in compliance with the License.
14#   You may obtain a copy of the License at
15#
16#       http://www.apache.org/licenses/LICENSE-2.0
17#
18#   Unless required by applicable law or agreed to in writing, software
19#   distributed under the License is distributed on an "AS IS" BASIS,
20#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21#   See the License for the specific language governing permissions and
22#   limitations under the License.
23import httplib
24import urllib
25import os
26import traceback
27import mimetypes
28import errno
29import re
30import logging
31
32log = logging.getLogger(__name__)
33   
34
35class FileResponseError(Exception):
36    """Base exception type for exceptions raised from FileResponse class
37    instances"""
38   
39class InvalidRangeRequest(FileResponseError):
40    """Raise for an invalid byte range requested"""
41    def __init__(self, *arg, **kw):
42        FileResponseError.__init__(self, *arg, **kw)
43        if len(arg) > 1:
44            self.contentRangeHdr = arg[1]
45        else:
46            self.contentRangeHdr = None
47           
48   
49class InvalidRangeRequestSyntax(FileResponseError):
50    """Raise for invalid range request syntax"""
51   
52   
53class FileResponse(object):
54    """Helper class creates iterable response based on a given block size"""
55    DEFAULT_BLK_SIZE = 1024
56    BYTE_RANGE_PREFIX = 'bytes='
57    BYTE_RANGE_SEP = '-'
58    CONTENT_RANGE_FIELDNAME = 'Content-range'
59    CONTENT_RANGE_FORMAT_STR = "bytes %d-%d/%d"
60    INVALID_CONTENT_RANGE_FORMAT_STR = "bytes */%d"
61   
62    __slots__ = (
63        'fileObj',
64        'fileSize',
65        '__blkSize',
66        'readLengths',
67        'contentLength',
68        'contentRange',
69        'contentRangeHdr',
70    )
71   
72    def __init__(self, filePath, requestRange=None, blkSize=DEFAULT_BLK_SIZE):
73        '''Open a file and set the blocks for reading, any input range set and
74        the response size
75        '''
76        self.fileObj = None
77        self.fileSize = None
78       
79        # the length of the content to return - this will be different to the
80        # file size if the client a byte range header field setting
81        self.contentLength = 0
82       
83        # None unless a valid input range was given
84        self.contentRange = None
85       
86        # Formatted for HTTP content range header field
87        self.contentRangeHdr = None
88
89        # This will call the relevant set property method
90        self.blkSize = blkSize
91       
92        # Array of blocks lengths for iterator to use to read the file
93        self.readLengths = []
94       
95        try:
96            self.fileObj = open(filePath, 'rb')
97            log.debug('Opened file %s', filePath)
98           
99        except IOError:
100            log.error('Failed to open file %r: %s', filePath, 
101                      traceback.format_exc()) 
102            raise   
103       
104        self.fileSize = os.path.getsize(filePath)
105        if requestRange is not None:
106           
107            # Prepare a content range header in case the range specified is
108            # invalid
109            contentRangeHdr = (self.__class__.CONTENT_RANGE_FIELDNAME,
110                               self.__class__.INVALID_CONTENT_RANGE_FORMAT_STR %
111                               self.fileSize)
112                               
113            try:
114                # Remove 'bytes=' prefix
115                rangeVals = requestRange.split(
116                                        self.__class__.BYTE_RANGE_PREFIX)[-1]
117                                       
118                # Convert into integers taking into account that a value may be
119                # absent
120                startStr, endStr = rangeVals.split(
121                                                self.__class__.BYTE_RANGE_SEP)
122                start = int(startStr or 0)
123                end = int(endStr or self.fileSize - 1)
124            except ValueError:
125                raise InvalidRangeRequestSyntax('Invalid format for request '
126                                                'range %r' % requestRange)
127           
128            # Verify range bounds
129            if start > end:
130                raise InvalidRangeRequest('Range start index %r is greater '
131                                          'than the end index %r for file %r' % 
132                                          (start, end, filePath),
133                                          contentRangeHdr)
134            elif start < 0:
135                raise InvalidRangeRequest('Range start index %r is less than '
136                                          'zero' % start,
137                                          contentRangeHdr) 
138            elif end >= self.fileSize:
139                # This is not an error -
140                # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
141                log.warning('Range end index %r is greater than the length %r '
142                            'of the requested resource %r - reseting to %r',
143                            end, self.fileSize, filePath, self.fileSize - 1)
144                end = self.fileSize - 1
145               
146            # Set the total content length to return
147            self.contentLength = end + 1 - start
148            self.contentRange = (start, end)
149            self.contentRangeHdr = (
150                self.__class__.CONTENT_RANGE_FIELDNAME, 
151                self.__class__.CONTENT_RANGE_FORMAT_STR % 
152                                        (self.contentRange + (self.fileSize,))
153            )
154            self.fileObj.seek(start)
155        else:           
156            # Set the total content length to return
157            self.contentLength = self.fileSize
158                 
159        nReads = self.contentLength / self.blkSize
160        lastReadLen = self.contentLength % self.blkSize
161        self.readLengths = [self.blkSize] * nReads
162        if lastReadLen > 0:
163            nReads += 1
164            self.readLengths.append(lastReadLen)
165       
166    def __iter__(self):
167        '''Read the file a block at a time'''
168        while len(self.readLengths) > 0:
169            output = self.fileObj.read(self.readLengths[-1])
170            self.readLengths.pop()
171            yield output
172#        output = '\n'
173#        while len(output) is not 0:
174#            output = self.fileObj.read(self.end+1)
175#            yield output
176
177    def _getBlkSize(self):
178        return self.__blkSize
179   
180    def _setBlkSize(self, value):
181        self.__blkSize = int(value)
182        if self.__blkSize < 0:
183            raise ValueError('Expecting positive integer value for block size '
184                             'attribute')
185           
186    blkSize = property(fget=_getBlkSize, fset=_setBlkSize,
187                        doc="block size for reading the file in the iterator "
188                            "and returning a response")
189
190
191# Map HTTP status code to code + standard message string
192_statusCode2Msg = lambda code: "%d %s" % (code, 
193                    httplib.responses.get(code, httplib.INTERNAL_SERVER_ERROR))
194   
195
196class FileServerApp(object):
197    """Application to serve static content
198   
199    @cvar PARAM_PREFIX: default configuration parameters prefix
200    @type PARAM_PREFIX: string
201    @cvar FILE_SYS_ROOT_PATH_OPTNAME: option name for setting the file system
202    root from which files will be exposed by this service
203    @type FILE_SYS_ROOT_PATH_OPTNAME: string
204    @cvar DEFAULT_READ_BLK_SIZE: default block size for read operations
205    @type DEFAULT_READ_BLK_SIZE: int
206    @cvar DEFAULT_WRITE_BLK_SIZE: default block size for write operations
207    @type DEFAULT_WRITE_BLK_SIZE: int
208    @cvar DEFAULT_CONTENT_TYPE: default HTML content type for responses
209    @type DEFAULT_CONTENT_TYPE: string
210    @cvar DEFAULT_MOUNT_POINT: default URI path to mount the application
211    @type DEFAULT_MOUNT_POINT: string
212    @cvar DEFAULT_HTTP_METHOD_MAP: dictionary maps HTTP method string keys to
213    methods in this class
214    @type DEFAULT_HTTP_METHOD_MAP: dict
215    @cvar FILE_ACCESS_ERROR_MAP: Map HTTP status string to a given file system
216    file access error
217    @type FILE_ACCESS_ERROR_MAP: dict
218    """
219    PARAM_PREFIX = 'fileserver.'
220    FILE_SYS_ROOT_PATH_OPTNAME = 'fileSysRootPath'
221   
222    DEFAULT_READ_BLK_SIZE = 1024
223    DEFAULT_WRITE_BLK_SIZE = 1024
224    DEFAULT_CONTENT_TYPE = 'text/plain; charset=utf-8'
225    DEFAULT_MOUNT_POINT = ''
226   
227    statusCode2Msg = staticmethod(_statusCode2Msg)
228   
229    FILE_ACCESS_ERROR_MAP = {
230         errno.ENOENT: _statusCode2Msg(httplib.NOT_FOUND),
231         errno.EACCES: _statusCode2Msg(httplib.FORBIDDEN)
232    }
233   
234    __slots__ = (
235        '__readBlkSize',
236        '__writeBlkSize',
237        '__httpMethodMap',
238        '__fileSysRoot',
239        '__mountPoint',
240        '__mimeTypes',
241        '__fileFilterRegEx'
242    )
243   
244    def __init__(self, 
245                 fileSysRootPath, 
246                 mountPoint=DEFAULT_MOUNT_POINT,
247                 readBlkSize=DEFAULT_READ_BLK_SIZE,
248                 writeBlkSize=DEFAULT_WRITE_BLK_SIZE,
249                 fileFilterRegEx=None,
250                 mimeTypesFilePath=None):
251        '''Initialise all attributes here
252       
253        @param fileSysRootPath: file system root from which files will be
254        exposed by this service
255        @type fileSysRootPath: string
256        @param mountPoint: URI path to mount the application
257        @type mountPoint: string
258        @param readBlkSize: block size for read operations
259        @type mountPoint: string
260        @param writeBlkSize: block size for write operations
261        @type writeBlkSize: int
262        @param fileFilterRegEx:
263        @type fileFilterRegEx: string / None type
264        @param mimeTypesFilePath: file path for file defining the MIME types
265        for files served by the application
266        @type mimeTypesFilePath: string / None type
267        '''
268       
269        self.__readBlkSize = None
270        self.__writeBlkSize = None
271        self.__httpMethodMap = None
272        self.__mountPoint = None
273        self.__fileSysRoot = None
274        self.__fileFilterRegEx = None
275       
276        # ... Then set default values here
277        self.fileSysRoot = os.path.abspath(
278                                os.path.expandvars(
279                                    os.path.expanduser(fileSysRootPath)
280                                    )
281                                )
282       
283        self.mountPoint = mountPoint
284       
285        # Set from property methods to apply validation - referencing
286        # self.__class__ means derived class could make an alternative setting
287       
288        # Block size for PUT or POST operations
289        self.readBlkSize = readBlkSize
290
291        # Block size for GET operation
292        self.writeBlkSize = writeBlkSize
293       
294        # Map HTTP method name to a method of this class
295        self.httpMethodMap = self.__class__.DEFAULT_HTTP_METHOD_MAP
296       
297        self.fileFilterRegEx = fileFilterRegEx
298       
299        # MIME types
300        self.mimeTypes = mimetypes.MimeTypes()
301       
302        if mimeTypesFilePath is not None:
303            self.mimeTypes.read(mimeTypesFilePath)
304
305    def _getFileFilterRegEx(self):
306        return self.__fileFilterRegEx
307
308    def _setFileFilterRegEx(self, value):
309        if value is None:
310            self.__fileFilterRegEx = value
311       
312        elif isinstance(value, basestring):
313            self.__fileFilterRegEx = re.compile(value)
314        else: 
315            raise TypeError('Expecting string or None type for '
316                            '"fileFilterRegEx" attribute; got %r' % type(value))
317       
318    fileFilterRegEx = property(_getFileFilterRegEx, _setFileFilterRegEx, 
319                               doc="Regular expression to filter out matching "
320                                   "files from being served")
321
322    def _getMimeTypes(self):
323        return self.__mimeTypes
324
325    def _setMimeTypes(self, value):
326        self.__mimeTypes = value
327
328    mimeTypes = property(_getMimeTypes, _setMimeTypes, 
329                         doc="Mime types object for determining response "
330                             "content type from file extension")
331
332    def _getFileSysRoot(self):
333        return self.__fileSysRoot
334
335    def _setFileSysRoot(self, value):
336        if not isinstance(value, basestring):
337            raise TypeError('Expecting string type for "fileSysRoot" '
338                            'attribute; got %r' % type(value))
339
340        self.__fileSysRoot = value
341
342    fileSysRoot = property(_getFileSysRoot, _setFileSysRoot, 
343                           doc="Root directory in file system from which to "
344                               "serve files")
345
346    def _getMountPoint(self):
347        return self.__mountPoint
348
349    def _setMountPoint(self, value):
350        if not isinstance(value, basestring):
351            raise TypeError('Expecting string type for "mountPoint" '
352                            'attribute; got %r' % type(value))
353           
354        self.__mountPoint = value
355
356    mountPoint = property(_getMountPoint, _setMountPoint, 
357                          doc="Mount point - root path for URI to serve files "
358                              "from")
359
360    def _getReadBlkSize(self):
361        return self.__readBlkSize
362
363    def _setReadBlkSize(self, value):
364        self.__readBlkSize = int(value)
365        if self.__readBlkSize < 0:
366            raise ValueError('Expecting positive integer value for block size '
367                             'attribute')
368
369    readBlkSize = property(_getReadBlkSize, _setReadBlkSize, 
370                           doc="Block size for read files for upload")
371
372    def _getWriteBlkSize(self):
373        return self.__writeBlkSize
374
375    def _setWriteBlkSize(self, value):
376        self.__writeBlkSize = int(value)
377        if self.__writeBlkSize < 0:
378            raise ValueError('Expecting positive integer value for block size '
379                             'attribute')
380       
381    writeBlkSize = property(_getWriteBlkSize, _setWriteBlkSize, 
382                            doc="Block size for writing files for download")
383
384    def _getHttpMethodMap(self):
385        return self.__httpMethodMap
386
387    def _setHttpMethodMap(self, value):
388        if not isinstance(value, dict):
389            raise TypeError('Expecting dict type for HTTP method map '
390                            'attribute; got %r' % type(value))
391           
392        for name, method in value.items():
393            if not isinstance(name, basestring):
394                raise TypeError('Expecting string type for HTTP method name; '
395                                'got %r' % type(name))
396                   
397            if not callable(method):
398                raise TypeError('Expecting callable for HTTP method ; got %r' % 
399                                type(method))
400                               
401        self.__httpMethodMap = value.copy()
402
403    httpMethodMap = property(_getHttpMethodMap, _setHttpMethodMap, 
404                             doc="Dictionary mapping HTTP method names to "
405                                 "methods of this class")
406
407    @classmethod
408    def app_factory(cls, global_conf, prefix=PARAM_PREFIX, **app_conf): 
409        """Function following Paste app factory signature
410       
411        @type global_conf: dict       
412        @param global_conf: PasteDeploy global configuration dictionary
413        @type prefix: basestring
414        @param prefix: prefix for configuration items
415        @type app_conf: dict       
416        @param app_conf: PasteDeploy application specific configuration
417        dictionary
418        """
419        # Filter based on prefix
420        if prefix:
421            prefixLen = len(prefix)
422            kw = dict([(k[prefixLen:], v) for k, v in app_conf.items() 
423                       if k.startswith(prefix)])
424           
425        # This app 
426        fileSysRootPath = kw.pop(cls.FILE_SYS_ROOT_PATH_OPTNAME)         
427        app = cls(fileSysRootPath, **kw)
428
429        return app
430   
431    def handler(self, environ, start_response):
432        """Translate URI path to local file system path and map correct
433        callback from the HTTP request method
434       
435        @param environ: environment dict
436        @type environ: dict-like object
437        @param start_response: WSGI start response function
438        @type start_response: function/callable
439        @return: response - file or directory listing
440        @rtype: iterable type
441        """
442        if self.mountPoint:
443            splitPath = environ['PATH_INFO'].split(self.mountPoint, 1)
444            if len(splitPath) < 2:
445                log.error('Error splitting environ["PATH_INFO"]=%r with mount '
446                          'point %r: returning 404 Not Found response',
447                          environ['PATH_INFO'], self.mountPoint)
448               
449                status = FileServerApp.statusCode2Msg(httplib.NOT_FOUND)
450                start_response(status,
451                               [('Content-length', str(len(status))),
452                                ('Content-type', 'text/plain')])
453                return [status]
454             
455            relativeURI = splitPath[1]
456        else:
457            relativeURI = environ['PATH_INFO']
458           
459        fileSysSubDir = urllib.url2pathname(relativeURI)
460       
461        # This if statement stops os.path.join doing a join with an
462        # absolute path '/...'.  If this is done, the first argument is
463        # obliterated and the result is '/' exposing the root file system to
464        # the web client!!
465        if fileSysSubDir.startswith('/'):
466            fileSysSubDir = fileSysSubDir[1:]
467               
468       
469        requestMethodName = environ['REQUEST_METHOD']
470        requestMethod = self.httpMethodMap.get(requestMethodName)
471        if requestMethod is None:
472            response = ('%r HTTP request method is not supported' % 
473                        requestMethodName)
474            status = FileServerApp.statusCode2Msg(httplib.METHOD_NOT_ALLOWED)
475            start_response(status,
476                           [('Content-length', str(len(response))),
477                            ('Content-type', 'text/plain')])
478            return [response]
479           
480        return requestMethod(self, fileSysSubDir, environ, start_response)
481       
482    def do_get(self, fileSysSubDir, environ, start_response):
483        """HTTP GET callback
484       
485        @param fileSysSubDir: sub-directory from file system root mount point
486        to do GET for.
487        @type fileSysSubDir: string
488        @param environ: environment dict
489        @type environ: dict-like object
490        @param start_response: WSGI start response function
491        @type start_response: function/callable
492        @return: response - file or directory listing
493        @rtype: iterable type
494        """
495        filePath = os.path.join(self.fileSysRoot, fileSysSubDir)
496       
497        scriptName = environ.get('SCRIPT_NAME', '')
498       
499        # Apply filter to filter out unwanted content
500        pat = self.fileFilterRegEx
501        if pat is not None:
502            _allowFile = lambda filename: pat.match(filename) is None
503        else:
504            _allowFile = lambda filename: True
505       
506        # Check for a HTTP Range request
507        requestRange = environ.get('HTTP_RANGE')
508       
509        if not _allowFile(fileSysSubDir):
510            status = FileServerApp.statusCode2Msg(httplib.NOT_FOUND)
511            response = status
512            start_response(status, 
513                           [('Content-Type', 'text/plain'),
514                            ('Content-length', str(len(response)))])
515            return [response]
516       
517        elif not os.path.exists(filePath):
518            response = status = FileServerApp.statusCode2Msg(httplib.NOT_FOUND)
519            log.error('Requesting URI path %r, corresponding file path %r '
520                      'doesn\'t exist: returning %s response',
521                      environ['PATH_INFO'],
522                      filePath,
523                      status)
524            start_response(status, 
525                           [('Content-Type', 'text/plain'),
526                            ('Content-Length', str(len(response)))])
527           
528            return [response]
529           
530        elif os.path.isdir(filePath):
531            dirContents = os.listdir(filePath)
532            mountPoint = self.mountPoint
533           
534            # Prepend '/' to pattern match test - it makes it easier to write
535            # a generic regular expression for filtering out both
536            # sub-directories - see above - and file names as here
537            lines = []
538            for filename in dirContents:
539                if _allowFile('/' + filename):
540                    absFilePath = os.path.join(filePath, filename)
541                    if os.path.isdir(absFilePath):
542                        filename += '/'
543                       
544                    uri = urllib.pathname2url(os.path.join(fileSysSubDir, 
545                                                           filename))
546                   
547                    line = '<a href="%s%s/%s">%s</a>' % (scriptName,
548                                                         mountPoint,
549                                                         uri,
550                                                         filename)
551                    lines.append(line)
552
553            response = ('<html><head/><body>%s</body></html>' % 
554                        '<br>'.join(lines))
555   
556            if requestRange is not None:
557                log.warning('Requested path %r will return a HTML type '
558                            'response listing directory content: ignoring HTTP '
559                            'range request %r', (environ['PATH_INFO'],
560                                                 requestRange))
561               
562            start_response(FileServerApp.statusCode2Msg(httplib.OK), 
563                           [('Cache-Control','no-cache'), 
564                            ('Pragma','no-cache'),
565                            ('Content-Type', 'text/html; charset=utf-8'),
566                            ('Content-Length', str(len(response)))])
567           
568            return [response]
569        else:
570            try:
571                fileResponse = FileResponse(filePath,
572                                            blkSize=self.writeBlkSize,
573                                            requestRange=requestRange)
574            except IOError, e:
575                # Map file access error to a HTTP response code
576                status = FileServerApp.mapFileAccessError2HttpStatus(e.errno)
577                response = status
578                start_response(status, 
579                               [('Content-Type', 'text/plain'),
580                                ('Content-length', str(len(response)))])
581                return [response]
582             
583            except InvalidRangeRequestSyntax, e:
584                # Byte range was requested for extraction but it was badly
585                # formatted
586                response = "%s\n" % e
587                log.error(response)
588                status = FileServerApp.statusCode2Msg(httplib.BAD_REQUEST)
589                start_response(status, 
590                               [('Content-Type', 'text/plain'),
591                                ('Content-length', str(len(response)))])
592                return [response]
593               
594            except InvalidRangeRequest, e:
595                # Byte range was requested for extraction but it's invalid for
596                # the requested file
597                response = "%s\n" % e
598                log.error(response)
599                status = FileServerApp.statusCode2Msg(
600                                        httplib.REQUESTED_RANGE_NOT_SATISFIABLE)
601                start_response(status, 
602                               [('Content-Type', 'text/plain'),
603                                ('Content-length', str(len(response))),
604                                 e.contentRangeHdr])
605                return [response]
606           
607            headers = [
608                ('Cache-Control','no-cache'), 
609                ('Pragma','no-cache'), 
610                ('Content-Length', str(fileResponse.contentLength)),
611                ('Content-Type', self.getContentType(environ['PATH_INFO']))
612            ]
613                                       
614            if requestRange:
615                status = FileServerApp.statusCode2Msg(httplib.PARTIAL_CONTENT)
616                headers.append(fileResponse.contentRangeHdr)
617            else:
618                status = FileServerApp.statusCode2Msg(httplib.OK)
619                               
620            start_response(status, headers)
621            return fileResponse
622       
623    def do_put(self, fileSysSubDir, environ, start_response):
624        """HTTP PUT callback
625       
626        @param environ: environment dict
627        @type environ: dict-like object
628        @param start_response: WSGI start response function
629        @type start_response: function/callable
630        @return: response
631        @rtype: iterable type
632        """
633        try:
634            filePath = os.path.join(self.fileSysRoot, fileSysSubDir)
635            fileObj = open(filePath, 'wb')
636            log.debug('opened file for writing %s', filePath)
637           
638        except IOError, e:
639            log.error('failed to open file for writing %r', filePath)
640            status = FileServerApp.mapFileAccessError2HttpStatus(e.errno)
641            response = status
642            start_response(status, 
643                           [('Content-Type', 'text/plain'),
644                            ('Content-length', str(len(response)))])
645            return [response]
646       
647        inputLength = environ['CONTENT_LENGTH']
648        inputStream = environ['wsgi.input']
649        nReads = inputLength / self.readBlkSize
650        remainder = inputLength % self.readBlkSize
651        readArray = [self.readBlkSize] * nReads
652        if remainder > 0:
653            nReads += 1
654            readArray.append(remainder)
655           
656        for length in readArray:
657            inputBlk = inputStream.read(length)
658            fileObj.write(inputBlk)
659           
660        return []
661
662    DEFAULT_HTTP_METHOD_MAP = {
663        'GET':          do_get, 
664        'PUT':          do_put, 
665    }
666   
667    @classmethod
668    def mapFileAccessError2HttpStatus(cls, errno):
669        """Map file access error to a standard HTTP response status message
670       
671        @param errno: error number returned fcrom file system
672        @type errno: int
673        @return: message string corresponding to error number
674        @rtype: string
675        """
676        return cls.FILE_ACCESS_ERROR_MAP.get(errno, 
677                            cls.statusCode2Msg(httplib.INTERNAL_SERVER_ERROR))
678                                                       
679    def getContentType(self, path_info):
680        """Make a best guess at the content type for the given path
681       
682        @param path_info: path to guess type for
683        @type path_info: string
684        @return: HTML content type for the given path
685        @rtype: string
686        """
687        contentType = self.mimeTypes.guess_type(path_info)[0]
688        if contentType is not None:
689            return contentType
690        else:
691            return self.__class__.DEFAULT_CONTENT_TYPE
692           
693    def __call__(self, environ, start_response):
694        """Make objects of this class callable to fit WSGI signature
695       
696        @param environ: environment dict
697        @type environ: dict-like object
698        @param start_response: WSGI start response function
699        @type start_response: function/callable
700        @return: response
701        @rtype: iterable type
702        """
703        return self.handler(environ, start_response)
704
Note: See TracBrowser for help on using the repository browser.