diff --git a/tornado/httputil.py b/tornado/httputil.py index 23705def..36d7dc8f 100644 --- a/tornado/httputil.py +++ b/tornado/httputil.py @@ -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() diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py index 195269bb..89c549c2 100644 --- a/tornado/test/web_test.py +++ b/tornado/test/web_test.py @@ -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) diff --git a/tornado/web.py b/tornado/web.py index 8ecaaedd..78d7b0ab 100644 --- a/tornado/web.py +++ b/tornado/web.py @@ -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