From 0f0d0473b1e21ade51c76d41a690b52bda8c57e7 Mon Sep 17 00:00:00 2001 From: Marsch Huynh Date: Sun, 20 Aug 2017 22:05:50 +0700 Subject: [PATCH] Support partial request for media resource --- docs/features.rst | 22 +++++++++++ eve/endpoints.py | 94 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 94 insertions(+), 22 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index 8e8de04b4..8ba11f1ec 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1776,6 +1776,28 @@ configuration. Enable ``AUTO_COLLAPSE_MULTI_KEYS`` and ``AUTO_CREATE_LISTS`` to make this possible. This allows to send multiple values for one key in ``multipart/form-data`` requests and in this way upload a list of files. +.. _partial_request: + +Partial request for media resource +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The partial request provides an ability to download a part of a file. +You can also pause and resume when you are downloading without restarting +your download. To use it, make sure you have ``Range`` in your request header. + + .. code-block:: console + + $ curl http://localhost/media/yourfilename -i -H "Range: bytes=0-10" + HTTP/1.1 206 PARTIAL CONTENT + Date: Sun, 20 Aug 2017 14:26:42 GMT + Content-Type: audio/mp4 + Content-Length: 11 + Connection: keep-alive + Content-Range: bytes 0-10/23671 + Last-Modified: Sat, 19 Aug 2017 03:25:36 GMT + Accept-Ranges: bytes + + ftypmp4% + .. _geojson_feature: GeoJSON diff --git a/eve/endpoints.py b/eve/endpoints.py index fc598bfd3..eb5a750ca 100644 --- a/eve/endpoints.py +++ b/eve/endpoints.py @@ -11,6 +11,8 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ +import re + from bson import tz_util from flask import abort, request, current_app as app, Response @@ -175,28 +177,76 @@ def media_endpoint(_id): .. versionadded:: 0.6 """ - file_ = app.media.get(_id) - if file_ is None: - return abort(404) - - if_modified_since = weak_date(request.headers.get('If-Modified-Since')) - if if_modified_since is not None: - if if_modified_since.tzinfo is None: - if_modified_since = if_modified_since.replace( - tzinfo=tz_util.utc) - - if if_modified_since > file_.upload_date: - return Response(status=304) - - headers = { - 'Last-Modified': date_to_rfc1123(file_.upload_date), - 'Content-Length': file_.length, - } - - response = Response(file_, headers=headers, mimetype=file_.content_type, - direct_passthrough=True) - - return response + range_header = request.headers.get('Range', None) + if not range_header: + file_ = app.media.get(_id) + if file_ is None: + return abort(404) + + if_modified_since = weak_date(request.headers.get('If-Modified-Since')) + if if_modified_since is not None: + if if_modified_since.tzinfo is None: + if_modified_since = if_modified_since.replace( + tzinfo=tz_util.utc) + + if if_modified_since > file_.upload_date: + return Response(status=304) + + headers = { + 'Last-Modified': date_to_rfc1123(file_.upload_date), + 'Content-Length': file_.length, + 'Accept-Ranges': 'bytes', + } + + response = Response( + file_, + status=200, + headers=headers, + mimetype=file_.content_type, + direct_passthrough=True + ) + + return response + else: + file_ = app.media.get(_id) + size = file_.length + byte1, byte2 = 0, None + + m = re.search('(\d+)-(\d*)', range_header) + g = m.groups() + + if g[0]: + byte1 = int(g[0]) + if g[1]: + byte2 = int(g[1]) + + length = size - byte1 + if byte2 is not None: + length = byte2 - byte1 + 1 + + data = None + file_.seek(byte1) + data = file_.read(length) + + headers = { + 'Last-Modified': date_to_rfc1123(file_.upload_date), + 'Content-Length': file_.length, + 'Accept-Ranges': 'bytes', + 'Content-Range': 'bytes {0}-{1}/{2}'.format( + byte1, + byte1 + length - 1, + size + ), + } + + response = Response( + data, + 206, + headers=headers, + mimetype=file_.content_type, + direct_passthrough=True) + + return response @requires_auth('resource')