Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

Changeset 549

Show
Ignore:
Timestamp:
08/23/05 12:48:57
Author:
fumanchu
Message:

1. Initial support for partial GET (Range request header): new _cphttptools.get_ranges() function for use in page handlers and serve_file().
2. New HTTPClientError exception; NotFound? now inherits from it.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/cherrypy/_cperror.py

    r529 r549  
    3939class NotReady(Error): 
    4040    """A request was made before the app server has been started.""" 
    41     pass 
    42  
    43 class NotFound(Error): 
    44     """ Happens when a URL couldn't be mapped to any class.method """ 
    4541    pass 
    4642 
     
    173169        else: 
    174170            raise ValueError("The %s status code is unknown." % status) 
     171 
     172 
     173_missing = object() 
     174 
     175class HTTPClientError(Error): 
     176    """Exception raised when the client has made an error in its request.""" 
     177     
     178    def __init__(self, status=400, body=_missing): 
     179        self.status = status = int(status) 
     180        if status < 400 or status > 499: 
     181            raise ValueError("status must be between 400 and 499.") 
     182         
     183        import cherrypy 
     184        cherrypy.response.status = status 
     185        if body is not _missing: 
     186            cherrypy.response.body = body 
     187 
     188 
     189class NotFound(HTTPClientError): 
     190    """ Happens when a URL couldn't be mapped to any class.method """ 
     191     
     192    def __init__(self, path): 
     193        self.args = (path,) 
     194        HTTPClientError.__init__(self, 404) 
  • trunk/cherrypy/_cphttptools.py

    r545 r549  
    303303            finally: 
    304304                applyFilters('onEndResource') 
    305         except cherrypy.NotFound: 
    306             cherrypy.response.status = 404 
    307             handleError(sys.exc_info()) 
    308305        except: 
     306            # This includes HTTPClientError and NotFound 
    309307            handleError(sys.exc_info()) 
    310308     
     
    580578            cherrypy.response.headerMap['Content-Length'] = len(content) 
    581579        else: 
    582             del cherrypy.response.headerMap['Content-Length'] 
     580            try: 
     581                del cherrypy.response.headerMap['Content-Length'] 
     582            except KeyError: 
     583                pass 
    583584     
    584585    # For some statuses, Internet Explorer 5+ shows "friendly error messages" 
     
    660661 
    661662 
     663def get_ranges(content_length): 
     664    """Return a list of (start, stop) indices from a Range header, or None. 
     665     
     666    Each (start, stop) tuple will be composed of two ints, which are suitable 
     667    for use in a slicing operation. That is, the header "Range: bytes=3-6", 
     668    if applied against a Python string, is requesting resource[3:7]. This 
     669    function will return the list [(3, 7)]. 
     670    """ 
     671     
     672    r = cherrypy.request.headerMap.get('Range') 
     673    if not r: 
     674        return None 
     675     
     676    result = [] 
     677    bytesunit, byteranges = r.split("=", 1) 
     678    for brange in byteranges.split(","): 
     679        start, stop = [x.strip() for x in brange.split("-", 1)] 
     680        if start: 
     681            if not stop: 
     682                stop = content_length - 1 
     683            start, stop = map(int, (start, stop)) 
     684            if start >= content_length: 
     685                # From rfc 2616 sec 14.16: 
     686                # "If the server receives a request (other than one 
     687                # including an If-Range request-header field) with an 
     688                # unsatisfiable Range request-header field (that is, 
     689                # all of whose byte-range-spec values have a first-byte-pos 
     690                # value greater than the current length of the selected 
     691                # resource), it SHOULD return a response code of 416 
     692                # (Requested range not satisfiable)." 
     693                continue 
     694            if stop < start: 
     695                # From rfc 2616 sec 14.16: 
     696                # "If the server ignores a byte-range-spec because it 
     697                # is syntactically invalid, the server SHOULD treat 
     698                # the request as if the invalid Range header field 
     699                # did not exist. (Normally, this means return a 200 
     700                # response containing the full entity)." 
     701                return None 
     702            result.append((start, stop + 1)) 
     703        else: 
     704            if not stop: 
     705                # See rfc quote above. 
     706                return None 
     707            # Negative subscript (last N bytes) 
     708            result.append((content_length - int(stop), content_length)) 
     709     
     710    if result == []: 
     711        cherrypy.response.headerMap['Content-Range'] = "bytes */%s" % content_length 
     712        b = "Invalid Range (first-byte-pos greater than Content-Length)" 
     713        raise cherrypy.HTTPClientError(416, b) 
     714     
     715    return result 
     716 
     717 
    662718def serve_file(filename): 
    663719    """Set status, headers, and body in order to serve the given file.""" 
     
    683739        ext = "" 
    684740     
     741    resp = cherrypy.response 
     742     
    685743    contentType = mimetypes.types_map.get(ext, "text/plain") 
    686     cherrypy.response.headerMap['Content-Type'] = contentType 
     744    resp.headerMap['Content-Type'] = contentType 
    687745     
    688746    strModifTime = httpdate(time.gmtime(stat.st_mtime)) 
     
    690748        # Check if if-modified-since date is the same as strModifTime 
    691749        if cherrypy.request.headerMap['If-Modified-Since'] == strModifTime: 
    692             cherrypy.response.status = "304 Not Modified" 
    693             cherrypy.response.body = [] 
     750            resp.status = "304 Not Modified" 
     751            resp.body = [] 
    694752            if getattr(cherrypy, "debug", None): 
    695753                cherrypy.log("    Found file (304 Not Modified): %s" % filename, "DEBUG") 
    696754            return 
    697     cherrypy.response.headerMap['Last-Modified'] = strModifTime 
     755    resp.headerMap['Last-Modified'] = strModifTime 
    698756     
    699757    # Set Content-Length and use an iterable (file object) 
    700758    #   this way CP won't load the whole file in memory 
    701     cherrypy.response.headerMap['Content-Length'] = stat[6] 
     759    c_len = stat[6] 
    702760    bodyfile = open(filename, 'rb') 
    703     cherrypy.response.body = fileGenerator(bodyfile) 
    704761    if getattr(cherrypy, "debug", None): 
    705762        cherrypy.log("    Found file: %s" % filename, "DEBUG") 
     763     
     764    resp.headerMap["Accept-Ranges"] = "bytes" 
     765    r = get_ranges(c_len) 
     766    if r: 
     767        if len(r) == 1: 
     768            # Return a single-part response. 
     769            start, stop = r[0] 
     770            r_len = stop - start 
     771            resp.status = "206 Partial Content" 
     772            resp.headerMap['Content-Range'] = ("bytes %s-%s/%s" % 
     773                                               (start, stop - 1, c_len)) 
     774            resp.headerMap['Content-Length'] = r_len 
     775            bodyfile.seek(start) 
     776            resp.body = [bodyfile.read(r_len)] 
     777        else: 
     778            # Return a multipart/byteranges response. 
     779            resp.status = "206 Partial Content" 
     780            import mimetools 
     781            boundary = mimetools.choose_boundary() 
     782            resp.headerMap['Content-Type'] = "multipart/byteranges; boundary=%s" % boundary 
     783            del resp.headerMap['Content-Length'] 
     784            def fileRanges(): 
     785                for start, stop in r: 
     786                    yield "--" + boundary 
     787                    yield "\nContent-type: %s" % contentType 
     788                    yield ("\nContent-range: bytes %s-%s/%s\n\n" 
     789                           % (start, stop - 1, c_len)) 
     790                    bodyfile.seek(start) 
     791                    yield bodyfile.read((stop + 1) - start) 
     792                    yield "\n" 
     793                # Final boundary 
     794                yield "--" + boundary 
     795            resp.body = fileRanges() 
     796    else: 
     797        resp.headerMap['Content-Length'] = c_len 
     798        resp.body = fileGenerator(bodyfile) 
    706799 
    707800 
  • trunk/cherrypy/test/test_core.py

    r545 r549  
    3030 
    3131import cherrypy 
     32from cherrypy import _cphttptools 
    3233import types 
    3334import os 
     
    170171        # Since status must start with an int, this should error. 
    171172        cherrypy.response.status = "ZOO OK" 
     173 
     174 
     175class Ranges(Test): 
     176     
     177    def get_ranges(self): 
     178        return repr(_cphttptools.get_ranges(8)) 
     179     
     180    def slice_file(self): 
     181        path = os.path.join(os.getcwd(), os.path.dirname(__file__)) 
     182        _cphttptools.serve_file(os.path.join(path, "static/index.html")) 
     183        # Ugly hack but it works 
     184        return cherrypy.response.body 
    172185 
    173186 
     
    446459            ignore.pop() 
    447460     
     461    def test_Ranges(self): 
     462        self.getPage("/ranges/get_ranges", [('Range', 'bytes=3-6')]) 
     463        self.assertBody("[(3, 7)]") 
     464         
     465        # Test multiple ranges and a suffix-byte-range-spec, for good measure. 
     466        self.getPage("/ranges/get_ranges", [('Range', 'bytes=2-4,-1')]) 
     467        self.assertBody("[(2, 5), (7, 8)]") 
     468         
     469        # Get a partial file. 
     470        self.getPage("/ranges/slice_file", [('Range', 'bytes=2-5')]) 
     471        self.assertStatus("206 Partial Content") 
     472        self.assertHeader("Content-Type", "text/html") 
     473        self.assertHeader("Content-Range", "bytes 2-5/14") 
     474        self.assertBody("llo,") 
     475         
     476        # What happens with overlapping ranges (and out of order, too)? 
     477        self.getPage("/ranges/slice_file", [('Range', 'bytes=4-6,2-5')]) 
     478        self.assertStatus("206 Partial Content") 
     479        ct = "" 
     480        for k, v in self.headers: 
     481            if k.lower() == "content-type": 
     482                ct = v 
     483                break 
     484        expected_type = "multipart/byteranges; boundary=" 
     485        self.assert_(ct.startswith(expected_type)) 
     486        boundary = ct[len(expected_type):] 
     487        expected_body = """--%s 
     488Content-type: text/html 
     489Content-range: bytes 4-6/14 
     490 
     491o, w 
     492--%s 
     493Content-type: text/html 
     494Content-range: bytes 2-5/14 
     495 
     496llo,  
     497--%s""" % (boundary, boundary, boundary) 
     498        self.assertBody(expected_body) 
     499        self.assertNoHeader("Content-Length") 
     500         
     501        # Test "416 Requested Range Not Satisfiable" 
     502        self.getPage("/ranges/slice_file", [('Range', 'bytes=2300-2900')]) 
     503        self.assertStatus("416 Requested Range Not Satisfiable") 
     504        self.assertHeader("Content-Range", "bytes */14") 
     505     
    448506    def testHeaderCaseSensitivity(self): 
    449507        # Tests that each header only appears once, regardless of case. 

Hosted by WebFaction

Log in as guest/cpguest to create tickets