Merge "py3: Fix s3api header casing"
This commit is contained in:
commit
d215fef8fa
|
@ -16,13 +16,6 @@
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
|
||||||
def _title(s):
|
|
||||||
if six.PY2:
|
|
||||||
return s.title()
|
|
||||||
else:
|
|
||||||
return s.encode('latin1').title().decode('latin1')
|
|
||||||
|
|
||||||
|
|
||||||
class HeaderKeyDict(dict):
|
class HeaderKeyDict(dict):
|
||||||
"""
|
"""
|
||||||
A dict that title-cases all keys on the way in, so as to be
|
A dict that title-cases all keys on the way in, so as to be
|
||||||
|
@ -36,35 +29,43 @@ class HeaderKeyDict(dict):
|
||||||
self.update(base_headers)
|
self.update(base_headers)
|
||||||
self.update(kwargs)
|
self.update(kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _title(s):
|
||||||
|
if six.PY2:
|
||||||
|
return s.title()
|
||||||
|
else:
|
||||||
|
return s.encode('latin1').title().decode('latin1')
|
||||||
|
|
||||||
def update(self, other):
|
def update(self, other):
|
||||||
if hasattr(other, 'keys'):
|
if hasattr(other, 'keys'):
|
||||||
for key in other.keys():
|
for key in other.keys():
|
||||||
self[_title(key)] = other[key]
|
self[self._title(key)] = other[key]
|
||||||
else:
|
else:
|
||||||
for key, value in other:
|
for key, value in other:
|
||||||
self[_title(key)] = value
|
self[self._title(key)] = value
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
return dict.get(self, _title(key))
|
return dict.get(self, self._title(key))
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
|
key = self._title(key)
|
||||||
if value is None:
|
if value is None:
|
||||||
self.pop(_title(key), None)
|
self.pop(key, None)
|
||||||
elif six.PY2 and isinstance(value, six.text_type):
|
elif six.PY2 and isinstance(value, six.text_type):
|
||||||
return dict.__setitem__(self, _title(key), value.encode('utf-8'))
|
return dict.__setitem__(self, key, value.encode('utf-8'))
|
||||||
elif six.PY3 and isinstance(value, six.binary_type):
|
elif six.PY3 and isinstance(value, six.binary_type):
|
||||||
return dict.__setitem__(self, _title(key), value.decode('latin-1'))
|
return dict.__setitem__(self, key, value.decode('latin-1'))
|
||||||
else:
|
else:
|
||||||
return dict.__setitem__(self, _title(key), str(value))
|
return dict.__setitem__(self, key, str(value))
|
||||||
|
|
||||||
def __contains__(self, key):
|
def __contains__(self, key):
|
||||||
return dict.__contains__(self, _title(key))
|
return dict.__contains__(self, self._title(key))
|
||||||
|
|
||||||
def __delitem__(self, key):
|
def __delitem__(self, key):
|
||||||
return dict.__delitem__(self, _title(key))
|
return dict.__delitem__(self, self._title(key))
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key, default=None):
|
||||||
return dict.get(self, _title(key), default)
|
return dict.get(self, self._title(key), default)
|
||||||
|
|
||||||
def setdefault(self, key, value=None):
|
def setdefault(self, key, value=None):
|
||||||
if key not in self:
|
if key not in self:
|
||||||
|
@ -72,4 +73,4 @@ class HeaderKeyDict(dict):
|
||||||
return self[key]
|
return self[key]
|
||||||
|
|
||||||
def pop(self, key, default=None):
|
def pop(self, key, default=None):
|
||||||
return dict.pop(self, _title(key), default)
|
return dict.pop(self, self._title(key), default)
|
||||||
|
|
|
@ -17,6 +17,7 @@ import re
|
||||||
from collections import MutableMapping
|
from collections import MutableMapping
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
|
from swift.common import header_key_dict
|
||||||
from swift.common import swob
|
from swift.common import swob
|
||||||
from swift.common.utils import config_true_value
|
from swift.common.utils import config_true_value
|
||||||
from swift.common.request_helpers import is_sys_meta
|
from swift.common.request_helpers import is_sys_meta
|
||||||
|
@ -26,42 +27,21 @@ from swift.common.middleware.s3api.utils import snake_to_camel, \
|
||||||
from swift.common.middleware.s3api.etree import Element, SubElement, tostring
|
from swift.common.middleware.s3api.etree import Element, SubElement, tostring
|
||||||
|
|
||||||
|
|
||||||
class HeaderKey(str):
|
class HeaderKeyDict(header_key_dict.HeaderKeyDict):
|
||||||
"""
|
"""
|
||||||
A string object that normalizes string as S3 clients expect with title().
|
Similar to the Swift's normal HeaderKeyDict class, but its key name is
|
||||||
|
normalized as S3 clients expect.
|
||||||
"""
|
"""
|
||||||
def title(self):
|
@staticmethod
|
||||||
if self.lower() == 'etag':
|
def _title(s):
|
||||||
|
s = header_key_dict.HeaderKeyDict._title(s)
|
||||||
|
if s.lower() == 'etag':
|
||||||
# AWS Java SDK expects only 'ETag'.
|
# AWS Java SDK expects only 'ETag'.
|
||||||
return 'ETag'
|
return 'ETag'
|
||||||
if self.lower().startswith('x-amz-'):
|
if s.lower().startswith('x-amz-'):
|
||||||
# AWS headers returned by S3 are lowercase.
|
# AWS headers returned by S3 are lowercase.
|
||||||
return self.lower()
|
return swob.bytes_to_wsgi(swob.wsgi_to_bytes(s).lower())
|
||||||
return str.title(self)
|
return s
|
||||||
|
|
||||||
|
|
||||||
class HeaderKeyDict(swob.HeaderKeyDict):
|
|
||||||
"""
|
|
||||||
Similar to the HeaderKeyDict class in Swift, but its key name is normalized
|
|
||||||
as S3 clients expect.
|
|
||||||
"""
|
|
||||||
def __getitem__(self, key):
|
|
||||||
return swob.HeaderKeyDict.__getitem__(self, HeaderKey(key))
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
return swob.HeaderKeyDict.__setitem__(self, HeaderKey(key), value)
|
|
||||||
|
|
||||||
def __contains__(self, key):
|
|
||||||
return swob.HeaderKeyDict.__contains__(self, HeaderKey(key))
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
return swob.HeaderKeyDict.__delitem__(self, HeaderKey(key))
|
|
||||||
|
|
||||||
def get(self, key, default=None):
|
|
||||||
return swob.HeaderKeyDict.get(self, HeaderKey(key), default)
|
|
||||||
|
|
||||||
def pop(self, key, default=None):
|
|
||||||
return swob.HeaderKeyDict.pop(self, HeaderKey(key), default)
|
|
||||||
|
|
||||||
|
|
||||||
class S3ResponseBase(object):
|
class S3ResponseBase(object):
|
||||||
|
@ -116,7 +96,7 @@ class S3Response(S3ResponseBase, swob.Response):
|
||||||
|
|
||||||
# Handle swift headers
|
# Handle swift headers
|
||||||
for key, val in sw_headers.items():
|
for key, val in sw_headers.items():
|
||||||
_key = key.lower()
|
_key = swob.bytes_to_wsgi(swob.wsgi_to_bytes(key).lower())
|
||||||
|
|
||||||
if _key.startswith('x-object-meta-'):
|
if _key.startswith('x-object-meta-'):
|
||||||
# Note that AWS ignores user-defined headers with '=' in the
|
# Note that AWS ignores user-defined headers with '=' in the
|
||||||
|
|
|
@ -35,6 +35,24 @@ class TestResponse(unittest.TestCase):
|
||||||
else:
|
else:
|
||||||
self.assertEqual('"theetag"', s3resp.headers['ETag'])
|
self.assertEqual('"theetag"', s3resp.headers['ETag'])
|
||||||
|
|
||||||
|
def test_response_s3api_user_meta_headers(self):
|
||||||
|
resp = Response(headers={
|
||||||
|
'X-Object-Meta-Foo': 'Bar',
|
||||||
|
'X-Object-Meta-Non-\xdcnicode-Value': '\xff',
|
||||||
|
'X-Object-Sysmeta-Baz': 'quux',
|
||||||
|
'Etag': 'unquoted',
|
||||||
|
'Content-type': 'text/plain',
|
||||||
|
'content-length': '0',
|
||||||
|
})
|
||||||
|
s3resp = S3Response.from_swift_resp(resp)
|
||||||
|
self.assertEqual(dict(s3resp.headers), {
|
||||||
|
'x-amz-meta-foo': 'Bar',
|
||||||
|
'x-amz-meta-non-\xdcnicode-value': '\xff',
|
||||||
|
'ETag': '"unquoted"',
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
'Content-Length': '0',
|
||||||
|
})
|
||||||
|
|
||||||
def test_response_s3api_sysmeta_headers(self):
|
def test_response_s3api_sysmeta_headers(self):
|
||||||
for _server_type in ('object', 'container'):
|
for _server_type in ('object', 'container'):
|
||||||
swift_headers = HeaderKeyDict(
|
swift_headers = HeaderKeyDict(
|
||||||
|
|
Loading…
Reference in New Issue