Changeset 549
- Timestamp:
- 08/23/05 12:48:57
- Files:
-
- trunk/cherrypy/_cperror.py (modified) (2 diffs)
- trunk/cherrypy/_cphttptools.py (modified) (5 diffs)
- trunk/cherrypy/test/test_core.py (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/cherrypy/_cperror.py
r529 r549 39 39 class NotReady(Error): 40 40 """A request was made before the app server has been started.""" 41 pass42 43 class NotFound(Error):44 """ Happens when a URL couldn't be mapped to any class.method """45 41 pass 46 42 … … 173 169 else: 174 170 raise ValueError("The %s status code is unknown." % status) 171 172 173 _missing = object() 174 175 class 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 189 class 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 303 303 finally: 304 304 applyFilters('onEndResource') 305 except cherrypy.NotFound:306 cherrypy.response.status = 404307 handleError(sys.exc_info())308 305 except: 306 # This includes HTTPClientError and NotFound 309 307 handleError(sys.exc_info()) 310 308 … … 580 578 cherrypy.response.headerMap['Content-Length'] = len(content) 581 579 else: 582 del cherrypy.response.headerMap['Content-Length'] 580 try: 581 del cherrypy.response.headerMap['Content-Length'] 582 except KeyError: 583 pass 583 584 584 585 # For some statuses, Internet Explorer 5+ shows "friendly error messages" … … 660 661 661 662 663 def 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 662 718 def serve_file(filename): 663 719 """Set status, headers, and body in order to serve the given file.""" … … 683 739 ext = "" 684 740 741 resp = cherrypy.response 742 685 743 contentType = mimetypes.types_map.get(ext, "text/plain") 686 cherrypy.response.headerMap['Content-Type'] = contentType744 resp.headerMap['Content-Type'] = contentType 687 745 688 746 strModifTime = httpdate(time.gmtime(stat.st_mtime)) … … 690 748 # Check if if-modified-since date is the same as strModifTime 691 749 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 = [] 694 752 if getattr(cherrypy, "debug", None): 695 753 cherrypy.log(" Found file (304 Not Modified): %s" % filename, "DEBUG") 696 754 return 697 cherrypy.response.headerMap['Last-Modified'] = strModifTime755 resp.headerMap['Last-Modified'] = strModifTime 698 756 699 757 # Set Content-Length and use an iterable (file object) 700 758 # this way CP won't load the whole file in memory 701 c herrypy.response.headerMap['Content-Length']= stat[6]759 c_len = stat[6] 702 760 bodyfile = open(filename, 'rb') 703 cherrypy.response.body = fileGenerator(bodyfile)704 761 if getattr(cherrypy, "debug", None): 705 762 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) 706 799 707 800 trunk/cherrypy/test/test_core.py
r545 r549 30 30 31 31 import cherrypy 32 from cherrypy import _cphttptools 32 33 import types 33 34 import os … … 170 171 # Since status must start with an int, this should error. 171 172 cherrypy.response.status = "ZOO OK" 173 174 175 class 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 172 185 173 186 … … 446 459 ignore.pop() 447 460 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 488 Content-type: text/html 489 Content-range: bytes 4-6/14 490 491 o, w 492 --%s 493 Content-type: text/html 494 Content-range: bytes 2-5/14 495 496 llo, 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 448 506 def testHeaderCaseSensitivity(self): 449 507 # Tests that each header only appears once, regardless of case.

