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

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

Due to the review from jknight, some improvments of the previous patch

  • static.py

     
    286286        """Return file size."""
    287287        return self.getsize()
    288288
     289    def _handleRangeRequest(self, request, file):
     290        """Performs (simple) Range-Header requests. Simple means, that only
     291        the first byte range is handled.
    289292
     293        @param file: file handle for the corresponding ressource
     294        @type file: file object
     295        @raise SyntaxError: The given Byte-Ranges-Specifier was invalid
     296        @return: content-length and the number of the byte to where to stop.
     297        @rtype: C{int}, C{int}
     298        """
     299
     300        size = self.getFileSize()
     301        range = request.getHeader('range')
     302        bytesrange = string.split(range, '=')
     303        if not (bytesrange[0] == 'bytes'):
     304            raise SyntaxError("unsupported Byte-Unit => ignoring range request")
     305        start, stop = string.split(bytesrange[1], ',', 1)[0].split('-')
     306        if not ((start is not None) or (stop is not None)):
     307            raise SyntaxError("syntactically invalid Range-Header, ignore..")
     308
     309        try:
     310            if start:
     311                start = int(start)
     312                if stop and (int(stop) < size):
     313                    stop = int(stop) + 1
     314                else:
     315                    stop = size
     316            else:
     317                lastbytes = int(stop)
     318                if size < lastbytes:
     319                    lastbytes = size
     320                start = size - lastbytes
     321                stop = size
     322        except ValueError, e:
     323            raise SyntaxError("invalid byte ranges => ignoring range request")
     324
     325        file.seek(start)
     326        content_length = stop - start
     327        if content_length <= 0:
     328            request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
     329            content_length = size
     330            request.method = 'HEAD' # no msg body will be transferred
     331        else:
     332            request.setResponseCode(http.PARTIAL_CONTENT)
     333            request.setHeader('content-range', "bytes %s-%s/%s" % (str(start), str(stop-1), str(size)))
     334
     335        return content_length, stop
     336
     337
    290338    def render(self, request):
    291339        """You know what you doing."""
    292340        self.restat()
     
    303351        if self.isdir():
    304352            return self.redirect(request)
    305353
    306         #for content-length
    307         fsize = size = self.getFileSize()
     354        request.setHeader('accept-ranges','bytes')
    308355
    309 #         request.setHeader('accept-ranges','bytes')
    310 
    311356        if self.type:
    312357            request.setHeader('content-type', self.type)
    313358        if self.encoding:
     
    325370        if request.setLastModified(self.getmtime()) is http.CACHED:
    326371            return ''
    327372
    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)
     373        # set the stop byte, and content-length
     374        content_length = stop = self.getFileSize()
     375       
     376        if request.getHeader('range') is not None:
     377            try:
     378                content_length, stop = self._handleRangeRequest(request, f)
     379            except SyntaxError, e:
     380                log.msg("Warning: " + str(e))
     381                request.setResponseCode(http.OK)
     382                content_length = stop = self.getFileSize()
    355383
    356         request.setHeader('content-length', str(fsize))
     384        request.setHeader('content-length', str(content_length))
    357385        if request.method == 'HEAD':
    358386            return ''
    359387
    360388        # return data
    361         FileTransfer(f, size, request)
     389        FileTransfer(f, stop, request)
    362390        # and make sure the connection doesn't get closed
    363391        return server.NOT_DONE_YET
    364392
  • test/test_web.py

     
    1515    uri='http://dummy/'
    1616    method = 'GET'
    1717
    18     def getHeader(self, h):
    19         return None
     18    def getHeader(self, name):
     19        return self.headers.get(name.lower(), None)
    2020
    2121    def registerProducer(self, prod,s):
    2222        self.go = 1
     
    3636        self.protoSession = session or server.Session(0, self)
    3737        self.args = {}
    3838        self.outgoingHeaders = {}
     39        self.headers = {}
     40        self.responseCode = None
    3941
    4042    def setHeader(self, name, value):
    4143        """TODO: make this assert on write() if the header is content-length
     
    5658        self.args[name] = [value]
    5759    def setResponseCode(self, code):
    5860        assert not self.written, "Response code cannot be set after data has been written: %s." % "@@@@".join(self.written)
     61        self.responseCode = code
    5962    def setLastModified(self, when):
    6063        assert not self.written, "Last-Modified cannot be set after data has been written: %s." % "@@@@".join(self.written)
    6164    def setETag(self, tag):
     
    535538    clientproto = 'HTTP/1.0'
    536539    sentLength = None
    537540
    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 
    545541    def getClientIP(self):
    546542        return self.client
    547543
  • 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.trial import unittest
     5from twisted.web import static, http
     6from twisted.web.test.test_web import DummyRequest
    77
    8     _headers = None
    9     _setHeaders = None
    10     _written = ''
    11 
    12     def __init__(self):
    13         self._headers = {}
    14         self._setHeaders = {}
    15 
    16     def getHeader(self, k):
    17         if self._headers is None:
    18             return None
    19         return self._headers.get(k)
    20 
    21     def setHeader(self, k, v):
    22         self._setHeaders.setdefault(k, []).append(v)
    23 
    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
    32 
    33     def write(self, data):
    34         self._written = self._written + data
    35 
    368class Range(unittest.TestCase):
    37     todo = (unittest.FailTest, 'No range support yet.')
    389
    3910    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()
     11        self.file = tempfile.NamedTemporaryFile()
     12        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'
     13        self.file.write(self.payload)
     14        self.file.flush()
     15       
     16        self.resource = static.File(self.file.name)
     17        self.resource.isLeaf = 1
     18        self.request = DummyRequest([''])       
     19        self.request.uri = self.file.name
     20        self.file.seek(0)
    4821
     22    def tearDown(self):
     23        self.file.close()
     24
    4925    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)
     26        self.request.headers['range'] = 'bytes=0-43'
     27        self.resource.render(self.request)
     28        self.assertEquals(len(''.join(self.request.written)), 44)
    5329
     30    def testBodyContent(self):
     31        self.request.headers['range'] = 'bytes=3-43'
     32        self.resource.render(self.request)
     33        self.assertEquals(''.join(self.request.written), self.payload[3:44])
     34
    5435    def testContentLength(self):
    5536        """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'])
     37        self.request.headers['range'] = 'bytes=0-43'
     38        self.resource.render(self.request)
     39        self.assertEquals(self.request.outgoingHeaders['content-length'], '44')
    5940
    6041    def testContentRange(self):
    6142        """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'])
     43        self.request.headers['range'] = 'bytes=0-43'
     44        self.resource.render(self.request)
     45        self.assertEquals(self.request.outgoingHeaders['content-range'], 'bytes 0-43/64')
     46
     47    def testStatusCodePartialContent(self):
     48        """Test if the correct status code 206 is submitted."""
     49        self.request.headers['range'] = 'bytes=0-43'
     50        self.resource.render(self.request)
     51        self.assertEquals(self.request.responseCode, http.PARTIAL_CONTENT)
     52
     53    def testStatusCodeRequestedRangeNotSatisfiable(self):
     54        """Test if the correct status code 416 is submitted."""
     55        self.request.headers['range'] = 'bytes=20-13'
     56        self.resource.render(self.request)
     57        self.assertEquals(self.request.responseCode, http.REQUESTED_RANGE_NOT_SATISFIABLE)
     58
     59    def testFirstByteSupport(self):
     60        """Test if all is correct handled if end-byte is omitted."""
     61        self.request.headers['range'] = 'bytes=23-'
     62        self.resource.render(self.request)
     63        self.assertEquals(''.join(self.request.written), self.payload[23:])
     64        self.assertEquals(len(''.join(self.request.written)), 41)
     65        self.assertEquals(self.request.outgoingHeaders['content-range'], 'bytes 23-63/64')
     66        self.assertEquals(self.request.outgoingHeaders['content-length'], '41')
     67
     68    def testLastByteSupport(self):
     69        """Test if all is correct handled if start-byte is omitted."""
     70        self.request.headers['range'] = 'bytes=-17'
     71        self.resource.render(self.request)
     72        self.assertEquals(''.join(self.request.written), self.payload[-17:])
     73        self.assertEquals(len(''.join(self.request.written)), 17)
     74        self.assertEquals(self.request.outgoingHeaders['content-range'], 'bytes 47-63/64')
     75        self.assertEquals(self.request.outgoingHeaders['content-length'], '17')
     76
     77    def testInvalidByteRangeNoStartNoEnd(self):
     78        """Test what happens when start and end is missing."""
     79        self.request.headers['range'] = 'bytes=-'
     80        self.resource.render(self.request)
     81        self.assertEquals(self.request.responseCode, http.OK)
     82        self.assertEquals(len(''.join(self.request.written)), 64)
     83        self.assertEquals(''.join(self.request.written), self.payload)
     84
     85    def testInvalidByteRangeStartGreaterEnd(self):
     86        """Test what happens if start byte is greate than end byte."""
     87        self.request.headers['range'] = 'bytes=23-21'
     88        self.resource.render(self.request)
     89        self.assertEquals(self.request.responseCode, http.REQUESTED_RANGE_NOT_SATISFIABLE)
     90        self.assertEquals(len(''.join(self.request.written)), 0)
     91
     92    def testMultipleRanges(self):
     93        """Test the response if multiple ranges are given. As the RFC2616
     94        says, we only have to return just on range. See section 14.16
     95        paragraph starting with: "Unlike byte-ranges-specifier"."""
     96        self.request.headers['range'] = 'bytes=23-43,17-28'
     97        self.resource.render(self.request)
     98        self.assertEquals(self.request.responseCode, http.PARTIAL_CONTENT)
     99        self.assertEquals(len(''.join(self.request.written)), 21)
     100        self.assertEquals(''.join(self.request.written), self.payload[23:44])
     101
     102    def testInvalidByteRangeContainingNaNs(self):
     103        """Test whats happening if the byte range has NaN in it."""
     104        self.request.headers['range'] = 'bytes=-abc#'
     105        self.resource.render(self.request)
     106        self.assertEquals(self.request.responseCode, http.OK)
     107        self.assertEquals(len(''.join(self.request.written)), 64)
     108        self.assertEquals(''.join(self.request.written), self.payload)