Use lxml for processing XML

The lxml XML toolkit is a binding for the C libraries libxml2 and libxslt.  It
is compatible with ElementTree API, which is more pythonic than the DOM
interface.  In addition, lxml has attractive features like XML validation and
namespace mapping.

Replacing the current DOM interface with the lxml API will simplify Swift3 code
a lot and improve maintainability, I believe.

Change-Id: Ie2291f180421559ed3320a173d2f7eea81f459d9
This commit is contained in:
MORITA Kazutaka 2014-05-26 11:20:07 +09:00
parent 2e78cd0355
commit 1f87522631
3 changed files with 178 additions and 271 deletions

View File

@ -1 +1,2 @@
swift>=1.8 swift>=1.8
lxml

View File

@ -54,8 +54,7 @@ following for an SAIO setup::
from urllib import quote from urllib import quote
import base64 import base64
from xml.sax.saxutils import escape as xml_escape from lxml.etree import fromstring, tostring, Element, SubElement
from xml.dom.minidom import parseString
from simplejson import loads from simplejson import loads
import email.utils import email.utils
@ -71,7 +70,9 @@ from swift.common.http import HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED, \
HTTP_NO_CONTENT, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, \ HTTP_NO_CONTENT, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, \
HTTP_CONFLICT, HTTP_UNPROCESSABLE_ENTITY, is_success, \ HTTP_CONFLICT, HTTP_UNPROCESSABLE_ENTITY, is_success, \
HTTP_REQUEST_ENTITY_TOO_LARGE HTTP_REQUEST_ENTITY_TOO_LARGE
from swift.common.middleware.acl import parse_acl, referrer_allowed
XMLNS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
MAX_BUCKET_LISTING = 1000 MAX_BUCKET_LISTING = 1000
@ -136,128 +137,64 @@ def get_err_response(code):
(HTTPServiceUnavailable, 'Please reduce your request rate')} (HTTPServiceUnavailable, 'Please reduce your request rate')}
resp, message = error_table[code] resp, message = error_table[code]
body = '<?xml version="1.0" encoding="UTF-8"?>\r\n<Error>\r\n ' \
'<Code>%s</Code>\r\n <Message>%s</Message>\r\n</Error>\r\n' \ elem = Element('Error')
% (code, message) SubElement(elem, 'Code').text = code
SubElement(elem, 'Message').text = message
body = tostring(elem, xml_declaration=True, encoding='UTF-8')
return resp(body=body, content_type='text/xml') return resp(body=body, content_type='text/xml')
def add_canonical_user(parent, tag, user, nsmap=None):
"""
Create an element for cannonical user.
"""
elem = SubElement(parent, tag, nsmap=nsmap)
SubElement(elem, 'ID').text = user
SubElement(elem, 'DisplayName').text = user
return elem
def get_acl(account_name, headers): def get_acl(account_name, headers):
""" """
Attempts to construct an S3 ACL based on what is found in the swift headers Attempts to construct an S3 ACL based on what is found in the swift headers
""" """
acl = 'private' # default to private elem = Element('AccessControlPolicy')
add_canonical_user(elem, 'Owner', account_name)
access_control_list = SubElement(elem, 'AccessControlList')
if 'x-container-read' in headers: # grant FULL_CONTROL to myself by default
if headers['x-container-read'] == ".r:*" or\ grant = SubElement(access_control_list, 'Grant')
".r:*," in headers['x-container-read'] or \ grantee = add_canonical_user(grant, 'Grantee', account_name,
",*," in headers['x-container-read']: nsmap={'xsi': XMLNS_XSI})
acl = 'public-read' grantee.set('{%s}type' % XMLNS_XSI, 'CanonicalUser')
if 'x-container-write' in headers: SubElement(grant, 'Permission').text = 'FULL_CONTROL'
if headers['x-container-write'] == ".r:*" or\
".r:*," in headers['x-container-write'] or \ referrers, _ = parse_acl(headers.get('x-container-read'))
",*," in headers['x-container-write']: if referrer_allowed('unknown', referrers):
if acl == 'public-read': # grant public-read access
acl = 'public-read-write' grant = SubElement(access_control_list, 'Grant')
else: grantee = SubElement(grant, 'Grantee', nsmap={'xsi': XMLNS_XSI})
acl = 'public-write' grantee.set('{%s}type' % XMLNS_XSI, 'Group')
SubElement(grantee, 'URI').text = \
'http://acs.amazonaws.com/groups/global/AllUsers'
SubElement(grant, 'Permission').text = 'READ'
referrers, _ = parse_acl(headers.get('x-container-write'))
if referrer_allowed('unknown', referrers):
# grant public-write access
grant = SubElement(access_control_list, 'Grant')
grantee = SubElement(grant, 'Grantee', nsmap={'xsi': XMLNS_XSI})
grantee.set('{%s}type' % XMLNS_XSI, 'Group')
SubElement(grantee, 'URI').text = \
'http://acs.amazonaws.com/groups/global/AllUsers'
SubElement(grant, 'Permission').text = 'WRITE'
body = tostring(elem, xml_declaration=True, encoding='UTF-8')
if acl == 'private':
body = ('<AccessControlPolicy>'
'<Owner>'
'<ID>%s</ID>'
'<DisplayName>%s</DisplayName>'
'</Owner>'
'<AccessControlList>'
'<Grant>'
'<Grantee xmlns:xsi="http://www.w3.org/2001/'
'XMLSchema-instance" xsi:type="CanonicalUser">'
'<ID>%s</ID>'
'<DisplayName>%s</DisplayName>'
'</Grantee>'
'<Permission>FULL_CONTROL</Permission>'
'</Grant>'
'</AccessControlList>'
'</AccessControlPolicy>' %
(account_name, account_name, account_name, account_name))
elif acl == 'public-read':
body = ('<AccessControlPolicy>'
'<Owner>'
'<ID>%s</ID>'
'<DisplayName>%s</DisplayName>'
'</Owner>'
'<AccessControlList>'
'<Grant>'
'<Grantee xmlns:xsi="http://www.w3.org/2001/'
'XMLSchema-instance" xsi:type="CanonicalUser">'
'<ID>%s</ID>'
'<DisplayName>%s</DisplayName>'
'</Grantee>'
'<Permission>FULL_CONTROL</Permission>'
'</Grant>'
'<Grant>'
'<Grantee xmlns:xsi="http://www.w3.org/2001/'
'XMLSchema-instance" xsi:type="Group">'
'<URI>http://acs.amazonaws.com/groups/global/AllUsers</URI>'
'</Grantee>'
'<Permission>READ</Permission>'
'</Grant>'
'</AccessControlList>'
'</AccessControlPolicy>' %
(account_name, account_name, account_name, account_name))
elif acl == 'public-read-write':
body = ('<AccessControlPolicy>'
'<Owner>'
'<ID>%s</ID>'
'<DisplayName>%s</DisplayName>'
'</Owner>'
'<AccessControlList>'
'<Grant>'
'<Grantee xmlns:xsi="http://www.w3.org/2001/'
'XMLSchema-instance" xsi:type="CanonicalUser">'
'<ID>%s</ID>'
'<DisplayName>%s</DisplayName>'
'</Grantee>'
'<Permission>FULL_CONTROL</Permission>'
'</Grant>'
'<Grant>'
'<Grantee xmlns:xsi="http://www.w3.org/2001/'
'XMLSchema-instance" xsi:type="Group">'
'<URI>http://acs.amazonaws.com/groups/global/AllUsers</URI>'
'</Grantee>'
'<Permission>READ</Permission>'
'</Grant>'
'</AccessControlList>'
'<AccessControlList>'
'<Grant>'
'<Grantee xmlns:xsi="http://www.w3.org/2001/'
'XMLSchema-instance" xsi:type="Group">'
'<URI>http://acs.amazonaws.com/groups/global/AllUsers</URI>'
'</Grantee>'
'<Permission>WRITE</Permission>'
'</Grant>'
'</AccessControlList>'
'</AccessControlPolicy>' %
(account_name, account_name, account_name, account_name))
else:
body = ('<AccessControlPolicy>'
'<Owner>'
'<ID>%s</ID>'
'<DisplayName>%s</DisplayName>'
'</Owner>'
'<AccessControlList>'
'<Grant>'
'<Grantee xmlns:xsi="http://www.w3.org/2001/'
'XMLSchema-instance" xsi:type="CanonicalUser">'
'<ID>%s</ID>'
'<DisplayName>%s</DisplayName>'
'</Grantee>'
'<Permission>FULL_CONTROL</Permission>'
'</Grant>'
'</AccessControlList>'
'</AccessControlPolicy>' %
(account_name, account_name, account_name, account_name))
return HTTPOk(body=body, content_type="text/plain") return HTTPOk(body=body, content_type="text/plain")
@ -322,13 +259,11 @@ def swift_acl_translate(acl, group='', user='', xml=False):
['HTTP_X_CONTAINER_READ', '.']] ['HTTP_X_CONTAINER_READ', '.']]
if xml: if xml:
# We are working with XML and need to parse it # We are working with XML and need to parse it
dom = parseString(acl) elem = fromstring(acl)
acl = 'unknown' acl = 'unknown'
for grant in dom.getElementsByTagName('Grant'): for grant in elem.findall('./AccessControlList/Grant'):
permission = grant.getElementsByTagName('Permission')[0]\ permission = grant.find('./Permission').text
.firstChild.data grantee = grant.find('./Grantee').get('{%s}type' % XMLNS_XSI)
grantee = grant.getElementsByTagName('Grantee')[0]\
.getAttributeNode('xsi:type').nodeValue
if permission == "FULL_CONTROL" and grantee == 'CanonicalUser' and\ if permission == "FULL_CONTROL" and grantee == 'CanonicalUser' and\
acl != 'public-read' and acl != 'public-read-write': acl != 'public-read' and acl != 'public-read-write':
acl = 'private' acl = 'private'
@ -415,14 +350,16 @@ class ServiceController(Controller):
containers = loads(resp.body) containers = loads(resp.body)
# we don't keep the creation time of a backet (s3cmd doesn't # we don't keep the creation time of a backet (s3cmd doesn't
# work without that) so we use something bogus. # work without that) so we use something bogus.
body = '<?xml version="1.0" encoding="UTF-8"?>' \ elem = Element('ListAllMyBucketsResult')
'<ListAllMyBucketsResult ' \ buckets = SubElement(elem, 'Buckets')
'xmlns="http://doc.s3.amazonaws.com/2006-03-01">' \ for c in containers:
'<Buckets>%s</Buckets>' \ bucket = SubElement(buckets, 'Bucket')
'</ListAllMyBucketsResult>' \ SubElement(bucket, 'Name').text = c['name']
% ("".join(['<Bucket><Name>%s</Name><CreationDate>' SubElement(bucket, 'CreationDate').text = \
'2009-02-03T16:45:09.000Z</CreationDate></Bucket>' '2009-02-03T16:45:09.000Z'
% xml_escape(i['name']) for i in containers]))
body = tostring(elem, xml_declaration=True, encoding='UTF-8')
return HTTPOk(content_type='application/xml', body=body) return HTTPOk(content_type='application/xml', body=body)
@ -481,37 +418,36 @@ class BucketController(Controller):
return get_err_response('InternalError') return get_err_response('InternalError')
objects = loads(resp.body) objects = loads(resp.body)
body = ('<?xml version="1.0" encoding="UTF-8"?>'
'<ListBucketResult ' elem = Element('ListBucketResult')
'xmlns="http://s3.amazonaws.com/doc/2006-03-01">' SubElement(elem, 'Prefix').text = req.params.get('prefix')
'<Prefix>%s</Prefix>' SubElement(elem, 'Marker').text = req.params.get('marker')
'<Marker>%s</Marker>' SubElement(elem, 'Delimiter').text = req.params.get('delimiter')
'<Delimiter>%s</Delimiter>' if max_keys > 0 and len(objects) == max_keys + 1:
'<IsTruncated>%s</IsTruncated>' is_truncated = 'true'
'<MaxKeys>%s</MaxKeys>' else:
'<Name>%s</Name>' is_truncated = 'false'
'%s' SubElement(elem, 'IsTruncated').text = is_truncated
'%s' SubElement(elem, 'MaxKeys').text = str(max_keys)
'</ListBucketResult>' % SubElement(elem, 'Name').text = self.container_name
(
xml_escape(req.params.get('prefix', '')), for o in objects[:max_keys]:
xml_escape(req.params.get('marker', '')), if 'subdir' not in o:
xml_escape(req.params.get('delimiter', '')), contents = SubElement(elem, 'Contents')
'true' if max_keys > 0 and len(objects) == (max_keys + 1) else SubElement(contents, 'Key').text = o['name']
'false', SubElement(contents, 'LastModified').text = \
max_keys, o['last_modified'] + 'Z'
xml_escape(self.container_name), SubElement(contents, 'ETag').text = o['hash']
"".join(['<Contents><Key>%s</Key><LastModified>%sZ</LastModif' SubElement(contents, 'Size').text = str(o['bytes'])
'ied><ETag>%s</ETag><Size>%s</Size><StorageClass>STA' add_canonical_user(contents, 'Owner', self.account_name)
'NDARD</StorageClass><Owner><ID>%s</ID><DisplayName>'
'%s</DisplayName></Owner></Contents>' % for o in objects[:max_keys]:
(xml_escape(i['name']), i['last_modified'], if 'subdir' in o:
i['hash'], common_prefixes = SubElement(elem, 'CommonPrefixes')
i['bytes'], self.account_name, self.account_name) SubElement(common_prefixes, 'Prefix').text = o['subdir']
for i in objects[:max_keys] if 'subdir' not in i]),
"".join(['<CommonPrefixes><Prefix>%s</Prefix></CommonPrefixes>' body = tostring(elem, xml_declaration=True, encoding='UTF-8')
% xml_escape(i['subdir'])
for i in objects[:max_keys] if 'subdir' in i])))
return HTTPOk(body=body, content_type='application/xml') return HTTPOk(body=body, content_type='application/xml')
def PUT(self, req): def PUT(self, req):
@ -662,9 +598,9 @@ class ObjectController(Controller):
return get_err_response('InternalError') return get_err_response('InternalError')
if 'HTTP_X_COPY_FROM' in req.environ: if 'HTTP_X_COPY_FROM' in req.environ:
body = '<CopyObjectResult>' \ elem = Element('CopyObjectResult')
'<ETag>"%s"</ETag>' \ SubElement(elem, 'ETag').text = '"%s"' % resp.etag
'</CopyObjectResult>' % resp.etag body = tostring(elem, xml_declaration=True, encoding='UTF-8')
return HTTPOk(body=body) return HTTPOk(body=body)
return HTTPOk(etag=resp.etag) return HTTPOk(etag=resp.etag)
@ -802,13 +738,11 @@ class LocationController(Controller):
else: else:
return get_err_response('InternalError') return get_err_response('InternalError')
body = ('<?xml version="1.0" encoding="UTF-8"?>' elem = Element('LocationConstraint')
'<LocationConstraint ' if self.location != 'US':
'xmlns="http://s3.amazonaws.com/doc/2006-03-01/"') elem.text = self.location
if self.location == 'US': body = tostring(elem, xml_declaration=True, encoding='UTF-8')
body += '/>'
else:
body += ('>%s</LocationConstraint>' % self.location)
return HTTPOk(body=body, content_type='application/xml') return HTTPOk(body=body, content_type='application/xml')
@ -837,9 +771,9 @@ class LoggingStatusController(Controller):
return get_err_response('InternalError') return get_err_response('InternalError')
# logging disabled # logging disabled
body = ('<?xml version="1.0" encoding="UTF-8"?>' elem = Element('BucketLoggingStatus')
'<BucketLoggingStatus ' body = tostring(elem, xml_declaration=True, encoding='UTF-8')
'xmlns="http://doc.s3.amazonaws.com/2006-03-01" />')
return HTTPOk(body=body, content_type='application/xml') return HTTPOk(body=body, content_type='application/xml')
def PUT(self, req): def PUT(self, req):
@ -859,31 +793,17 @@ class MultiObjectDeleteController(Controller):
Handles Delete Multiple Objects. Handles Delete Multiple Objects.
""" """
def object_key_iter(xml): def object_key_iter(xml):
dom = parseString(xml) elem = fromstring(xml)
delete = dom.getElementsByTagName('Delete')[0] for obj in elem.iterchildren('Object'):
for obj in delete.getElementsByTagName('Object'): key = obj.find('./Key').text
key = obj.getElementsByTagName('Key')[0].firstChild.data version = obj.find('./VersionId')
version = None if version is not None:
if obj.getElementsByTagName('VersionId').length > 0: version = version.text
version = obj.getElementsByTagName('VersionId')[0]\
.firstChild.data
yield (key, version) yield (key, version)
def get_deleted_elem(key): elem = Element('DeleteResult')
return ' <Deleted>\r\n' \
' <Key>%s</Key>\r\n' \
' </Deleted>\r\n' % (key)
def get_err_elem(key, err_code, message):
return ' <Error>\r\n' \
' <Key>%s</Key>\r\n' \
' <Code>%s</Code>\r\n' \
' <Message>%s</Message>\r\n' \
' </Error>\r\n' % (key, err_code, message)
body = '<?xml version="1.0" encoding="UTF-8"?>\r\n' \
'<DeleteResult ' \
'xmlns="http://doc.s3.amazonaws.com/2006-03-01">\r\n'
for key, version in object_key_iter(req.body): for key, version in object_key_iter(req.body):
if version is not None: if version is not None:
# TODO: delete the specific version of the object # TODO: delete the specific version of the object
@ -900,15 +820,20 @@ class MultiObjectDeleteController(Controller):
status = sub_resp.status_int status = sub_resp.status_int
if status == HTTP_NO_CONTENT or status == HTTP_NOT_FOUND: if status == HTTP_NO_CONTENT or status == HTTP_NOT_FOUND:
body += get_deleted_elem(key) deleted = SubElement(elem, 'Deleted')
SubElement(deleted, 'Key').text = key
else: else:
error = SubElement(elem, 'Error')
SubElement(error, 'Key').text = key
if status == HTTP_UNAUTHORIZED: if status == HTTP_UNAUTHORIZED:
body += get_err_elem(key, 'AccessDenied', 'Access Denied') SubElement(error, 'Code').text = 'AccessDenied'
SubElement(error, 'Message').text = 'Access Denied'
else: else:
body += get_err_elem(key, 'InternalError', SubElement(error, 'Code').text = 'InternalError'
'Internal Error') SubElement(error, 'Message').text = 'Internal Error'
body = tostring(elem, xml_declaration=True, encoding='UTF-8')
body += '</DeleteResult>\r\n'
return HTTPOk(body=body) return HTTPOk(body=body)
@ -1010,8 +935,9 @@ class VersioningController(Controller):
return get_err_response('InternalError') return get_err_response('InternalError')
# Just report there is no versioning configured here. # Just report there is no versioning configured here.
body = ('<VersioningConfiguration ' elem = Element('VersioningConfiguration')
'xmlns="http://s3.amazonaws.com/doc/2006-03-01/"/>') body = tostring(elem, xml_declaration=True, encoding='UTF-8')
return HTTPOk(body=body, content_type="text/plain") return HTTPOk(body=body, content_type="text/plain")
def PUT(self, req): def PUT(self, req):

