Allow chunked image upload in v2 API

* Related to bp api-v2-image-upload

Change-Id: I728cdd28cab1d63d67cd7ad6c9d33535af4d5535
This commit is contained in:
Brian Waldon 2012-05-08 15:39:00 -07:00
parent 2c102130ed
commit 7f761e16ce
4 changed files with 79 additions and 16 deletions

View File

@ -41,15 +41,16 @@ class ImageDataController(base.Controller):
except exception.NotFound: except exception.NotFound:
raise webob.exc.HTTPNotFound(_("Image does not exist")) raise webob.exc.HTTPNotFound(_("Image does not exist"))
def upload(self, req, image_id, data): def upload(self, req, image_id, data, size):
self._get_image(req.context, image_id) self._get_image(req.context, image_id)
try: try:
location, size, checksum = self.store_api.add_to_backend( location, size, checksum = self.store_api.add_to_backend(
'file', image_id, data, len(data)) 'file', image_id, data, size)
except exception.Duplicate: except exception.Duplicate:
raise webob.exc.HTTPConflict() raise webob.exc.HTTPConflict()
self.db_api.image_update(req.context, image_id, {'location': location}) values = {'location': location, 'size': size}
self.db_api.image_update(req.context, image_id, values)
def download(self, req, image_id): def download(self, req, image_id):
image = self._get_image(req.context, image_id) image = self._get_image(req.context, image_id)
@ -63,7 +64,13 @@ class ImageDataController(base.Controller):
class RequestDeserializer(wsgi.JSONRequestDeserializer): class RequestDeserializer(wsgi.JSONRequestDeserializer):
def upload(self, request): def upload(self, request):
return {'data': request.body} try:
request.get_content_type('application/octet-stream')
except exception.InvalidContentType:
raise webob.exc.HTTPUnsupportedMediaType()
image_size = request.content_length or None
return {'size': image_size, 'data': request.body_file}
class ResponseSerializer(wsgi.JSONResponseSerializer): class ResponseSerializer(wsgi.JSONResponseSerializer):

View File

@ -105,7 +105,7 @@ class TestImages(functional.FunctionalTest):
# Upload some image data # Upload some image data
path = self._url('/v2/images/%s/file' % image_id) path = self._url('/v2/images/%s/file' % image_id)
headers = self._headers() headers = self._headers({'Content-Type': 'application/octet-stream'})
response = requests.put(path, headers=headers, data='ZZZZZ') response = requests.put(path, headers=headers, data='ZZZZZ')
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
@ -155,13 +155,13 @@ class TestImages(functional.FunctionalTest):
# Upload some image data # Upload some image data
path = self._url('/v2/images/%s/file' % image_id) path = self._url('/v2/images/%s/file' % image_id)
headers = self._headers() headers = self._headers({'Content-Type': 'application/octet-stream'})
response = requests.put(path, headers=headers, data='ZZZZZ') response = requests.put(path, headers=headers, data='ZZZZZ')
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
# Uploading duplicate data should be rejected with a 409 # Uploading duplicate data should be rejected with a 409
path = self._url('/v2/images/%s/file' % image_id) path = self._url('/v2/images/%s/file' % image_id)
headers = self._headers() headers = self._headers({'Content-Type': 'application/octet-stream'})
response = requests.put(path, headers=headers, data='XXX') response = requests.put(path, headers=headers, data='XXX')
self.assertEqual(409, response.status_code) self.assertEqual(409, response.status_code)

View File

