Merge "Handling HTTP range requests in Glance"
This commit is contained in:
commit
6d7b15c4e4
@ -18,6 +18,7 @@ from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import excutils
|
||||
import six
|
||||
import webob.exc
|
||||
|
||||
import glance.api.policy
|
||||
@ -282,17 +283,20 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
|
||||
|
||||
def download(self, response, image):
|
||||
offset, chunk_size = 0, None
|
||||
range_val = response.request.get_content_range()
|
||||
# NOTE(dharinic): In case of a malformed content range,
|
||||
# glance/common/wsgi.py will raise HTTPRequestRangeNotSatisfiable
|
||||
# (setting status_code to 416)
|
||||
range_val = response.request.get_content_range(image.size)
|
||||
|
||||
if range_val:
|
||||
# NOTE(flaper87): if not present, both, start
|
||||
# and stop, will be None.
|
||||
if range_val.start is not None:
|
||||
if range_val.start is not None and range_val.stop is not None:
|
||||
offset = range_val.start
|
||||
|
||||
if range_val.stop is not None:
|
||||
chunk_size = range_val.stop - offset
|
||||
|
||||
response.status_int = 206
|
||||
|
||||
response.headers['Content-Type'] = 'application/octet-stream'
|
||||
|
||||
try:
|
||||
@ -301,6 +305,11 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
|
||||
# an iterator very strange
|
||||
response.app_iter = iter(image.get_data(offset=offset,
|
||||
chunk_size=chunk_size))
|
||||
# NOTE(dharinic): In case of a full image download, when
|
||||
# chunk_size was none, reset it to image.size to set the
|
||||
# response header's Content-Length.
|
||||
if not chunk_size:
|
||||
chunk_size = image.size
|
||||
except glance_store.NotFound as e:
|
||||
raise webob.exc.HTTPNoContent(explanation=e.msg)
|
||||
except glance_store.RemoteServiceUnavailable as e:
|
||||
@ -318,7 +327,7 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
|
||||
response.headers['Content-MD5'] = image.checksum
|
||||
# NOTE(markwash): "response.app_iter = ..." also erroneously resets the
|
||||
# content-length
|
||||
response.headers['Content-Length'] = str(image.size)
|
||||
response.headers['Content-Length'] = six.text_type(chunk_size)
|
||||
|
||||
def upload(self, response, result):
|
||||
response.status_int = 204
|
||||
|
@ -972,14 +972,17 @@ class Request(webob.Request):
|
||||
langs = i18n.get_available_languages('glance')
|
||||
return self.accept_language.best_match(langs)
|
||||
|
||||
def get_content_range(self):
|
||||
def get_content_range(self, image_size):
|
||||
"""Return the `Range` in a request."""
|
||||
range_str = self.headers.get('Content-Range')
|
||||
if range_str is not None:
|
||||
range_ = webob.byterange.ContentRange.parse(range_str)
|
||||
if range_ is None:
|
||||
# NOTE(dharinic): Ensure that a range like 1-4/* for an image
|
||||
# size of 3 is invalidated.
|
||||
if range_ is None or (range_.length is None and
|
||||
range_.stop > image_size):
|
||||
msg = _('Malformed Content-Range header: %s') % range_str
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
raise webob.exc.HTTPRequestRangeNotSatisfiable(explanation=msg)
|
||||
return range_
|
||||
|
||||
|
||||
|
@ -771,10 +771,18 @@ class TestImages(functional.FunctionalTest):
|
||||
headers = self._headers({'Content-Range': content_range})
|
||||
path = self._url('/v2/images/%s/file' % image_id)
|
||||
response = requests.get(path, headers=headers)
|
||||
self.assertEqual(206, response.status_code)
|
||||
result_body += response.text
|
||||
|
||||
self.assertEqual(result_body, image_data)
|
||||
|
||||
# test for failure on unsatisfiable request range.
|
||||
content_range = 'bytes 3-16/15'
|
||||
headers = self._headers({'Content-Range': content_range})
|
||||
path = self._url('/v2/images/%s/file' % image_id)
|
||||
response = requests.get(path, headers=headers)
|
||||
self.assertEqual(416, response.status_code)
|
||||
|
||||
self.stop_servers()
|
||||
|
||||
def test_download_policy_when_cache_is_not_enabled(self):
|
||||
|
@ -67,7 +67,7 @@ class RequestTest(test_utils.BaseTestCase):
|
||||
def test_content_range(self):
|
||||
request = wsgi.Request.blank('/tests/123')
|
||||
request.headers["Content-Range"] = 'bytes 10-99/*'
|
||||
range_ = request.get_content_range()
|
||||
range_ = request.get_content_range(120)
|
||||
self.assertEqual(10, range_.start)
|
||||
self.assertEqual(100, range_.stop) # non-inclusive
|
||||
self.assertIsNone(range_.length)
|
||||
@ -75,8 +75,8 @@ class RequestTest(test_utils.BaseTestCase):
|
||||
def test_content_range_invalid(self):
|
||||
request = wsgi.Request.blank('/tests/123')
|
||||
request.headers["Content-Range"] = 'bytes=0-99'
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
request.get_content_range)
|
||||
self.assertRaises(webob.exc.HTTPRequestRangeNotSatisfiable,
|
||||
request.get_content_range, 120)
|
||||
|
||||
def test_content_type_missing(self):
|
||||
request = wsgi.Request.blank('/tests/123')
|
||||
|
@ -548,6 +548,54 @@ class TestImageDataSerializer(test_utils.BaseTestCase):
|
||||
self.assertEqual('application/octet-stream',
|
||||
response.headers['Content-Type'])
|
||||
|
||||
def _test_partial_download_successful(self, d_range):
|
||||
request = wsgi.Request.blank('/')
|
||||
request.environ = {}
|
||||
request.headers['Content-Range'] = d_range
|
||||
response = webob.Response()
|
||||
response.request = request
|
||||
image = FakeImage(size=3, data=[b'Z', b'Z', b'Z'])
|
||||
self.serializer.download(response, image)
|
||||
self.assertEqual(206, response.status_code)
|
||||
self.assertEqual('2', response.headers['Content-Length'])
|
||||
|
||||
def test_partial_download_successful_with_range(self):
|
||||
self._test_partial_download_successful('bytes 1-2/3')
|
||||
self._test_partial_download_successful('bytes 1-2/*')
|
||||
|
||||
def _test_partial_download_failures(self, d_range):
|
||||
request = wsgi.Request.blank('/')
|
||||
request.environ = {}
|
||||
request.headers['Content-Range'] = d_range
|
||||
response = webob.Response()
|
||||
response.request = request
|
||||
image = FakeImage(size=3, data=[b'Z', b'Z', b'Z'])
|
||||
self.assertRaises(webob.exc.HTTPRequestRangeNotSatisfiable,
|
||||
self.serializer.download,
|
||||
response, image)
|
||||
return
|
||||
|
||||
def test_partial_download_failure_with_range(self):
|
||||
self._test_partial_download_failures('bytes 1-4/3')
|
||||
self._test_partial_download_failures('bytes 1-4/*')
|
||||
self._test_partial_download_failures('bytes 4-1/3')
|
||||
self._test_partial_download_failures('bytes 4-1/*')
|
||||
|
||||
def test_download_failure_with_valid_range(self):
|
||||
with mock.patch.object(glance.api.policy.ImageProxy,
|
||||
'get_data') as mock_get_data:
|
||||
mock_get_data.side_effect = glance_store.NotFound(image="image")
|
||||
request = wsgi.Request.blank('/')
|
||||
request.environ = {}
|
||||
request.headers['Content-Range'] = 'bytes %s-%s/3' % (1, 2)
|
||||
response = webob.Response()
|
||||
response.request = request
|
||||
image = FakeImage(size=3, data=[b'Z', b'Z', b'Z'])
|
||||
image.get_data = mock_get_data
|
||||
self.assertRaises(webob.exc.HTTPNoContent,
|
||||
self.serializer.download,
|
||||
response, image)
|
||||
|
||||
def test_download_with_checksum(self):
|
||||
request = wsgi.Request.blank('/')
|
||||
request.environ = {}
|
||||
|
Loading…
Reference in New Issue
Block a user