Changeset 7038 for ceda_http_fileserver


Ignore:
Timestamp:
21/06/10 10:47:25 (9 years ago)
Author:
pjkersha
Message:

Incomplete - task 9: Data Browser Replacement

  • added support for client byte range requests via the the HTTP Range header field - tested in unit tests
Location:
ceda_http_fileserver/trunk/ceda_http_fileserver/ceda/server/wsgi/fileserver
Files:
2 edited

Legend:

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

    r7032 r7038  
    4141class InvalidRangeRequest(FileResponseError): 
    4242    """Raise for an invalid byte range requested""" 
    43      
     43    def __init__(self, *arg, **kw): 
     44        FileResponseError.__init__(self, *arg, **kw) 
     45        if len(arg) > 1: 
     46            self.contentRangeHdr = arg[1] 
     47        else: 
     48            self.contentRangeHdr = None 
     49             
     50     
     51class InvalidRangeRequestSyntax(FileResponseError): 
     52    """Raise for invalid range request syntax""" 
     53    
    4454     
    4555class FileResponse(object): 
    4656    """Helper class creates iterable response based on a given block size""" 
    47     DEFAULT_READ_SIZE = 1024 
    48      
    49     def __init__(self, readSize=DEFAULT_READ_SIZE): 
     57    DEFAULT_BLK_SIZE = 1024 
     58    BYTE_RANGE_SEP = '-' 
     59    CONTENT_RANGE_FIELDNAME = 'Content-range' 
     60    CONTENT_RANGE_FORMAT_STR = "%d-%d/%d" 
     61    INVALID_CONTENT_RANGE_FORMAT_STR = "*/%d" 
     62     
     63    __slots__ = ( 
     64        'fileObj', 
     65        'fileSize', 
     66        '__blkSize', 
     67        'readLengths', 
     68        'contentLength', 
     69        'contentRange', 
     70        'contentRangeHdr', 
     71    ) 
     72     
     73    def __init__(self, filePath, requestRange=None, blkSize=DEFAULT_BLK_SIZE): 
     74        '''Open a file and set the blocks for reading, any input range set and 
     75        the response size 
     76        ''' 
    5077        self.fileObj = None 
    5178        self.fileSize = None 
    52         self.readSize = readSize 
    53         self.start = None 
    54         self.end = None 
    55          
    56     def __call__(self, fileObj, fileName, rangeRequest=None): 
    57         self.fileSize = os.path.getsize(fileName) 
    58         if rangeRequest is not None: 
    59             start, end = [int(i) for i in requestRange.split(':')] 
     79         
     80        # the length of the content to return - this will be different to the 
     81        # file size if the client a byte range header field setting 
     82        self.contentLength = 0 
     83         
     84        # None unless a valid input range was given 
     85        self.contentRange = None 
     86         
     87        # Formatted for HTTP content range header field 
     88        self.contentRangeHdr = None 
     89 
     90        # This will call the relevant set property method 
     91        self.blkSize = blkSize 
     92         
     93        # Array of blocks lengths for iterator to use to read the file 
     94        self.readLengths = [] 
     95         
     96        try: 
     97            self.fileObj = open(filePath, 'rb') 
     98            log.debug('Opened file %s', filePath) 
     99             
     100        except IOError: 
     101            log.error('Failed to open file %r: %s', filePath,  
     102                      traceback.format_exc())      
     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                start, end = [ 
     115                    int(i)  
     116                    for i in requestRange.split(FileResponse.BYTE_RANGE_SEP) 
     117                ] 
     118            except ValueError: 
     119                raise InvalidRangeRequestSyntax('Invalid format for request ' 
     120                                                'range %r' % requestRange) 
    60121             
    61122            # Verify range bounds 
    62123            if start > end: 
    63124                raise InvalidRangeRequest('Range start index %r is greater ' 
    64                                           'than the end index %r' %  
    65                                           (start, end)) 
     125                                          'than the end index %r for file %r' %  
     126                                          (start, end, filePath), 
     127                                          contentRangeHdr) 
    66128            elif start < 0: 
    67129                raise InvalidRangeRequest('Range start index %r is less than ' 
    68                                           'zero' % start) 
     130                                          'zero' % start, 
     131                                          contentRangeHdr) 
    69132                 
    70133            elif end >= self.fileSize: 
    71134                raise InvalidRangeRequest('Range end index %r is greater than ' 
    72                                           'the length of the requested ' 
    73                                           'resource %r' % (end, self.fileSize)) 
    74         else: 
    75             start = 0 
    76             self.end = self.fileSize - 1 
    77              
    78         self.fileObj = fileObj 
    79         self.fileObj.seek(start) 
     135                                          'the length %r of the requested ' 
     136                                          'resource %r' % (end,  
     137                                                           self.fileSize, 
     138                                                           filePath), 
     139                                          contentRangeHdr) 
     140         
     141            # Set the total content length to return 
     142            self.contentLength = end + 1 - start  
     143            self.contentRange = (start, end) 
     144            self.contentRangeHdr = ( 
     145                self.__class__.CONTENT_RANGE_FIELDNAME,  
     146                self.__class__.CONTENT_RANGE_FORMAT_STR %  
     147                                        (self.contentRange + (self.fileSize,)) 
     148            ) 
     149            self.fileObj.seek(start) 
     150        else:             
     151            # Set the total content length to return 
     152            self.contentLength = self.fileSize 
     153                   
     154        nReads = self.contentLength / self.blkSize 
     155        lastReadLen = self.contentLength % self.blkSize 
     156        self.readLengths = [self.blkSize] * nReads 
     157        if lastReadLen > 0: 
     158            nReads += 1 
     159            self.readLengths.append(lastReadLen) 
    80160         
    81161    def __iter__(self): 
    82         output = '\n' 
    83         while len(output) is not 0: 
    84             output = self.fileObj.read(self.end+1) 
     162        '''Read the file a block at a time''' 
     163        while len(self.readLengths) > 0: 
     164            output = self.fileObj.read(self.readLengths[-1]) 
     165            self.readLengths.pop() 
    85166            yield output 
    86  
    87     def _getReadSize(self): 
    88         return self.__readSize 
    89      
    90     def _setReadSize(self, value): 
    91         self.__readSize = int(value) 
    92         if self.__readSize < 0: 
     167#        output = '\n' 
     168#        while len(output) is not 0: 
     169#            output = self.fileObj.read(self.end+1) 
     170#            yield output 
     171 
     172    def _getBlkSize(self): 
     173        return self.__blkSize 
     174     
     175    def _setBlkSize(self, value): 
     176        self.__blkSize = int(value) 
     177        if self.__blkSize < 0: 
    93178            raise ValueError('Expecting positive integer value for block size ' 
    94179                             'attribute') 
    95180             
    96     readSize = property(fget=_getReadSize, fset=_setReadSize, 
    97                         doc="block size reading the file in the iterator and " 
    98                             "returning a response") 
     181    blkSize = property(fget=_getBlkSize, fset=_setBlkSize, 
     182                        doc="block size for reading the file in the iterator " 
     183                            "and returning a response") 
    99184 
    100185 
     
    102187_statusCode2Msg = lambda code: "%d %s" % (code,  
    103188                    httplib.responses.get(code, httplib.INTERNAL_SERVER_ERROR)) 
    104              
    105 _parseRangeRequest = lambda rangeStr: tuple( 
    106                                         [int(i) for i in rangeStr.split(':')]) 
    107  
     189     
    108190 
    109191class FileServerApp(object): 
     
    118200     
    119201    statusCode2Msg = staticmethod(_statusCode2Msg) 
    120     parseRangeRequest = staticmethod(lambda rangeStr: tuple( 
    121                                      [int(i) for i in rangeStr.split(':')])) 
    122202     
    123203    # Map HTTP status string to a given file system file access error 
     
    128208     
    129209    __slots__ = ( 
    130         '__fileResponse', 
    131210        '__readBlkSize', 
     211        '__writeBlkSize', 
    132212        '__httpMethodMap', 
    133213        '__fileSysRoot', 
     
    146226         
    147227        # Initialise all attributes here 
    148         self.__fileResponse = FileResponse() 
    149228        self.__readBlkSize = None 
     229        self.__writeBlkSize = None 
    150230        self.__httpMethodMap = None 
    151231        self.__mountPoint = None 
     
    194274            raise TypeError('Expecting string or None type for ' 
    195275                            '"fileFilterRegEx" attribute; got %r' % type(value)) 
    196  
    197          
    198  
     276         
    199277    fileFilterRegEx = property(_getFileFilterRegEx, _setFileFilterRegEx,  
    200278                               doc="Regular expression to filter out matching " 
     
    252330 
    253331    def _getWriteBlkSize(self): 
    254         return self.__fileResponse.readSize 
     332        return self.__writeBlkSize 
    255333 
    256334    def _setWriteBlkSize(self, value): 
    257         self.__fileResponse.readSize = value 
     335        self.__writeBlkSize = int(value) 
     336        if self.__writeBlkSize < 0: 
     337            raise ValueError('Expecting positive integer value for block size ' 
     338                             'attribute') 
    258339         
    259340    writeBlkSize = property(_getWriteBlkSize, _setWriteBlkSize,  
     
    407488                        '<br>'.join(lines)) 
    408489     
    409             if rangeRequest is not None: 
     490            if requestRange is not None: 
    410491                log.warning('Requested path %r will return a HTML type ' 
    411492                            'response listing directory content: ignoring HTTP ' 
    412493                            'range request %r', (environ['PATH_INFO'], 
    413                                                  rangeRequest)) 
     494                                                 requestRange)) 
    414495                 
    415496            start_response(FileServerApp.statusCode2Msg(httplib.OK),  
     
    422503        else: 
    423504            try: 
     505                fileResponse = FileResponse(filePath, 
     506                                            blkSize=self.writeBlkSize, 
     507                                            requestRange=requestRange) 
    424508                fileObj = open(filePath, 'rb') 
    425509                log.debug('opened file %s', filePath) 
     
    428512                # Map file access error to a HTTP response code 
    429513                status = FileServerApp.mapFileAccessError2HttpStatus(e.errno) 
    430                 log.error('failed to open file %r: %s', filePath,  
    431                           traceback.format_exc()) 
    432514                response = status 
    433515                start_response(status,  
     
    435517                                ('Content-length', str(len(response)))]) 
    436518                return [response] 
    437              
    438             try: 
    439                 self.__fileResponse(fileObj, filePath,  
    440                                     rangeRequest=rangeRequest) 
     519               
     520            except InvalidRangeRequestSyntax, e: 
     521                # Byte range was requested for extraction but it was badly  
     522                # formatted 
     523                response = str(e) 
     524                log.error(response) 
     525                status = FileServerApp.statusCode2Msg(httplib.BAD_REQUEST) 
     526                start_response(status,  
     527                               [('Content-Type', 'text/plain'), 
     528                                ('Content-length', str(len(response)))]) 
     529                return [response] 
     530                 
    441531            except InvalidRangeRequest, e: 
     532                # Byte range was requested for extraction but it's invalid for 
     533                # the requested file 
    442534                response = str(e) 
    443535                log.error(response) 
     
    446538                start_response(status,  
    447539                               [('Content-Type', 'text/plain'), 
    448                                 ('Content-length', str(len(response)))]) 
     540                                ('Content-length', str(len(response))), 
     541                                 e.contentRangeHdr]) 
    449542                return [response] 
    450                              
    451             start_response(FileServerApp.statusCode2Msg(httplib.OK),  
    452                        [('Cache-Control','no-cache'),  
    453                         ('Pragma','no-cache'),  
    454                         ('Content-Length', str(self.__fileResponse.fileSize)), 
    455                         ('Content-Type',  
    456                          self.getContentType(environ['PATH_INFO']))]) 
    457             return self.__fileResponse 
     543             
     544            headers = [ 
     545                ('Cache-Control','no-cache'),  
     546                ('Pragma','no-cache'),  
     547                ('Content-Length', str(fileResponse.contentLength)), 
     548                ('Content-Type', self.getContentType(environ['PATH_INFO'])) 
     549            ] 
     550                                         
     551            if requestRange: 
     552                status = FileServerApp.statusCode2Msg(httplib.PARTIAL_CONTENT) 
     553                headers.append(fileResponse.contentRangeHdr) 
     554            else: 
     555                status = FileServerApp.statusCode2Msg(httplib.OK) 
     556                                
     557            start_response(status, headers) 
     558            return fileResponse 
    458559         
    459560    def do_put(self, fileSysSubDir, environ, start_response): 
  • ceda_http_fileserver/trunk/ceda_http_fileserver/ceda/server/wsgi/fileserver/test/test_fileserver.py

    r7032 r7038  
    6565    def __init__(self, *args, **kwargs): 
    6666        self.fileServerApp = FileServerApp(self.__class__.HTDOCS_DIR) 
     67         
     68        # Small block size to test WSGI iterator for writing response 
     69        self.fileServerApp.writeBlkSize = 256 
    6770        self.app = paste.fixture.TestApp(self.fileServerApp) 
    6871          
     
    157160         
    158161    def test11AccessForbidden(self): 
    159          
     162        # Set a file on the file system with permissions so that it can be read 
     163        # and ensure that the server returns a 403 Forbidden response for a  
     164        # request for this file 
    160165        jpegFilePath = path.join(self.__class__.HTDOCS_DIR, 
    161166                                 self.__class__.JPEG_REL_FILEPATH) 
     
    215220                                status=httplib.PARTIAL_CONTENT) 
    216221        self.assert_('content-range' in response.header_dict) 
    217          
     222        print('Content-range: %s' % response.header_dict['content-range']) 
     223        self.assert_(len(response.body) == 600) 
     224         
     225    def test15CatchInvalidRangeSyntax(self): 
     226        # Specify invalid range syntax 
     227        headers = {'Range': 'a bad range'} 
     228        response = self.app.get(self.__class__.PDF_REL_URIPATH, 
     229                                headers=headers,  
     230                                status=httplib.BAD_REQUEST) 
     231         
     232    def test15CatchInvalidRange(self): 
     233        # Specify a range of bytes for retrieval 
     234        headers = {'Range': '100-99999999999999999'} 
     235        response = self.app.get(self.__class__.PDF_REL_URIPATH, 
     236                                headers=headers,  
     237                                status=httplib.REQUESTED_RANGE_NOT_SATISFIABLE) 
     238        self.assert_('content-range' in response.header_dict)     
     239        print('Content-range: %s' % response.header_dict['content-range']) 
     240         
     241               
    218242class FileServerAppPasterTestCase(FileServerAppTestCaseBase):   
    219243    """Base class for common Paste Deploy related set-up""" 
Note: See TracChangeset for help on using the changeset viewer.