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): def _parse_request_range(range_header):
"""Parses a 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") >>> start, end = _parse_request_range("bytes=1-2")
>>> rh >>> start, end
slice(1, 3, None) (1, 3)
>>> [0, 1, 2, 3, 4][rh] >>> [0, 1, 2, 3, 4][start:end]
[1, 2] [1, 2]
>>> _parse_request_range("bytes=6-") >>> _parse_request_range("bytes=6-")
slice(6, None, None) (6, None)
>>> _parse_request_range("bytes=-6") >>> _parse_request_range("bytes=-6")
slice(-6, None, None) (-6, None)
>>> _parse_request_range("bytes=") >>> _parse_request_range("bytes=")
slice(None, None, None) (None, None)
>>> _parse_request_range("foo=42") >>> _parse_request_range("foo=42")
>>> _parse_request_range("bytes=1-2,6-10") >>> _parse_request_range("bytes=1-2,6-10")
@ -276,26 +278,21 @@ def _parse_request_range(range_header):
end = None end = None
else: else:
end += 1 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: """Returns a suitable Content-Range header:
>>> print(_get_content_range("abcd", slice(None, 1))) >>> print(_get_content_range(None, 1, 4))
0-0/4 0-0/4
>>> print(_get_content_range("abcd", slice(1, 3))) >>> print(_get_content_range(1, 3, 4))
1-2/4 1-2/4
>>> print(_get_content_range("abcd", slice(None, None))) >>> print(_get_content_range(None, None, 4))
0-3/4 0-3/4
""" """
data_len = len(data)
start, stop = request_range.start, request_range.stop
start = start or 0 start = start or 0
if start < 0: end = (end or total) - 1
start = data_len + start return "%s-%s/%s" % (start, end, total)
stop = (stop or data_len) - 1
return "%s-%s/%s" %(start, stop, data_len)
def _int_or_none(val): def _int_or_none(val):
val = val.strip() val = val.strip()

View File

@ -989,7 +989,8 @@ class CustomStaticFileTest(WebTestCase):
return absolute_path return absolute_path
@classmethod @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': if path == 'CustomStaticFileTest:foo.txt':
return b'bar' return b'bar'
raise Exception("unexpected path %r" % path) raise Exception("unexpected path %r" % path)

View File

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