From e85181d0bb2d48083a0aa3b1bb92f4c99269682d Mon Sep 17 00:00:00 2001 From: Eriq Augustine Date: Sat, 16 Nov 2024 09:02:01 -0500 Subject: [PATCH] Added support for basic (not multipart) range requests for static files. --- sharkclipper/api/handlers.py | 64 +++++++++++++++++++++++++++++++++--- sharkclipper/api/server.py | 4 ++- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/sharkclipper/api/handlers.py b/sharkclipper/api/handlers.py index 0bc5d2f..8eae79f 100644 --- a/sharkclipper/api/handlers.py +++ b/sharkclipper/api/handlers.py @@ -41,7 +41,7 @@ def static(handler, path, **kwargs): # Build the static path skipping the '/static' part of the URL path. static_path = os.path.join(STATIC_DIR, *parts[2:]) - return _serve_file(static_path, "static path not found: '%s'." % (path)) + return _serve_file(static_path, "static path not found: '%s'." % (path), **kwargs) # Get a temp file that we create/work with. def temp(handler, path, temp_dir = None, **kwargs): @@ -51,7 +51,7 @@ def temp(handler, path, temp_dir = None, **kwargs): # Build the temp path skipping the '/temp' part of the URL path. temp_path = os.path.join(temp_dir, *parts[2:]) - return _serve_file(temp_path, "temp path not found: '%s'." % (path)) + return _serve_file(temp_path, "temp path not found: '%s'." % (path), **kwargs) # Get the server version. def version(handler, path, **kwargs): @@ -163,7 +163,7 @@ def _get_exif_timestamp(time): # Format. return time.strftime(EXIF_DATETIME_FORMAT) -def _serve_file(path, not_found_message = None): +def _serve_file(path, not_found_message = None, range_header = None, **kwargs): if (not os.path.isfile(path)): if (not_found_message is None): not_found_message = "path not found: '%s'." % (path) @@ -178,4 +178,60 @@ def _serve_file(path, not_found_message = None): if (mime_info is not None): headers['Content-Type'] = mime_info[0] - return data, None, headers + code = http.HTTPStatus.OK + + data_length = len(data) + range_parts = _parseRange(range_header, data_length) + if (range_parts is not None): + data = data[range_parts[0]:(range_parts[1] + 1)] + code = http.HTTPStatus.PARTIAL_CONTENT + headers['Content-Range'] = "bytes %d-%d/%d" % (range_parts[0], range_parts[1], data_length) + + return data, code, headers + +# Parse the range header. +# Return None if a single byte range could not be parsed (multi-ranges are not supported), +# else return [startByteIndex, endByteIndex] (inclusive). +# Note that the inclusivity means that these are not the same as a Python list slice +# (you will need to add one to the end index). +# If not present, startByteIndex will be defaulted to 0 and endByteIndex will be data_length. +def _parseRange(raw_range_header, data_length): + try: + return _parseRangeHelper(raw_range_header, data_length) + except Exception as ex: + logging.warn("Cannot parse range header '%s': '%s'." % (raw_range_header, ex)) + return None + +def _parseRangeHelper(raw_range_header, data_length): + if (raw_range_header is None): + return None + + text = str(raw_range_header).strip().lower() + + parts = [part.strip() for part in text.split('=')] + if (len(parts) != 2): + raise ValueError('Bad unit split.') + + units, ranges = parts + if (units != 'bytes'): + raise ValueError('Units is not bytes.') + + parts = [part.strip() for part in ranges.split(',')] + if (len(parts) != 1): + raise ValueError('Multipart ranges not supported.') + + parts = [part.strip() for part in parts[0].split('-')] + if (len(parts) != 2): + raise ValueError('Range does not have exactly two components.') + + start = parts[0] + if (start == ''): + start = 0 + start = int(start) + + end = parts[1] + if (end == ''): + end = (data_length - 1) + end = int(end) + + return [start, end] diff --git a/sharkclipper/api/server.py b/sharkclipper/api/server.py index 0401aba..75668a7 100644 --- a/sharkclipper/api/server.py +++ b/sharkclipper/api/server.py @@ -107,10 +107,12 @@ def _read_post_file(self): def _do_request(self, **kwargs): logging.debug("Serving: " + self.path) + range_header = self.headers.get('Range', None) + code = http.HTTPStatus.OK headers = {} - result = self._route(self.path, **kwargs) + result = self._route(self.path, range_header = range_header, **kwargs) if (result is None): # All handling was done internally, the response is complete. return