diff --git a/middlewares/__init__.py b/middlewares/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/middlewares/last_modified.py b/middlewares/last_modified.py index a038052..4f75856 100644 --- a/middlewares/last_modified.py +++ b/middlewares/last_modified.py @@ -1,61 +1,79 @@ -# Copyright (c) 2013 OpenStack, LLC. +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2013 eNovance SAS # -# 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 +# Author: Fabien Boucher # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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. +# 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. import time + from swift.common.utils import get_logger -from swift.common.swob import Request, wsgify +from swift.common.wsgi import make_pre_authed_request +from swift.common.swob import wsgify -class LastModified(object): +class LastModifiedMiddleware(object): """ + LastModified is a middleware that add a meta to a container + when that container and/or objects in it are modified. The meta + data will contains the epoch timestamp. This middleware aims + to be used with the synchronizer. It limits the tree parsing + by giving a way to know a container has been modified since the + last container synchronization. + + Actions that lead to the container meta modification : + - POST/PUT on container + - POST/PUT/DELETE on object in it + + The following shows an example of proxy-server.conf: + [pipeline:main] + pipeline = catch_errors cache tempauth last-modified proxy-server + + [filter:last-modified] + use = egg:swift#last_modified + # will show as X-Container-Meta-${key_name} for the container's header. + key_name = Last-Modified """ def __init__(self, app, conf): self.app = app self.conf = conf self.logger = get_logger(self.conf, log_route='last_modified') + self.key_name = conf.get('key_name', + 'Last-Modified').strip().replace(' ', '-') def update_last_modified_meta(self, req, env): vrs, account, container, obj = req.split_path(1, 4, True) + path = env['PATH_INFO'] if obj: - env['PATH_INFO'] = env['PATH_INFO'].split('/%s' % obj)[0] - env['REQUEST_METHOD'] = 'POST' - headers = {'X-Container-Meta-Last-Modified': str(time.time())} - set_meta_req = Request.blank(env['PATH_INFO'], - headers=headers, - environ=env) - return set_meta_req.get_response(self.app) - - def req_passthrough(self, req): - return req.get_response(self.app) + path = path.split('/%s' % obj)[0] + metakey = 'X-Container-Meta-%s' % self.key_name + headers = {metakey: str(time.time())} + set_meta_req = make_pre_authed_request(env, + method='POST', + path=path, + headers=headers, + swift_source='lm') + set_meta_req.get_response(self.app) @wsgify def __call__(self, req): vrs, account, container, obj = req.split_path(1, 4, True) - if req.method in ('POST', 'PUT') and container or \ - req.method == 'DELETE' and obj: + if (req.method in ('POST', 'PUT') and + container or req.method == 'DELETE' and obj): new_env = req.environ.copy() - user_resp = self.req_passthrough(req) - if user_resp.status_int // 100 == 2: - # Update Container Meta Last-Modified in case of - # successful request - update_resp = self.update_last_modified_meta(req, - new_env) - if update_resp.status_int // 100 != 2: - self.logger.info('Unable to update Meta Last-Modified') - return user_resp + self.update_last_modified_meta(req, new_env) return self.app @@ -63,6 +81,4 @@ def filter_factory(global_conf, **local_conf): conf = global_conf.copy() conf.update(local_conf) - def last_modified_filter(app): - return LastModified(app, conf) - return last_modified_filter + return lambda app: LastModifiedMiddleware(app, conf) diff --git a/tests/test_middleware_lm.py b/tests/test_middleware_lm.py new file mode 100644 index 0000000..769f402 --- /dev/null +++ b/tests/test_middleware_lm.py @@ -0,0 +1,156 @@ +# -*- encoding: utf-8 -*- + +# Copyright 2013 eNovance. +# 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 +# +# Author : "Fabien Boucher " +# +# 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. + +import unittest + +from middlewares import last_modified as middleware +from swift.common.swob import Response, Request + + +class FakeApp(object): + def __init__(self, status_headers_body=None): + self.status_headers_body = status_headers_body + if not self.status_headers_body: + self.status_headers_body = ('204 No Content', {}, '') + + def __call__(self, env, start_response): + status, headers, body = self.status_headers_body + return Response(status=status, headers=headers, + body=body)(env, start_response) + + +class FakeRequest(object): + def get_response(self, app): + pass + + +class TestLastModifiedMiddleware(unittest.TestCase): + + def _make_request(self, path, **kwargs): + req = Request.blank("/v1/AUTH_account/%s" % path, **kwargs) + return req + + def setUp(self): + self.conf = {'key_name': 'Last-Modified'} + self.test_default = middleware.filter_factory(self.conf)(FakeApp()) + + def test_denied_method_conf(self): + app = FakeApp() + test = middleware.filter_factory({})(app) + self.assertEquals(test.key_name, 'Last-Modified') + test = middleware.filter_factory({'key_name': "Last Modified"})(app) + self.assertEquals(test.key_name, 'Last-Modified') + test = middleware.filter_factory({'key_name': "Custom Key"})(app) + self.assertEquals(test.key_name, 'Custom-Key') + + def test_PUT_on_container(self): + self.called = False + + def make_pre_authed_request(*args, **kargs): + self.called = True + return FakeRequest() + + middleware.make_pre_authed_request = make_pre_authed_request + req = self._make_request('cont', + environ={'REQUEST_METHOD': 'PUT'}) + req.get_response(self.test_default) + self.assertEqual(self.called, True) + + def test_POST_on_container(self): + self.called = False + + def make_pre_authed_request(*args, **kargs): + self.called = True + return FakeRequest() + + middleware.make_pre_authed_request = make_pre_authed_request + req = self._make_request('cont', + environ={'REQUEST_METHOD': 'POST'}) + req.get_response(self.test_default) + self.assertEqual(self.called, True) + + def test_DELETE_on_container(self): + self.called = False + + def make_pre_authed_request(*args, **kargs): + self.called = True + return FakeRequest() + + middleware.make_pre_authed_request = make_pre_authed_request + req = self._make_request('cont', + environ={'REQUEST_METHOD': 'DELETE'}) + req.get_response(self.test_default) + self.assertEqual(self.called, False) + + def test_GET_on_container_and_object(self): + self.called = False + + def make_pre_authed_request(*args, **kargs): + self.called = True + return FakeRequest() + + middleware.make_pre_authed_request = make_pre_authed_request + req = self._make_request('cont', + environ={'REQUEST_METHOD': 'GET'}) + req.get_response(self.test_default) + self.assertEqual(self.called, False) + self.called = False + req = self._make_request('cont/obj', + environ={'REQUEST_METHOD': 'GET'}) + req.get_response(self.test_default) + self.assertEqual(self.called, False) + + def test_POST_on_object(self): + self.called = False + + def make_pre_authed_request(*args, **kargs): + self.called = True + return FakeRequest() + + middleware.make_pre_authed_request = make_pre_authed_request + req = self._make_request('cont/obj', + environ={'REQUEST_METHOD': 'POST'}) + req.get_response(self.test_default) + self.assertEqual(self.called, True) + + def test_PUT_on_object(self): + self.called = False + + def make_pre_authed_request(*args, **kargs): + self.called = True + return FakeRequest() + + middleware.make_pre_authed_request = make_pre_authed_request + req = self._make_request('cont/obj', + environ={'REQUEST_METHOD': 'PUT'}) + req.get_response(self.test_default) + self.assertEqual(self.called, True) + + def test_DELETE_on_object(self): + self.called = False + + def make_pre_authed_request(*args, **kargs): + self.called = True + return FakeRequest() + + middleware.make_pre_authed_request = make_pre_authed_request + req = self._make_request('cont/obj', + environ={'REQUEST_METHOD': 'DELETE'}) + req.get_response(self.test_default) + self.assertEqual(self.called, True) diff --git a/tools/pip-requires b/tools/pip-requires index 3814adb..fe2547f 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -1,5 +1,5 @@ simplejson -swift +http://tarballs.openstack.org/swift/swift-master.tar.gz#egg=swift python-swiftclient webob python-dateutil