Skip to content

Commit

Permalink
Compress response's content according to client's accepted encoding
Browse files Browse the repository at this point in the history
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
  • Loading branch information
flaper87 committed Apr 30, 2013
1 parent a9f9f13 commit 0a4f4af
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 4 deletions.
3 changes: 3 additions & 0 deletions etc/glance-api-paste.ini
Expand Up @@ -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
65 changes: 65 additions & 0 deletions 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
1 change: 1 addition & 0 deletions glance/common/wsgi.py
Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions glance/tests/functional/__init__.py
Expand Up @@ -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
Expand All @@ -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
Expand Down
50 changes: 50 additions & 0 deletions 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()

0 comments on commit 0a4f4af

Please sign in to comment.