diff --git a/etc/glance-api-paste.ini b/etc/glance-api-paste.ini index 0b29bc9d56..8c1cfdaf3d 100644 --- a/etc/glance-api-paste.ini +++ b/etc/glance-api-paste.ini @@ -55,3 +55,6 @@ paste.filter_factory = glance.api.middleware.context:UnauthenticatedContextMiddl [filter:authtoken] paste.filter_factory = keystoneclient.middleware.auth_token:filter_factory delay_auth_decision = true + +[filter:gzip] +paste.filter_factory = glance.api.middleware.gzip:GzipMiddleware.factory diff --git a/glance/api/middleware/gzip.py b/glance/api/middleware/gzip.py new file mode 100644 index 0000000000..922800ef82 --- /dev/null +++ b/glance/api/middleware/gzip.py @@ -0,0 +1,65 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Use gzip compression if the client accepts it. +""" + +import re + +from glance.common import wsgi +import glance.openstack.common.log as logging + +LOG = logging.getLogger(__name__) + + +class GzipMiddleware(wsgi.Middleware): + + re_zip = re.compile(r'\bgzip\b') + + def __init__(self, app): + LOG.info(_("Initialized gzip middleware")) + super(GzipMiddleware, self).__init__(app) + + def process_response(self, response): + request = response.request + accept_encoding = request.headers.get('Accept-Encoding', '') + + if self.re_zip.search(accept_encoding): + # NOTE(flaper87): Webob removes the content-md5 when + # app_iter is called. We'll keep it and reset it later + checksum = response.headers.get("Content-MD5") + + # NOTE(flaper87): We'll use lazy for images so + # that they can be compressed without reading + # the whole content in memory. Notice that using + # lazy will set response's content-length to 0. + content_type = response.headers["Content-Type"] + lazy = content_type == "application/octet-stream" + + # NOTE(flaper87): Webob takes care of the compression + # process, it will replace the body either with a + # compressed body or a generator - used for lazy com + # pression - depending on the lazy value. + # + # Webob itself will set the Content-Encoding header. + response.encode_content(lazy=lazy) + + if checksum: + response.headers['Content-MD5'] = checksum + + return response diff --git a/glance/common/wsgi.py b/glance/common/wsgi.py index aef5d10e29..7349316a95 100644 --- a/glance/common/wsgi.py +++ b/glance/common/wsgi.py @@ -548,6 +548,7 @@ class Resource(object): may raise a webob.exc exception or return a dict, which will be serialized by requested content type. """ + def __init__(self, controller, deserializer=None, serializer=None): """ :param controller: object that implement methods created by routes lib diff --git a/glance/tests/functional/__init__.py b/glance/tests/functional/__init__.py index 0a24cd6abe..87586c0d7d 100644 --- a/glance/tests/functional/__init__.py +++ b/glance/tests/functional/__init__.py @@ -354,24 +354,25 @@ def __init__(self, test_dir, port, policy_file, delayed_delete=False, flavor = %(deployment_flavor)s """ self.paste_conf_base = """[pipeline:glance-api] -pipeline = versionnegotiation unauthenticated-context rootapp +pipeline = versionnegotiation gzip unauthenticated-context rootapp [pipeline:glance-api-caching] -pipeline = versionnegotiation unauthenticated-context cache rootapp +pipeline = versionnegotiation gzip unauthenticated-context cache rootapp [pipeline:glance-api-cachemanagement] pipeline = versionnegotiation + gzip unauthenticated-context cache cache_manage rootapp [pipeline:glance-api-fakeauth] -pipeline = versionnegotiation fakeauth context rootapp +pipeline = versionnegotiation gzip fakeauth context rootapp [pipeline:glance-api-noauth] -pipeline = versionnegotiation context rootapp +pipeline = versionnegotiation gzip context rootapp [composite:rootapp] paste.composite_factory = glance.api:root_app_factory @@ -392,6 +393,9 @@ def __init__(self, test_dir, port, policy_file, delayed_delete=False, paste.filter_factory = glance.api.middleware.version_negotiation:VersionNegotiationFilter.factory +[filter:gzip] +paste.filter_factory = glance.api.middleware.gzip:GzipMiddleware.factory + [filter:cache] paste.filter_factory = glance.api.middleware.cache:CacheFilter.factory diff --git a/glance/tests/functional/test_gzip_middleware.py b/glance/tests/functional/test_gzip_middleware.py new file mode 100644 index 0000000000..59fff30edb --- /dev/null +++ b/glance/tests/functional/test_gzip_middleware.py @@ -0,0 +1,50 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Red Hat, Inc +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Tests gzip middleware.""" + +import httplib2 + +from glance.tests import functional +from glance.tests import utils + + +class GzipMiddlewareTest(functional.FunctionalTest): + + @utils.skip_if_disabled + def test_gzip_requests(self): + self.cleanup() + self.start_servers(**self.__dict__.copy()) + + def request(path, headers=None): + # We don't care what version we're using here so, + # sticking with latest + url = 'http://127.0.0.1:%s/v2/%s' % (self.api_port, path) + http = httplib2.Http() + return http.request(url, 'GET', headers=headers) + + # Accept-Encoding: Identity + headers = {'Accept-Encoding': 'identity'} + response, content = request('images', headers=headers) + self.assertEqual(response.get("-content-encoding"), None) + + # Accept-Encoding: gzip + headers = {'Accept-Encoding': 'gzip'} + response, content = request('images', headers=headers) + self.assertEqual(response.get("-content-encoding"), 'gzip') + + self.stop_servers()