Changeset 7009 for ceda_http_fileserver


Ignore:
Timestamp:
14/06/10 11:59:27 (9 years ago)
Author:
pjkersha
Message:

Incomplete - task 9: Data Browser Replacement - Refactoring to include:

  • block based reading and writing of requests/responses
  • Fix to make WSGI compliant input read
  • configurable HTTP method support


Next:

  • URL encoding of file names for GET
  • USe mimetypes package
Location:
ceda_http_fileserver/trunk/ceda_http_fileserver/ceda/server/wsgi/fileserver
Files:
5 added
2 edited

Legend:

Unmodified
Added
Removed
  • ceda_http_fileserver/trunk/ceda_http_fileserver/ceda/server/wsgi/fileserver/app.py

    r6999 r7009  
    2323 
    2424from urlparse import urlparse 
    25 import os, sys 
     25import httplib 
     26import os 
    2627import logging 
    2728 
    28 logger = logging.getLogger(__name__) 
     29log = logging.getLogger(__name__) 
    2930 
    3031# Content type sources taken from http://en.wikipedia.org/wiki/MIME_type 
     
    8283     
    8384class FileResponse(object): 
    84     readsize = 1024 
    85      
    86     def __init__(self, f, filename): 
    87         self.size = os.path.getsize(filename) 
    88         self.f = f 
     85    DEFAULT_READ_SIZE = 1024 
     86     
     87    def __init__(self, readSize=DEFAULT_READ_SIZE): 
     88        self.fileObj = None 
     89        self.fileSize = None 
     90        self.readSize = readSize 
     91 
     92    def __call__(self, fileObj, fileName): 
     93        self.fileSize = os.path.getsize(fileName) 
     94        self.fileObj = fileObj         
    8995         
    9096    def __iter__(self): 
    9197        output = '\n' 
    9298        while len(output) is not 0: 
    93             output = self.f.read(self.readsize) 
     99            output = self.fileObj.read(self.readSize) 
    94100            yield output 
     101 
     102    def _getReadSize(self): 
     103        return self.__readSize 
     104     
     105    def _setReadSize(self, value): 
     106        self.__readSize = int(value) 
     107        if self.__readSize < 0: 
     108            raise ValueError('Expecting positive integer value for block size ' 
     109                             'attribute') 
     110             
     111    readSize = property(fget=_getReadSize, fset=_setReadSize, 
     112                        doc="block size reading the file in the iterator and " 
     113                            "returning a response") 
    95114 
    96115 
     
    98117    """Application to serve static content""" 
    99118    PARAM_PREFIX = 'fileserver.' 
    100      
    101     def __init__(self, root_path='', mount_point=None): 
     119    DEFAULT_READ_BLK_SIZE = 1024 
     120    DEFAULT_WRITE_BLK_SIZE = 1024 
     121     
     122    __slots__ = ( 
     123        '__fileResponse', 
     124        '__readBlkSize', 
     125        '__httpMethodMap', 
     126        'path', 
     127        'mount_point' 
     128    ) 
     129     
     130    def __init__(self, root_path, mount_point=None): 
     131         
     132        self.__fileResponse = FileResponse() 
     133        self.__readBlkSize = None 
     134        self.__httpMethodMap = None 
     135         
    102136        self.path = os.path.abspath(os.path.expanduser(root_path)) 
    103137        self.mount_point = mount_point 
     138         
     139        # Set from property methods to apply validation - referencing  
     140        # self.__class__ means derived class could make an alternative setting 
     141         
     142        # Block size for PUT or POST operations 
     143        self.readBlkSize = self.__class__.DEFAULT_READ_BLK_SIZE 
     144 
     145        # Block size for GET operation 
     146        self.writeBlkSize = self.__class__.DEFAULT_WRITE_BLK_SIZE 
     147         
     148        # Map HTTP method name to a method of this class 
     149        self.httpMethodMap = self.__class__.DEFAULT_HTTP_METHOD_MAP 
     150 
     151    def _getReadBlkSize(self): 
     152        return self.__readBlkSize 
     153 
     154    def _setReadBlkSize(self, value): 
     155        self.__readBlkSize = int(value) 
     156        if self.__readBlkSize < 0: 
     157            raise ValueError('Expecting positive integer value for block size ' 
     158                             'attribute') 
     159 
     160    readBlkSize = property(_getReadBlkSize, _setReadBlkSize,  
     161                           doc="ReadBlkSize's Docstring") 
     162 
     163    def _getWriteBlkSize(self): 
     164        return self.__fileResponse.readSize 
     165 
     166    def _setWriteBlkSize(self, value): 
     167        self.__fileResponse.readSize = value 
     168         
     169    writeBlkSize = property(_getWriteBlkSize, _setWriteBlkSize,  
     170                            doc="WriteBlkSize's Docstring") 
     171 
     172    def _getHttpMethodMap(self): 
     173        return self.__httpMethodMap 
     174 
     175    def _setHttpMethodMap(self, value): 
     176        if not isinstance(value, dict): 
     177            raise TypeError('Expecting dict type for HTTP method map ' 
     178                            'attribute; got %r' % type(value)) 
     179             
     180        for name, method in value.items(): 
     181            if not isinstance(name, basestring): 
     182                raise TypeError('Expecting string type for HTTP method name; ' 
     183                                'got %r' % type(name)) 
     184                     
     185            if not callable(method): 
     186                raise TypeError('Expecting callable for HTTP method ; got %r' %  
     187                                type(method)) 
     188                                 
     189        self.__httpMethodMap = value.copy() 
     190 
     191    httpMethodMap = property(_getHttpMethodMap, _setHttpMethodMap,  
     192                             doc="HttpMethodMap's Docstring") 
    104193 
    105194    @classmethod 
     
    134223        serve_file = serve_file.replace('%20', ' ') 
    135224         
    136         def do_get(): 
    137             # This if statement stops os.path.join doing a join with the  
    138             # absolute path '/'.  If this is done, the first argument is  
    139             # obliterated and the result is '/' exposing the root file system to 
    140             # the web client!! 
    141             if serve_file == '/': 
    142                 file = self.path 
    143             else: 
    144                 file = os.path.join(self.path, serve_file) 
    145              
    146             from urlparse import urljoin 
    147             isDir = os.path.isdir(file) 
    148             if serve_file.endswith('/') or isDir: 
    149                 if isDir: 
    150                     dirContents = os.listdir(file) 
     225        requestMethodName = environ['REQUEST_METHOD'] 
     226        requestMethod = self.httpMethodMap.get(requestMethodName) 
     227        if requestMethod is None: 
     228            response = ('%r HTTP request method is not supported' %  
     229                        requestMethodName) 
     230            status = "%d %s" % (httplib.METHOD_NOT_ALLOWED, 
     231                                httplib.responses[httplib.METHOD_NOT_ALLOWED]) 
     232            start_response(status, 
     233                           [('Content-length', str(len(response))), 
     234                            ('Content-type', 'text/plain')]) 
     235            return [response] 
     236             
     237        return requestMethod(self, serve_file, environ, start_response) 
     238         
     239    def do_get(self, serve_file, environ, start_response): 
     240        # This if statement stops os.path.join doing a join with the  
     241        # absolute path '/'.  If this is done, the first argument is  
     242        # obliterated and the result is '/' exposing the root file system to 
     243        # the web client!! 
     244        if serve_file == '/': 
     245            filePath = self.path 
     246        else: 
     247            filePath = os.path.join(self.path, serve_file) 
     248         
     249        from urlparse import urljoin 
     250        isDir = os.path.isdir(filePath) 
     251        if serve_file.endswith('/') or isDir: 
     252            if isDir: 
     253                dirContents = os.listdir(filePath) 
    151254#                     
    152255#                    lines = [ 
     
    155258#                        for filename in dirContents] 
    156259 
    157                     lines = ['<a href="%s">%s</a>' %  
    158                              (urljoin(serve_file, filename), filename)  
    159                              for filename in dirContents] 
    160  
    161                     response = '<html>' + '<br>'.join(lines)+ '</html>' 
    162                     start_response('200 OK',  
     260                lines = ['<a href="%s">%s</a>' %  
     261                         (urljoin(serve_file, filename), filename)  
     262                         for filename in dirContents] 
     263 
     264                response = '<html>' + '<br>'.join(lines)+ '</html>' 
     265                start_response('200 OK',  
    163266                               [('Cache-Control','no-cache'),  
    164267                                ('Pragma','no-cache'), 
    165268                                ('Content-Type', 'text/html; charset=utf-8'), 
    166269                                ('Content-Length', str(len(response)))]) 
    167                      
    168                     return [response] 
    169                 else: 
    170                     logger.error('failed to list directory %s/%s' % (self.path, serve_file)) 
    171                     start_response('404 Not found', [('Content-Type', 'text/plain')]) 
    172                     return ['404 Not Found'] 
    173              
    174             try: 
    175                 if os.name == 'nt' or sys.platform == 'cygwin': 
    176                     f = open(os.path.join(self.path, serve_file), 'rb') 
    177                 else: 
    178                     f = open(os.path.join(self.path, serve_file), 'r') 
    179                 logger.debug('opened file %s' % serve_file) 
    180             except IOError: 
    181                 logger.error('failed to open file %s/%s' % (self.path, serve_file)) 
    182                 start_response('404 Not found', [('Content-Type', 'text/plain')]) 
     270                 
     271                return [response] 
     272            else: 
     273                log.error('failed to list directory %s/%s', self.path,  
     274                          serve_file) 
     275                start_response('404 Not found',  
     276                               [('Content-Type', 'text/plain')]) 
    183277                return ['404 Not Found'] 
    184              
    185             response = FileResponse(f, os.path.join(self.path, serve_file)) 
    186             start_response('200 OK', [('Cache-Control','no-cache'), ('Pragma','no-cache'),  
    187                                       ('Content-Length', str(response.size),), 
    188                                       ('Content-Type', self.guess_content_type(environ['PATH_INFO']))]) 
    189             return response 
    190              
    191         def do_put(): 
    192             #Write file 
    193             try: 
    194                 f = open(os.path.join(self.path, serve_file), 'w') 
    195                 logger.debug('opened file for writing %s' % serve_file) 
    196             except: 
    197                 logger.error('failed to open file for writiing %s/%s' % (self.path, serve_file)) 
    198                 start_response('403 Forbidden', [('Content-Type', 'text/plain')]) 
    199                 return ['403 Forbidden'] 
    200              
    201             f.write(environ['wsgi.input'].read()) 
    202              
    203         def do_mkcollection(): 
    204             pass 
    205              
    206         http_method_map = {'GET':do_get, 'PUT':do_put, 'MKCOLLECTION':do_mkcollection} 
    207         return http_method_map[environ['REQUEST_METHOD']]() 
    208              
     278         
     279        try: 
     280            f = open(os.path.join(self.path, serve_file), 'rb') 
     281            log.debug('opened file %s' % serve_file) 
     282        except IOError: 
     283            log.error('failed to open file %s/%s' % (self.path, serve_file)) 
     284            start_response('404 Not found', [('Content-Type', 'text/plain')]) 
     285            return ['404 Not Found'] 
     286         
     287        response = self.__fileResponse(f, os.path.join(self.path, serve_file)) 
     288        start_response('200 OK',  
     289                       [('Cache-Control','no-cache'),  
     290                        ('Pragma','no-cache'),  
     291                        ('Content-Length', str(response.size),), 
     292                        ('Content-Type',  
     293                         self.guess_content_type(environ['PATH_INFO']))]) 
     294        return response 
     295         
     296    def do_put(self, serve_file, environ, start_response): 
     297        #Write file 
     298        try: 
     299            f = open(os.path.join(self.path, serve_file), 'w') 
     300            log.debug('opened file for writing %s' % serve_file) 
     301        except: 
     302            log.error('failed to open file for writing %s/%s', self.path,  
     303                      serve_file) 
     304            start_response('403 Forbidden', [('Content-Type', 'text/plain')]) 
     305            return ['403 Forbidden'] 
     306         
     307        self.readBlkSize = 1024 
     308        inputLength = environ['CONTENT_LENGTH'] 
     309        inputStream = environ['wsgi.input'] 
     310        nReads = inputLength / self.readBlkSize 
     311        remainder = inputLength % self.readBlkSize 
     312        readArray = [self.readBlkSize] * nReads 
     313        if remainder > 0: 
     314            nReads += 1 
     315            readArray.append(remainder) 
     316             
     317        for length in readArray: 
     318            inputBlk = inputStream.read(length) 
     319            f.write(inputBlk) 
     320         
     321    def do_mkcollection(self, serve_file, environ, start_response): 
     322        pass 
     323             
     324    DEFAULT_HTTP_METHOD_MAP = { 
     325        'GET':          do_get,  
     326        'PUT':          do_put,  
     327        'MKCOLLECTION': do_mkcollection 
     328    } 
    209329 
    210330    def guess_content_type(self, path_info): 
  • ceda_http_fileserver/trunk/ceda_http_fileserver/ceda/server/wsgi/fileserver/test/test_fileserver.py

    r6999 r7009  
    1616from os import path 
    1717 
     18import httplib 
     19 
    1820import paste.fixture 
    1921from paste.deploy import loadapp 
     
    2325 
    2426class FileServerAppTestCase(unittest.TestCase): 
     27    THIS_DIR = path.abspath(path.dirname(__file__)) 
     28    HTDOCS_DIRNAME = 'htdocs' 
     29    HTDOCS_DIR = path.join(THIS_DIR, HTDOCS_DIRNAME) 
     30 
    2531    def __init__(self, *args, **kwargs): 
    26         app = FileServerApp() 
    27         self.app = paste.fixture.TestApp(app) 
     32        self.fileServerApp = FileServerApp(self.__class__.HTDOCS_DIR) 
     33        self.app = paste.fixture.TestApp(self.fileServerApp) 
    2834          
    2935        unittest.TestCase.__init__(self, *args, **kwargs) 
    3036 
    3137    def test01Assert(self): 
    32         # Check the middleware has set the MyProxy client object in environ 
    3338        response = self.app.get('/', status=200) 
     39        print(response) 
    3440        self.assert_(response) 
    3541 
    36     def test01(self): 
    37         pass 
     42    def test02CheckBlkSizeValidation(self): 
     43        def _set(x): 
     44            self.fileServerApp.readBlkSize = x 
     45             
     46        self.assertRaises(ValueError, _set, 'my block size')   
     47        self.assertRaises(ValueError, _set, -10) 
     48         
     49    def test03CheckHttpMethodValidation(self): 
     50        def _set(): 
     51            self.fileServerApp.httpMethodMap = None 
     52             
     53        self.assertRaises(TypeError, _set) 
     54         
     55    def test04TrapHttpMethodNotAllowed(self): 
     56        self.fileServerApp.httpMethodMap = {} 
     57        response = self.app.get('/', status=httplib.METHOD_NOT_ALLOWED) 
    3858     
    3959     
Note: See TracChangeset for help on using the changeset viewer.