Add start and end parameters to get_content() instead of reading everything.

This commit is contained in:
Ben Darnell 2013-05-19 13:57:31 -04:00
parent e59aa2f438
commit 2e73d4b635
3 changed files with 51 additions and 28 deletions

View File

@ -238,19 +238,21 @@ class HTTPFile(ObjectDict):
def _parse_request_range(range_header):
"""Parses a Range header.
Returns either ``None`` or an instance of ``slice``:
Returns either ``None`` or tuple ``(start, end)``.
Note that while the HTTP headers use inclusive byte positions,
this method returns indexes suitable for use in slices.
>>> rh = _parse_request_range("bytes=1-2")
>>> rh
slice(1, 3, None)
>>> [0, 1, 2, 3, 4][rh]
>>> start, end = _parse_request_range("bytes=1-2")
>>> start, end
(1, 3)
>>> [0, 1, 2, 3, 4][start:end]
[1, 2]
>>> _parse_request_range("bytes=6-")
slice(6, None, None)
(6, None)
>>> _parse_request_range("bytes=-6")
slice(-6, None, None)
(-6, None)
>>> _parse_request_range("bytes=")
slice(None, None, None)
(None, None)
>>> _parse_request_range("foo=42")
>>> _parse_request_range("bytes=1-2,6-10")
@ -276,26 +278,21 @@ def _parse_request_range(range_header):
end = None
else:
end += 1
return slice(start, end)
return (start, end)
def _get_content_range(data, request_range):
def _get_content_range(start, end, total):
"""Returns a suitable Content-Range header:
>>> print(_get_content_range("abcd", slice(None, 1)))
>>> print(_get_content_range(None, 1, 4))
0-0/4
>>> print(_get_content_range("abcd", slice(1, 3)))
>>> print(_get_content_range(1, 3, 4))
1-2/4
>>> print(_get_content_range("abcd", slice(None, None)))
>>> print(_get_content_range(None, None, 4))
0-3/4
"""
data_len = len(data)
start, stop = request_range.start, request_range.stop
start = start or 0
if start < 0:
start = data_len + start
stop = (stop or data_len) - 1
return "%s-%s/%s" %(start, stop, data_len)
end = (end or total) - 1
return "%s-%s/%s" % (start, end, total)
def _int_or_none(val):
val = val.strip()

View File

@ -989,7 +989,8 @@ class CustomStaticFileTest(WebTestCase):
return absolute_path
@classmethod
def get_content(self, path):
def get_content(self, path, start=None, end=None):
assert start is None and end is None
if path == 'CustomStaticFileTest:foo.txt':
return b'bar'
raise Exception("unexpected path %r" % path)

View File

@ -1799,12 +1799,17 @@ class StaticFileHandler(RequestHandler):
% range_header)
return
data = self.get_content(self.absolute_path)
if request_range:
start, end = request_range
size = self.get_content_size()
if start < 0:
start += size
self.set_status(206) # Partial Content
content_range = httputil._get_content_range(data, request_range)
self.set_header("Content-Range", content_range)
data = data[request_range]
self.set_header("Content-Range",
httputil._get_content_range(start, end, size))
else:
start = end = None
data = self.get_content(self.absolute_path, start, end)
if include_body:
self.write(data)
else:
@ -1909,7 +1914,7 @@ class StaticFileHandler(RequestHandler):
return absolute_path
@classmethod
def get_content(cls, abspath):
def get_content(cls, abspath, start=None, end=None):
"""Retrieve the content of the requested resource which is located
at the given absolute path.
@ -1919,7 +1924,13 @@ class StaticFileHandler(RequestHandler):
``abspath`` is able to stand on its own as a cache key.
"""
with open(abspath, "rb") as file:
return file.read()
if start is not None:
file.seek(start)
if end is not None:
remaining = end - (start or 0)
return file.read(remaining)
else:
return file.read()
@classmethod
def get_content_version(cls, abspath):
@ -1931,13 +1942,27 @@ class StaticFileHandler(RequestHandler):
data = cls.get_content(abspath)
return hashlib.md5(data).hexdigest()
def _stat(self):
if not hasattr(self, '_stat_result'):
self._stat_result = os.stat(self.absolute_path)
return self._stat_result
def get_content_size(self):
"""Retrieve the total size of the resource at the given path.
This method may be overridden by subclasses. It will only
be called if a partial result is requested from `get_content`
"""
stat_result = self._stat()
return stat_result[stat.ST_SIZE]
def get_modified_time(self):
"""Returns the time that ``self.absolute_path`` was last modified.
May be overridden in subclasses. Should return a `~datetime.datetime`
object or None.
"""
stat_result = os.stat(self.absolute_path)
stat_result = self._stat()
modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
return modified