def _parse_request_range(range_header):
"""Parses a Range header.
- Returns either ``None`` or an instance of ``slice``:
-
- >>> rh = _parse_request_range("bytes=1-2")
- >>> rh
- slice(1, 3, None)
- >>> [0, 1, 2, 3, 4][rh]
+ 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.
+
+ >>> 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")
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()
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)
% 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:
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.
``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):
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