View File

@ -20,7 +20,7 @@ import hashlib
import base64 import base64
from urllib import unquote, quote from urllib import unquote, quote
import xml.dom.minidom from lxml.etree import fromstring, tostring, Element, SubElement
import simplejson import simplejson
from swift.common import swob from swift.common import swob
@ -78,15 +78,12 @@ class TestSwift3(unittest.TestCase):
('with space', '2011-01-05T02:19:14.275290', 0, 390), ('with space', '2011-01-05T02:19:14.275290', 0, 390),
('with%20space', '2011-01-05T02:19:14.275290', 0, 390)) ('with%20space', '2011-01-05T02:19:14.275290', 0, 390))
json_pattern = ['"name":%s', '"last_modified":%s', '"hash":%s', json_pattern = ['"name":"%s"', '"last_modified":"%s"', '"hash":"%s"',
'"bytes":%s'] '"bytes":%s']
json_pattern = '{' + ','.join(json_pattern) + '}' json_pattern = '{' + ','.join(json_pattern) + '}'
json_out = [] json_out = []
for b in self.objects: for b in self.objects:
name = simplejson.dumps(b[0]) json_out.append(json_pattern % b)
time = simplejson.dumps(b[1])
json_out.append(json_pattern %
(name, time, b[2], b[3]))
object_list = '[' + ','.join(json_out) + ']' object_list = '[' + ','.join(json_out) + ']'
self.swift.register('GET', '/v1/AUTH_test/junk', swob.HTTPOk, {}, self.swift.register('GET', '/v1/AUTH_test/junk', swob.HTTPOk, {},
object_list) object_list)
@ -121,6 +118,11 @@ class TestSwift3(unittest.TestCase):
self.swift.register('DELETE', '/v1/AUTH_test/bucket/object', self.swift.register('DELETE', '/v1/AUTH_test/bucket/object',
swob.HTTPNoContent, {}, None) swob.HTTPNoContent, {}, None)
def _get_error_code(self, body):
elem = fromstring(body)
self.assertEquals(elem.tag, 'Error')
return elem.find('./Code').text
def call_app(self, req, app=None, expect_exception=False): def call_app(self, req, app=None, expect_exception=False):
if app is None: if app is None:
app = self.app app = self.app
@ -163,20 +165,14 @@ class TestSwift3(unittest.TestCase):
req = Request.blank('/something', req = Request.blank('/something',
headers={'Authorization': 'hoge'}) headers={'Authorization': 'hoge'})
status, headers, body = self.call_swift3(req) status, headers, body = self.call_swift3(req)
dom = xml.dom.minidom.parseString(body) self.assertEquals(self._get_error_code(body), 'AccessDenied')
self.assertEquals(dom.firstChild.nodeName, 'Error')
code = dom.getElementsByTagName('Code')[0].childNodes[0].nodeValue
self.assertEquals(code, 'AccessDenied')
def test_bad_method(self): def test_bad_method(self):
req = Request.blank('/', req = Request.blank('/',
environ={'REQUEST_METHOD': 'PUT'}, environ={'REQUEST_METHOD': 'PUT'},
headers={'Authorization': 'AWS test:tester:hmac'}) headers={'Authorization': 'AWS test:tester:hmac'})
status, headers, body = self.call_swift3(req) status, headers, body = self.call_swift3(req)
dom = xml.dom.minidom.parseString(body) self.assertEquals(self._get_error_code(body), 'MethodNotAllowed')
self.assertEquals(dom.firstChild.nodeName, 'Error')
code = dom.getElementsByTagName('Code')[0].childNodes[0].nodeValue
self.assertEquals(code, 'MethodNotAllowed')
def test_path_info_encode(self): def test_path_info_encode(self):
bucket_name = 'b%75cket' bucket_name = 'b%75cket'
@ -199,9 +195,7 @@ class TestSwift3(unittest.TestCase):
req = Request.blank(path, environ={'REQUEST_METHOD': method}, req = Request.blank(path, environ={'REQUEST_METHOD': method},
headers=headers) headers=headers)
status, headers, body = self.call_swift3(req) status, headers, body = self.call_swift3(req)
dom = xml.dom.minidom.parseString(body) return self._get_error_code(body)
self.assertEquals(dom.firstChild.nodeName, 'Error')
return dom.getElementsByTagName('Code')[0].childNodes[0].nodeValue
def test_service_GET_error(self): def test_service_GET_error(self):
code = self._test_method_error('GET', '', swob.HTTPUnauthorized) code = self._test_method_error('GET', '', swob.HTTPUnauthorized)
@ -218,17 +212,17 @@ class TestSwift3(unittest.TestCase):
status, headers, body = self.call_swift3(req) status, headers, body = self.call_swift3(req)
self.assertEquals(status.split()[0], '200') self.assertEquals(status.split()[0], '200')
dom = xml.dom.minidom.parseString(body) elem = fromstring(body)
self.assertEquals(dom.firstChild.nodeName, 'ListAllMyBucketsResult') self.assertEquals(elem.tag, 'ListAllMyBucketsResult')
buckets = [n for n in dom.getElementsByTagName('Bucket')] all_buckets = elem.find('./Buckets')
listing = [n for n in buckets[0].childNodes if n.nodeName != '#text'] buckets = all_buckets.iterchildren('Bucket')
listing = list(list(buckets)[0])
self.assertEquals(len(listing), 2) self.assertEquals(len(listing), 2)
names = [] names = []
for b in buckets: for b in all_buckets.iterchildren('Bucket'):
if b.childNodes[0].nodeName == 'Name': names.append(b.find('./Name').text)
names.append(b.childNodes[0].childNodes[0].nodeValue)
self.assertEquals(len(names), len(self.buckets)) self.assertEquals(len(names), len(self.buckets))
for i in self.buckets: for i in self.buckets:
@ -252,20 +246,17 @@ class TestSwift3(unittest.TestCase):
status, headers, body = self.call_swift3(req) status, headers, body = self.call_swift3(req)
self.assertEquals(status.split()[0], '200') self.assertEquals(status.split()[0], '200')
dom = xml.dom.minidom.parseString(body) elem = fromstring(body)
self.assertEquals(dom.firstChild.nodeName, 'ListBucketResult') self.assertEquals(elem.tag, 'ListBucketResult')
name = dom.getElementsByTagName('Name')[0].childNodes[0].nodeValue name = elem.find('./Name').text
self.assertEquals(name, bucket_name) self.assertEquals(name, bucket_name)
objects = [n for n in dom.getElementsByTagName('Contents')] objects = elem.iterchildren('Contents')
names = [] names = []
for o in objects: for o in objects:
if o.childNodes[0].nodeName == 'Key': names.append(o.find('./Key').text)
names.append(o.childNodes[0].childNodes[0].nodeValue) self.assertTrue(o.find('./LastModified').text.endswith('Z'))
if o.childNodes[1].nodeName == 'LastModified':
self.assertTrue(
o.childNodes[1].childNodes[0].nodeValue.endswith('Z'))
self.assertEquals(len(names), len(self.objects)) self.assertEquals(len(names), len(self.objects))
for i in self.objects: for i in self.objects:
@ -279,18 +270,16 @@ class TestSwift3(unittest.TestCase):
'QUERY_STRING': 'max-keys=5'}, 'QUERY_STRING': 'max-keys=5'},
headers={'Authorization': 'AWS test:tester:hmac'}) headers={'Authorization': 'AWS test:tester:hmac'})
status, headers, body = self.call_swift3(req) status, headers, body = self.call_swift3(req)
dom = xml.dom.minidom.parseString(body) elem = fromstring(body)
self.assertEquals(dom.getElementsByTagName('IsTruncated')[0]. self.assertEquals(elem.find('./IsTruncated').text, 'false')
childNodes[0].nodeValue, 'false')
req = Request.blank('/%s' % bucket_name, req = Request.blank('/%s' % bucket_name,
environ={'REQUEST_METHOD': 'GET', environ={'REQUEST_METHOD': 'GET',
'QUERY_STRING': 'max-keys=4'}, 'QUERY_STRING': 'max-keys=4'},
headers={'Authorization': 'AWS test:tester:hmac'}) headers={'Authorization': 'AWS test:tester:hmac'})
status, headers, body = self.call_swift3(req) status, headers, body = self.call_swift3(req)
dom = xml.dom.minidom.parseString(body) elem = fromstring(body)
self.assertEquals(dom.getElementsByTagName('IsTruncated')[0]. self.assertEquals(elem.find('./IsTruncated').text, 'true')
childNodes[0].nodeValue, 'true')
def test_bucket_GET_max_keys(self): def test_bucket_GET_max_keys(self):
bucket_name = 'junk' bucket_name = 'junk'
@ -300,9 +289,8 @@ class TestSwift3(unittest.TestCase):
'QUERY_STRING': 'max-keys=5'}, 'QUERY_STRING': 'max-keys=5'},
headers={'Authorization': 'AWS test:tester:hmac'}) headers={'Authorization': 'AWS test:tester:hmac'})
status, headers, body = self.call_swift3(req) status, headers, body = self.call_swift3(req)
dom = xml.dom.minidom.parseString(body) elem = fromstring(body)
self.assertEquals(dom.getElementsByTagName('MaxKeys')[0]. self.assertEquals(elem.find('./MaxKeys').text, '5')
childNodes[0].nodeValue, '5')
_, path = self.swift.calls[-1] _, path = self.swift.calls[-1]
_, query_string = path.split('?') _, query_string = path.split('?')
args = dict(cgi.parse_qsl(query_string)) args = dict(cgi.parse_qsl(query_string))
@ -313,9 +301,8 @@ class TestSwift3(unittest.TestCase):
'QUERY_STRING': 'max-keys=5000'}, 'QUERY_STRING': 'max-keys=5000'},
headers={'Authorization': 'AWS test:tester:hmac'}) headers={'Authorization': 'AWS test:tester:hmac'})
status, headers, body = self.call_swift3(req) status, headers, body = self.call_swift3(req)
dom = xml.dom.minidom.parseString(body) elem = fromstring(body)
self.assertEquals(dom.getElementsByTagName('MaxKeys')[0]. self.assertEquals(elem.find('./MaxKeys').text, '1000')
childNodes[0].nodeValue, '1000')
_, path = self.swift.calls[-1] _, path = self.swift.calls[-1]
_, query_string = path.split('?') _, query_string = path.split('?')
args = dict(cgi.parse_qsl(query_string)) args = dict(cgi.parse_qsl(query_string))
@ -328,13 +315,10 @@ class TestSwift3(unittest.TestCase):
'delimiter=a&marker=b&prefix=c'}, 'delimiter=a&marker=b&prefix=c'},
headers={'Authorization': 'AWS test:tester:hmac'}) headers={'Authorization': 'AWS test:tester:hmac'})
status, headers, body = self.call_swift3(req) status, headers, body = self.call_swift3(req)
dom = xml.dom.minidom.parseString(body) elem = fromstring(body)
self.assertEquals(dom.getElementsByTagName('Prefix')[0]. self.assertEquals(elem.find('./Prefix').text, 'c')
childNodes[0].nodeValue, 'c') self.assertEquals(elem.find('./Marker').text, 'b')
self.assertEquals(dom.getElementsByTagName('Marker')[0]. self.assertEquals(elem.find('./Delimiter').text, 'a')
childNodes[0].nodeValue, 'b')
self.assertEquals(dom.getElementsByTagName('Delimiter')[0].
childNodes[0].nodeValue, 'a')
_, path = self.swift.calls[-1] _, path = self.swift.calls[-1]
_, query_string = path.split('?') _, query_string = path.split('?')
args = dict(cgi.parse_qsl(query_string)) args = dict(cgi.parse_qsl(query_string))
@ -386,13 +370,12 @@ class TestSwift3(unittest.TestCase):
status, headers, body = self.call_swift3(req) status, headers, body = self.call_swift3(req)
self.assertEquals(status.split()[0], '204') self.assertEquals(status.split()[0], '204')
def _check_acl(self, owner, resp): def _check_acl(self, owner, body):
dom = xml.dom.minidom.parseString("".join(resp)) elem = fromstring(body)
self.assertEquals(dom.firstChild.nodeName, 'AccessControlPolicy') self.assertEquals(elem.tag, 'AccessControlPolicy')
permission = dom.getElementsByTagName('Permission')[0] permission = elem.find('./AccessControlList/Grant/Permission').text
name = permission.childNodes[0].nodeValue self.assertEquals(permission, 'FULL_CONTROL')
self.assertEquals(name, 'FULL_CONTROL') name = elem.find('./AccessControlList/Grant/Grantee/ID').text
name = dom.getElementsByTagName('ID')[0].childNodes[0].nodeValue
self.assertEquals(name, owner) self.assertEquals(name, owner)
def test_bucket_acl_GET(self): def test_bucket_acl_GET(self):
@ -409,8 +392,8 @@ class TestSwift3(unittest.TestCase):
environ={'REQUEST_METHOD': 'GET'}, environ={'REQUEST_METHOD': 'GET'},
headers={'Authorization': 'AWS test:tester:hmac'}) headers={'Authorization': 'AWS test:tester:hmac'})
status, headers, body = self.call_swift3(req) status, headers, body = self.call_swift3(req)
dom = xml.dom.minidom.parseString(body) elem = fromstring(body)
self.assertEquals(dom.firstChild.nodeName, 'VersioningConfiguration') self.assertEquals(elem.tag, 'VersioningConfiguration')
def _test_object_GETorHEAD(self, method): def _test_object_GETorHEAD(self, method):
req = Request.blank('/bucket/object', req = Request.blank('/bucket/object',
@ -535,15 +518,12 @@ class TestSwift3(unittest.TestCase):
self.assertEquals(status.split()[0], '204') self.assertEquals(status.split()[0], '204')
def test_object_multi_DELETE(self): def test_object_multi_DELETE(self):
body = '<?xml version="1.0" encoding="UTF-8"?> \ elem = Element('Delete')
<Delete>\ for key in ['Key1', 'Key2']:
<Object>\ obj = SubElement(elem, 'Object')
<Key>Key1</Key>\ SubElement(obj, 'Key').text = key
</Object>\ body = tostring(elem, xml_declaration=True, encoding='UTF-8')
<Object>\
<Key>Key2</Key>\
</Object>\
</Delete>'
req = Request.blank('/bucket?delete', req = Request.blank('/bucket?delete',
environ={'REQUEST_METHOD': 'POST'}, environ={'REQUEST_METHOD': 'POST'},
headers={'Authorization': 'AWS test:tester:hmac'}, headers={'Authorization': 'AWS test:tester:hmac'},