@ -16,11 +16,9 @@
import logging import logging
import uuid import uuid
import webob
import glance.common.context import glance.common.context
from glance.common import exception from glance.common import exception
from glance.common import utils from glance.common import wsgi
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -34,7 +32,7 @@ USER1 = '54492ba0-f4df-4e4e-be62-27f4d76b29cf'
USER2 = '0b3b3006-cb76-4517-ae32-51397e22c754' USER2 = '0b3b3006-cb76-4517-ae32-51397e22c754'
class FakeRequest(webob.Request): class FakeRequest(wsgi.Request):
def __init__(self): def __init__(self):
#TODO(bcwaldon): figure out how to fake this out cleanly #TODO(bcwaldon): figure out how to fake this out cleanly
super(FakeRequest, self).__init__({'REQUEST_METHOD': 'POST'}) super(FakeRequest, self).__init__({'REQUEST_METHOD': 'POST'})
@ -204,6 +202,6 @@ class FakeStoreAPI(object):
def add_to_backend(self, scheme, image_id, data, size): def add_to_backend(self, scheme, image_id, data, size):
if image_id in self.data: if image_id in self.data:
raise exception.Duplicate() raise exception.Duplicate()
self.data[image_id] = (data, size) self.data[image_id] = (data, size or len(data))
checksum = 'Z' checksum = 'Z'
return (image_id, size, checksum) return (image_id, size, checksum)

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import StringIO
import unittest import unittest
import webob import webob
@ -53,7 +54,7 @@ class TestImagesController(unittest.TestCase):
def test_upload_download(self): def test_upload_download(self):
request = test_utils.FakeRequest() request = test_utils.FakeRequest()
self.controller.upload(request, test_utils.UUID2, 'YYYY') self.controller.upload(request, test_utils.UUID2, 'YYYY', 4)
output = self.controller.download(request, test_utils.UUID2) output = self.controller.download(request, test_utils.UUID2)
expected = {'data': 'YYYY', 'size': 4} expected = {'data': 'YYYY', 'size': 4}
self.assertEqual(expected, output) self.assertEqual(expected, output)
@ -61,12 +62,19 @@ class TestImagesController(unittest.TestCase):
def test_upload_non_existant_image(self): def test_upload_non_existant_image(self):
request = test_utils.FakeRequest() request = test_utils.FakeRequest()
self.assertRaises(webob.exc.HTTPNotFound, self.controller.upload, self.assertRaises(webob.exc.HTTPNotFound, self.controller.upload,
request, utils.generate_uuid(), 'YYYY') request, utils.generate_uuid(), 'YYYY', 4)
def test_upload_data_exists(self): def test_upload_data_exists(self):
request = test_utils.FakeRequest() request = test_utils.FakeRequest()
self.assertRaises(webob.exc.HTTPConflict, self.controller.upload, self.assertRaises(webob.exc.HTTPConflict, self.controller.upload,
request, test_utils.UUID1, 'YYYY') request, test_utils.UUID1, 'YYYY', 4)
def test_upload_download_no_size(self):
request = test_utils.FakeRequest()
self.controller.upload(request, test_utils.UUID2, 'YYYY', None)
output = self.controller.download(request, test_utils.UUID2)
expected = {'data': 'YYYY', 'size': 4}
self.assertEqual(expected, output)
class TestImageDataDeserializer(unittest.TestCase): class TestImageDataDeserializer(unittest.TestCase):
@ -75,11 +83,61 @@ class TestImageDataDeserializer(unittest.TestCase):
def test_upload(self): def test_upload(self):
request = test_utils.FakeRequest() request = test_utils.FakeRequest()
request.headers['Content-Type'] = 'application/octet-stream'
request.body = 'YYY' request.body = 'YYY'
request.headers['Content-Length'] = 3
output = self.deserializer.upload(request) output = self.deserializer.upload(request)
expected = {'data': 'YYY'} data = output.pop('data')
self.assertEqual(data.getvalue(), 'YYY')
expected = {'size': 3}
self.assertEqual(expected, output) self.assertEqual(expected, output)
def test_upload_chunked(self):
request = test_utils.FakeRequest()
request.headers['Content-Type'] = 'application/octet-stream'
# If we use body_file, webob assumes we want to do a chunked upload,
# ignoring the Content-Length header
request.body_file = StringIO.StringIO('YYY')
output = self.deserializer.upload(request)
data = output.pop('data')
self.assertEqual(data.getvalue(), 'YYY')
expected = {'size': None}
self.assertEqual(expected, output)
def test_upload_chunked_with_content_length(self):
request = test_utils.FakeRequest()
request.headers['Content-Type'] = 'application/octet-stream'
request.body_file = StringIO.StringIO('YYY')
# The deserializer shouldn't care if the Content-Length is
# set when the user is attempting to send chunked data.
request.headers['Content-Length'] = 3
output = self.deserializer.upload(request)
data = output.pop('data')
self.assertEqual(data.getvalue(), 'YYY')
expected = {'size': 3}
self.assertEqual(expected, output)
def test_upload_with_incorrect_content_length(self):
request = test_utils.FakeRequest()
request.headers['Content-Type'] = 'application/octet-stream'
# The deserializer shouldn't care if the Content-Length and
# actual request body length differ. That job is left up
# to the controller
request.body = 'YYY'
request.headers['Content-Length'] = 4
output = self.deserializer.upload(request)
data = output.pop('data')
self.assertEqual(data.getvalue(), 'YYY')
expected = {'size': 4}
self.assertEqual(expected, output)
def test_upload_wrong_content_type(self):
request = test_utils.FakeRequest()
request.headers['Content-Type'] = 'application/json'
request.body = 'YYYYY'
self.assertRaises(webob.exc.HTTPUnsupportedMediaType,
self.deserializer.upload, request)
class TestImageDataSerializer(unittest.TestCase): class TestImageDataSerializer(unittest.TestCase):
def setUp(self): def setUp(self):