Ticket #1493: twisted.web.static.3.diff

File twisted.web.static.3.diff, 16.8 KB (added by brubelsabs, 14 years ago)

beautified docstrings and testnames as well 2 blanks between methods, and new MalformedHeader Exception derived from Error

  • test/test_web.py

     
    1515    uri='http://dummy/'
    1616    method = 'GET'
    1717
    18     def getHeader(self, h):
    19         return None
    2018
     19    def getHeader(self, name):
     20        return self.headers.get(name.lower(), None)
     21
     22
    2123    def registerProducer(self, prod,s):
    2224        self.go = 1
    2325        while self.go:
     
    3638        self.protoSession = session or server.Session(0, self)
    3739        self.args = {}
    3840        self.outgoingHeaders = {}
     41        self.headers = {}
     42        self.responseCode = None
    3943
     44
    4045    def setHeader(self, name, value):
    4146        """TODO: make this assert on write() if the header is content-length
    4247        """
     
    5459        self.finished = self.finished + 1
    5560    def addArg(self, name, value):
    5661        self.args[name] = [value]
     62
     63
    5764    def setResponseCode(self, code):
    5865        assert not self.written, "Response code cannot be set after data has been written: %s." % "@@@@".join(self.written)
     66        self.responseCode = code
     67
     68
    5969    def setLastModified(self, when):
    6070        assert not self.written, "Last-Modified cannot be set after data has been written: %s." % "@@@@".join(self.written)
    6171    def setETag(self, tag):
     
    535545    clientproto = 'HTTP/1.0'
    536546    sentLength = None
    537547
    538     def __init__(self, *a, **kw):
    539         DummyRequest.__init__(self, *a, **kw)
    540         self.headers = {}
    541 
    542     def getHeader(self, h):
    543         return self.headers.get(h.lower(), None)
    544 
    545548    def getClientIP(self):
    546549        return self.client
    547550
  • test/test_static.py

     
    1 from twisted.trial import unittest
    21import os
    3 from twisted.web import static
     2import tempfile
    43
    5 class FakeRequest:
    6     method = 'GET'
     4from twisted.python import log
     5from twisted.trial import unittest
     6from twisted.web import static, http
     7from twisted.web.test.test_web import DummyRequest
    78
    8     _headers = None
    9     _setHeaders = None
    10     _written = ''
    119
    12     def __init__(self):
    13         self._headers = {}
    14         self._setHeaders = {}
    1510
    16     def getHeader(self, k):
    17         if self._headers is None:
    18             return None
    19         return self._headers.get(k)
     11class Range(unittest.TestCase):
     12    def setUp(self):
     13        self.file = tempfile.NamedTemporaryFile()
     14        self.payload = '\xf8u\xf3E\x8c7\xce\x00\x9e\xb6a0y0S\xf0\xef\xac\xb7\xbe\xb5\x17M\x1e\x136k{\x1e\xbe\x0c\x07\x07\t\xd0\xbckY\xf5I\x0b\xb8\x88oZ\x1d\x85b\x1a\xcdk\xf2\x1d&\xfd%\xdd\x82q/A\x10Y\x8b'
     15        self.file.write(self.payload)
     16        self.file.flush()
     17        self.resource = static.File(self.file.name)
     18        self.resource.isLeaf = 1
     19        self.request = DummyRequest([''])       
     20        self.request.uri = self.file.name
     21        self.file.seek(0)
     22        self.catcher = []
     23        log.addObserver(self.catcher.append)
    2024
    21     def setHeader(self, k, v):
    22         self._setHeaders.setdefault(k, []).append(v)
    2325
    24     def setLastModified(self, x):
    25         pass
    26     def registerProducer(self, producer, x):
    27         producer.resumeProducing()
    28     def unregisterProducer(self):
    29         pass
    30     def finish(self):
    31         pass
     26    def tearDown(self):
     27        self.file.close()
     28        log.removeObserver(self.catcher.append)
    3229
    33     def write(self, data):
    34         self._written = self._written + data
    3530
    36 class Range(unittest.TestCase):
    37     todo = (unittest.FailTest, 'No range support yet.')
     31    def _assert_logged(self, expected):
     32        """
     33        Asserts that a given log occured with an expected message.
     34        """
    3835
    39     def setUp(self):
    40         self.tmpdir = self.mktemp()
    41         os.mkdir(self.tmpdir)
    42         name = os.path.join(self.tmpdir, 'junk')
    43         f = file(name, 'w')
    44         f.write(8000 * 'x')
    45         f.close()
    46         self.file = static.File(name)
    47         self.request = FakeRequest()
     36        log_item = self.catcher.pop()
     37        self.assertEquals(log_item["message"][0], expected)
     38        self.assert_(len(self.catcher) == 0, "An additional log occured: " +
     39                repr(log_item))
    4840
    49     def testBodyLength(self):
    50         self.request._headers['range'] = 'bytes=0-1999'
    51         self.file.render(self.request)
    52         self.assertEquals(len(self.request._written), 2000)
    5341
    54     def testContentLength(self):
    55         """Content-Length of a request is correct."""
    56         self.request._headers['range'] = 'bytes=0-1999'
    57         self.file.render(self.request)
    58         self.assertEquals(self.request._setHeaders['content-length'], ['2000'])
     42    def test_BodyLength(self):
     43        self.request.headers['range'] = 'bytes=0-43'
     44        self.resource.render(self.request)
     45        self.assertEquals(len(''.join(self.request.written)), 44)
    5946
    60     def testContentRange(self):
    61         """Content-Range of a request is correct."""
    62         self.request._headers['range'] = 'bytes=0-1999'
    63         self.file.render(self.request)
    64         self.assertEquals(self.request._setHeaders.get('content-range'), ['bytes 0-1999/8000'])
     47
     48    def test_ByteUnit(self):
     49        self.request.headers['range'] = 'foobar=0-43'
     50        self.resource.render(self.request)
     51        expected = ("Warning: ignoring malformed Range-Header due to:\n\t400" +
     52            " Unsupported Byte-Unit: 'foobar'\n\tProceed without this header" +
     53            " field")
     54        self._assert_logged(expected)
     55        self.assertEquals(''.join(self.request.written), self.payload)
     56        self.assertEquals(self.request.responseCode, http.OK)
     57
     58    def test_BodyContent(self):
     59        self.request.headers['range'] = 'bytes=3-43'
     60        self.resource.render(self.request)
     61        self.assertEquals(''.join(self.request.written), self.payload[3:44])
     62
     63
     64    def test_ContentLength(self):
     65        """
     66        Content-Length of a request is correct.
     67        """
     68
     69        self.request.headers['range'] = 'bytes=0-43'
     70        self.resource.render(self.request)
     71        self.assertEquals(self.request.outgoingHeaders['content-length'], '44')
     72
     73
     74    def test_ContentRange(self):
     75        """
     76        Content-Range of a request is correct.
     77        """
     78
     79        self.request.headers['range'] = 'bytes=0-43'
     80        self.resource.render(self.request)
     81        self.assertEquals(self.request.outgoingHeaders['content-range'], 'bytes 0-43/64')
     82
     83
     84    def test_StatusCodePartialContent(self):
     85        """
     86        Test if the correct status code 206 is submitted.
     87        """
     88
     89        self.request.headers['range'] = 'bytes=0-43'
     90        self.resource.render(self.request)
     91        self.assertEquals(self.request.responseCode, http.PARTIAL_CONTENT)
     92
     93
     94    def test_StatusCodeRequestedRangeNotSatisfiable(self):
     95        """
     96        Test if the correct status code 416 is submitted.
     97        """
     98        self.request.headers['range'] = 'bytes=20-13'
     99        self.resource.render(self.request)
     100        self.assertEquals(self.request.responseCode, http.REQUESTED_RANGE_NOT_SATISFIABLE)
     101
     102
     103    def test_FirstByteSupport(self):
     104        """
     105        Test if all is correct handled if end-byte is omitted.
     106        """
     107
     108        self.request.headers['range'] = 'bytes=23-'
     109        self.resource.render(self.request)
     110        self.assertEquals(''.join(self.request.written), self.payload[23:])
     111        self.assertEquals(len(''.join(self.request.written)), 41)
     112        self.assertEquals(self.request.outgoingHeaders['content-range'], 'bytes 23-63/64')
     113        self.assertEquals(self.request.outgoingHeaders['content-length'], '41')
     114
     115
     116    def test_LastByteSupport(self):
     117        """
     118        Test if all is correct handled if start-byte is omitted.
     119        """
     120
     121        self.request.headers['range'] = 'bytes=-17'
     122        self.resource.render(self.request)
     123        self.assertEquals(''.join(self.request.written), self.payload[-17:])
     124        self.assertEquals(len(''.join(self.request.written)), 17)
     125        self.assertEquals(self.request.outgoingHeaders['content-range'], 'bytes 47-63/64')
     126        self.assertEquals(self.request.outgoingHeaders['content-length'], '17')
     127
     128
     129    def test_InvalidByteRangeNoStartNoEnd(self):
     130        """
     131        Test what happens when start and end is missing.
     132        """
     133
     134        self.request.headers['range'] = 'bytes=-'
     135        self.resource.render(self.request)
     136        self.assertEquals(self.request.responseCode, http.OK)
     137        self.assertEquals(len(''.join(self.request.written)), 64)
     138        self.assertEquals(''.join(self.request.written), self.payload)
     139        expected = ("Warning: ignoring malformed Range-Header due to:\n\t400" +
     140            " Invalid Byte-Range: '-'\n\tProceed without this header field")
     141        self._assert_logged(expected)
     142
     143
     144    def test_InvalidByteRangeStartGreaterEnd(self):
     145        """
     146        Test what happens if start byte is greate than end byte.
     147        """
     148
     149        self.request.headers['range'] = 'bytes=23-21'
     150        self.resource.render(self.request)
     151        self.assertEquals(self.request.responseCode, http.REQUESTED_RANGE_NOT_SATISFIABLE)
     152        self.assertEquals(len(''.join(self.request.written)), 0)
     153
     154
     155    def test_MultipleRanges(self):
     156        """
     157        Test the response if multiple ranges are given. As the RFC2616
     158        says, we only have to return just on range. See section 14.16
     159        paragraph starting with: "Unlike byte-ranges-specifier".
     160        """
     161
     162        self.request.headers['range'] = 'bytes=23-43,17-28'
     163        self.resource.render(self.request)
     164        self.assertEquals(self.request.responseCode, http.PARTIAL_CONTENT)
     165        self.assertEquals(len(''.join(self.request.written)), 21)
     166        self.assertEquals(''.join(self.request.written), self.payload[23:44])
     167
     168
     169    def test_InvalidByteRangeContainingNaNs(self):
     170        """
     171        Test whats happening if the byte range has NaN in it.
     172        """
     173
     174        self.request.headers['range'] = 'bytes=-abc#'
     175        self.resource.render(self.request)
     176        self.assertEquals(self.request.responseCode, http.OK)
     177        self.assertEquals(len(''.join(self.request.written)), 64)
     178        self.assertEquals(''.join(self.request.written), self.payload)
     179        expected = ("Warning: ignoring malformed Range-Header due to:\n\t400 " +
     180                   "Invalid Byte-Range: '-abc#'\n\tProceed without this " +
     181                   "header field")
     182        self._assert_logged(expected)
  • error.py

     
    1212
    1313class Error(Exception):
    1414    def __init__(self, code, message = None, response = None):
     15        """
     16        Initializes a basic exception.
     17
     18        @param code: Refers to an HTTP status code e.g. http.NOT_FOUND. If no
     19        message is given the given code is mapped to a descriptive string and
     20        used instead.
     21        """
     22
    1523        message = message or http.responses.get(code)
    1624        Exception.__init__(self, code, message, response)
    1725        self.status = code
    1826        self.response = response
    19    
     27
     28
    2029    def __str__(self):
    2130        return '%s %s' % (self[0], self[1])
    2231
     32
     33
     34class MalformedHeader(Error):
     35    def __init__(self, code = None, message = None, response = None):
     36        if (not code):
     37            code = http.BAD_REQUEST
     38        Error.__init__(self, code, message, response)
     39
     40
     41
    2342class PageRedirect(Error):
    2443    """A request that resulted in a http redirect """
    2544    def __init__(self, code, message = None, response = None, location = None):
  • static.py

     
    150150    type = types.get(ext, defaultType)
    151151    return type, enc
    152152
     153
     154
    153155class File(resource.Resource, styles.Versioned, filepath.FilePath):
    154156    """
    155157    File is a resource that represents a plain non-interpreted file
     
    287289        return self.getsize()
    288290
    289291
     292    def _handleRangeRequest(self, request, file):
     293        """
     294        Performs (simple) Range-Header requests. Simple means, that only
     295        the first byte range is handled.
     296
     297        @param file: file handle for the corresponding ressource
     298        @type file: file object
     299        @raise SyntaxError: The given Byte-Ranges-Specifier was invalid
     300        @return: content-length and the number of the byte to where to stop.
     301        @rtype: C{int}, C{int}
     302        """
     303
     304        size = self.getFileSize()
     305        range = request.getHeader('range')
     306        bytesrange = string.split(range, '=')
     307        if not (bytesrange[0] == 'bytes'):
     308            raise error.MalformedHeader(message="Unsupported Byte-Unit: " +
     309                    repr(bytesrange[0]))
     310        start, stop = string.split(bytesrange[1], ',', 1)[0].split('-')
     311        if ((start is None) and (stop is None)):
     312            raise error.MalformedHeader(message="Invalid Byte-Range: " +
     313                    repr(bytesrange[1]))
     314
     315        try:
     316            if start:
     317                start = int(start)
     318                if stop and (int(stop) < size):
     319                    stop = int(stop) + 1
     320                else:
     321                    stop = size
     322            else:
     323                lastbytes = int(stop)
     324                if size < lastbytes:
     325                    lastbytes = size
     326                start = size - lastbytes
     327                stop = size
     328        except ValueError, e:
     329            raise error.MalformedHeader(message="Invalid Byte-Range: " +
     330                    repr(range.split('=')[1].split(',')[0]))
     331
     332        file.seek(start)
     333        content_length = stop - start
     334        if content_length <= 0:
     335            request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
     336            content_length = size
     337            request.method = 'HEAD' # no msg body will be transferred
     338        else:
     339            request.setResponseCode(http.PARTIAL_CONTENT)
     340            request.setHeader('content-range', "bytes %s-%s/%s" % (str(start), str(stop-1), str(size)))
     341
     342        return content_length, stop
     343
     344
    290345    def render(self, request):
    291346        """You know what you doing."""
    292347        self.restat()
     
    303358        if self.isdir():
    304359            return self.redirect(request)
    305360
    306         #for content-length
    307         fsize = size = self.getFileSize()
     361        request.setHeader('accept-ranges','bytes')
    308362
    309 #         request.setHeader('accept-ranges','bytes')
    310 
    311363        if self.type:
    312364            request.setHeader('content-type', self.type)
    313365        if self.encoding:
     
    325377        if request.setLastModified(self.getmtime()) is http.CACHED:
    326378            return ''
    327379
    328 # Commented out because it's totally broken. --jknight 11/29/04
    329 #         try:
    330 #             range = request.getHeader('range')
    331 #
    332 #             if range is not None:
    333 #                 # This is a request for partial data...
    334 #                 bytesrange = string.split(range, '=')
    335 #                 assert bytesrange[0] == 'bytes',\
    336 #                        "Syntactically invalid http range header!"
    337 #                 start, end = string.split(bytesrange[1],'-')
    338 #                 if start:
    339 #                     f.seek(int(start))
    340 #                 if end:
    341 #                     end = int(end)
    342 #                     size = end
    343 #                 else:
    344 #                     end = size
    345 #                 request.setResponseCode(http.PARTIAL_CONTENT)
    346 #                 request.setHeader('content-range',"bytes %s-%s/%s " % (
    347 #                     str(start), str(end), str(size)))
    348 #                 #content-length should be the actual size of the stuff we're
    349 #                 #sending, not the full size of the on-server entity.
    350 #                 fsize = end - int(start)
    351 #
    352 #             request.setHeader('content-length', str(fsize))
    353 #         except:
    354 #             traceback.print_exc(file=log.logfile)
     380        # set the stop byte, and content-length
     381        content_length = stop = self.getFileSize()
     382       
     383        if request.getHeader('range') is not None:
     384            try:
     385                content_length, stop = self._handleRangeRequest(request, f)
     386            except error.MalformedHeader, e:
     387                log.msg("Warning: ignoring malformed Range-Header due to:\n" +
     388                        "\t" + str(e) + "\n\tProceed without this header field")
     389                request.setResponseCode(http.OK)
     390                content_length = stop = self.getFileSize()
    355391
    356         request.setHeader('content-length', str(fsize))
     392        request.setHeader('content-length', str(content_length))
    357393        if request.method == 'HEAD':
    358394            return ''
    359395
    360396        # return data
    361         FileTransfer(f, size, request)
     397        FileTransfer(f, stop, request)
    362398        # and make sure the connection doesn't get closed
    363399        return server.NOT_DONE_YET
    364400
     401
    365402    def redirect(self, request):
    366403        return redirectTo(addSlash(request), request)
    367404