Compress response's content according to client's accepted encoding

Currently Glance ignores the Accept-Encoding header and returns
responses as they are regardless the client accepts gzip or other type
of compression.

This patch adds this capability to glance (by using a middleware)
supporting just gzip for now.

Important note:
    - The patch uses a lazy compression for Content-Type
    application/octet-stream but in order to do that, the
    content-length has to be unset which means that when an image is
    downloaded the content-length will be unknown to the client.

Fixes bug: 1150380

Change-Id: Ieb65837d4e3fe310f97d9666882ecc572b14956a
This commit is contained in:
Flaper Fesp 2013-04-27 14:42:15 +02:00
parent a9f9f137bb
commit 0a4f4aff6c
5 changed files with 127 additions and 4 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -354,24 +354,25 @@ enable_v2_api= %(enable_v2_api)s
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 @@ paste.app_factory = glance.api.v2.router:API.factory
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

View File

@ -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()