Split controllers

The current Swift3 handles all the subresouce requests in BucketController and
Objectcontroller, but it complicates the source code a lot because of many
conditional branches and long line method definitions.  Creating a different
controller class for each subresource improves code readability and
maintainability.

This patch introduces the following controller classes to handle subresource
requests.

 - AclController
 - LocationController
 - LoggingStatusController
 - MultiObjectDeleteController
 - PartController
 - UploadsController
 - UploadController
 - VersioningController

Each class name is based on the resource type written in S3 server access log.
For example, LoggingStatusController handles logging subresource requests which
are logged as 'LOGGING_STATUS' operations in the s3 access log.

Change-Id: Ic6d04301303bc97c1ee9e69ead7db3628b45ac5d
This commit is contained in:
MORITA Kazutaka 2014-05-15 10:56:15 +09:00
parent 72058735f7
commit bb8d99d1cf
1 changed files with 368 additions and 150 deletions

View File

@ -370,15 +370,30 @@ def validate_bucket_name(name):
return True
class ServiceController(object):
class Controller(object):
"""
Base WSGI controller class for the middleware
"""
def __init__(self, req, app, account_name, token, container_name=None,
object_name=None, **kwargs):
self.app = app
self.account_name = account_name
self.container_name = container_name
self.object_name = object_name
req.environ['HTTP_X_AUTH_TOKEN'] = token
if object_name:
req.path_info = '/v1/%s/%s/%s' % (account_name, container_name,
object_name)
elif container_name:
req.path_info = '/v1/%s/%s' % (account_name, container_name)
else:
req.path_info = '/v1/%s' % (account_name)
class ServiceController(Controller):
"""
Handles account level requests.
"""
def __init__(self, req, app, account_name, token, **kwargs):
self.app = app
req.environ['HTTP_X_AUTH_TOKEN'] = token
req.path_info = '/v1/%s' % account_name
def GET(self, req):
"""
Handle GET Service request
@ -407,18 +422,10 @@ class ServiceController(object):
return HTTPOk(content_type='application/xml', body=body)
class BucketController(object):
class BucketController(Controller):
"""
Handles bucket request.
"""
def __init__(self, req, app, account_name, token, container_name,
**kwargs):
self.app = app
self.container_name = container_name
self.account_name = account_name
req.environ['HTTP_X_AUTH_TOKEN'] = token
req.path_info = '/v1/%s/%s' % (account_name, container_name)
def HEAD(self, req):
"""
Handle HEAD Bucket (Get Metadata) request
@ -447,16 +454,10 @@ class BucketController(object):
if req.params.get('max-keys').isdigit() is False:
return get_err_response('InvalidArgument')
if 'uploads' in req.params:
# Pass it through, the s3multi upload helper will handle it.
return self.app
max_keys = min(int(req.params.get('max-keys', MAX_BUCKET_LISTING)),
MAX_BUCKET_LISTING)
if 'acl' not in req.params:
#acl request sent with format=json etc confuses swift
req.query_string = 'format=json&limit=%s' % (max_keys + 1)
req.query_string = 'format=json&limit=%s' % (max_keys + 1)
if 'marker' in req.params:
req.query_string += '&marker=%s' % quote(req.params['marker'])
if 'prefix' in req.params:
@ -466,16 +467,6 @@ class BucketController(object):
quote(req.params['delimiter'])
resp = req.get_response(self.app)
status = resp.status_int
headers = resp.headers
if is_success(status) and 'acl' in req.params:
return get_acl(self.account_name, headers)
if 'versioning' in req.params:
# Just report there is no versioning configured here.
body = ('<VersioningConfiguration '
'xmlns="http://s3.amazonaws.com/doc/2006-03-01/"/>')
return HTTPOk(body=body, content_type="text/plain")
if status != HTTP_OK:
if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
@ -485,23 +476,6 @@ class BucketController(object):
else:
return get_err_response('InvalidURI')
if 'location' in req.params:
body = ('<?xml version="1.0" encoding="UTF-8"?>'
'<LocationConstraint '
'xmlns="http://s3.amazonaws.com/doc/2006-03-01/"')
if self.location == 'US':
body += '/>'
else:
body += ('>%s</LocationConstraint>' % self.location)
return HTTPOk(body=body, content_type='application/xml')
if 'logging' in req.params:
# logging disabled
body = ('<?xml version="1.0" encoding="UTF-8"?>'
'<BucketLoggingStatus '
'xmlns="http://doc.s3.amazonaws.com/2006-03-01" />')
return HTTPOk(body=body, content_type='application/xml')
objects = loads(resp.body)
body = ('<?xml version="1.0" encoding="UTF-8"?>'
'<ListBucketResult '
@ -565,17 +539,6 @@ class BucketController(object):
except (ValueError, TypeError):
return get_err_response('InvalidArgument')
if 'acl' in req.params:
# We very likely have an XML-based ACL request.
translated_acl = swift_acl_translate(req.body, xml=True)
if translated_acl == 'Unsupported':
return get_err_response('Unsupported')
elif translated_acl == 'InvalidArgument':
return get_err_response('InvalidArgument')
for header, acl in translated_acl:
req.headers[header] = acl
req.method = 'POST'
resp = req.get_response(self.app)
status = resp.status_int
@ -608,101 +571,18 @@ class BucketController(object):
return HTTPNoContent()
def _delete_multiple_objects(self, req):
def _object_key_iter(xml):
dom = parseString(xml)
delete = dom.getElementsByTagName('Delete')[0]
for obj in delete.getElementsByTagName('Object'):
key = obj.getElementsByTagName('Key')[0].firstChild.data
version = None
if obj.getElementsByTagName('VersionId').length > 0:
version = obj.getElementsByTagName('VersionId')[0]\
.firstChild.data
yield (key, version)
def _get_deleted_elem(key):
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):
if version is not None:
# TODO: delete the specific version of the object
return get_err_response('Unsupported')
sub_req = Request(req.environ.copy())
sub_req.query_string = ''
sub_req.content_length = 0
sub_req.method = 'DELETE'
controller = ObjectController(sub_req, self.app, self.account_name,
req.environ['HTTP_X_AUTH_TOKEN'],
self.container_name, key)
sub_resp = controller.DELETE(sub_req)
status = sub_resp.status_int
if status == HTTP_NO_CONTENT or status == HTTP_NOT_FOUND:
body += _get_deleted_elem(key)
else:
if status == HTTP_UNAUTHORIZED:
body += _get_err_elem(key, 'AccessDenied', 'Access Denied')
else:
body += _get_err_elem(key, 'InvalidURI', 'Invalid URI')
body += '</DeleteResult>\r\n'
return HTTPOk(body=body)
def POST(self, req):
"""
Handle POST Bucket (Delete/Upload Multiple Objects) request
Handle POST Bucket request
"""
if 'delete' in req.params:
return self._delete_multiple_objects(req)
if 'uploads' in req.params:
# Pass it through, the s3multi upload helper will handle it.
return self.app
if 'uploadId' in req.params:
# Pass it through, the s3multi upload helper will handle it.
return self.app
return get_err_response('Unsupported')
class ObjectController(object):
class ObjectController(Controller):
"""
Handles requests on objects
"""
def __init__(self, req, app, account_name, token, container_name,
object_name, **kwargs):
self.app = app
self.account_name = account_name
self.container_name = container_name
req.environ['HTTP_X_AUTH_TOKEN'] = token
req.path_info = '/v1/%s/%s/%s' % (account_name, container_name,
object_name)
def GETorHEAD(self, req):
# Let s3multi handle it.
if 'uploadId' in req.params or 'uploads' in req.params:
return self.app
if 'acl' in req.params:
# ACL requests need to make a HEAD call rather than GET
req.method = 'HEAD'
req.script_name = ''
req.query_string = ''
resp = req.get_response(self.app)
status = resp.status_int
headers = resp.headers
@ -711,11 +591,6 @@ class ObjectController(object):
resp.app_iter = None
if is_success(status):
if 'acl' in req.params:
# Method must be GET or the body wont be returned to the caller
req.environ['REQUEST_METHOD'] = 'GET'
return get_acl(self.account_name, headers)
new_hdrs = {}
for key, val in headers.iteritems():
_key = key.lower()
@ -815,6 +690,332 @@ class ObjectController(object):
return HTTPNoContent()
class AclController(Controller):
"""
Handles the following APIs:
- GET Bucket acl
- PUT Bucket acl
- GET Object acl
- PUT Object acl
Those APIs are logged as ACL operations in the S3 server log.
"""
def GET(self, req):
"""
Handles GET Bucket acl and GET Object acl.
"""
if self.object_name:
# Handle Object ACL
# ACL requests need to make a HEAD call rather than GET
req.method = 'HEAD'
req.script_name = ''
req.query_string = ''
resp = req.get_response(self.app)
status = resp.status_int
headers = resp.headers
if is_success(status):
# Method must be GET or the body wont be returned to the caller
req.environ['REQUEST_METHOD'] = 'GET'
return get_acl(self.account_name, headers)
elif status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
return get_err_response('AccessDenied')
elif status == HTTP_NOT_FOUND:
return get_err_response('NoSuchKey')
else:
return get_err_response('InvalidURI')
else:
# Handle Bucket ACL
resp = req.get_response(self.app)
status = resp.status_int
headers = resp.headers
if is_success(status):
return get_acl(self.account_name, headers)
if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
return get_err_response('AccessDenied')
elif status == HTTP_NOT_FOUND:
return get_err_response('NoSuchBucket')
else:
return get_err_response('InvalidURI')
def PUT(self, req):
"""
Handles PUT Bucket acl and PUT Object acl.
"""
if self.object_name:
# Handle Object ACL
return get_err_response('Unsupported')
else:
# Handle Bucket ACL
# We very likely have an XML-based ACL request.
translated_acl = swift_acl_translate(req.body, xml=True)
if translated_acl == 'Unsupported':
return get_err_response('Unsupported')
elif translated_acl == 'InvalidArgument':
return get_err_response('InvalidArgument')
for header, acl in translated_acl:
req.headers[header] = acl
req.method = 'POST'
resp = req.get_response(self.app)
status = resp.status_int
if status != HTTP_CREATED and status != HTTP_NO_CONTENT:
if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
return get_err_response('AccessDenied')
elif status == HTTP_ACCEPTED:
return get_err_response('BucketAlreadyExists')
else:
return get_err_response('InvalidURI')
return HTTPOk(headers={'Location': self.container_name})
class LocationController(Controller):
"""
Handles GET Bucket location, which is logged as a LOCATION operation in the
S3 server log.
"""
def GET(self, req):
"""
Handles GET Bucket location.
"""
resp = req.get_response(self.app)
status = resp.status_int
if status != HTTP_OK:
if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
return get_err_response('AccessDenied')
elif status == HTTP_NOT_FOUND:
return get_err_response('NoSuchBucket')
else:
return get_err_response('InvalidURI')
body = ('<?xml version="1.0" encoding="UTF-8"?>'
'<LocationConstraint '
'xmlns="http://s3.amazonaws.com/doc/2006-03-01/"')
if self.location == 'US':
body += '/>'
else:
body += ('>%s</LocationConstraint>' % self.location)
return HTTPOk(body=body, content_type='application/xml')
class LoggingStatusController(Controller):
"""
Handles the following APIs:
- GET Bucket logging
- PUT Bucket logging
Those APIs are logged as LOGGING_STATUS operations in the S3 server log.
"""
def GET(self, req):
"""
Handles GET Bucket logging.
"""
resp = req.get_response(self.app)
status = resp.status_int
if status != HTTP_OK:
if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
return get_err_response('AccessDenied')
elif status == HTTP_NOT_FOUND:
return get_err_response('NoSuchBucket')
else:
return get_err_response('InvalidURI')
# logging disabled
body = ('<?xml version="1.0" encoding="UTF-8"?>'
'<BucketLoggingStatus '
'xmlns="http://doc.s3.amazonaws.com/2006-03-01" />')
return HTTPOk(body=body, content_type='application/xml')
def PUT(self, req):
"""
Handles PUT Bucket logging.
"""
return get_err_response('Unsupported')
class MultiObjectDeleteController(Controller):
"""
Handles Delete Multiple Objects, which is logged as a MULTI_OBJECT_DELETE
operation in the S3 server log.
"""
def POST(self, req):
"""
Handles Delete Multiple Objects.
"""
def object_key_iter(xml):
dom = parseString(xml)
delete = dom.getElementsByTagName('Delete')[0]
for obj in delete.getElementsByTagName('Object'):
key = obj.getElementsByTagName('Key')[0].firstChild.data
version = None
if obj.getElementsByTagName('VersionId').length > 0:
version = obj.getElementsByTagName('VersionId')[0]\
.firstChild.data
yield (key, version)
def get_deleted_elem(key):
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):
if version is not None:
# TODO: delete the specific version of the object
return get_err_response('Unsupported')
sub_req = Request(req.environ.copy())
sub_req.query_string = ''
sub_req.content_length = 0
sub_req.method = 'DELETE'
controller = ObjectController(sub_req, self.app, self.account_name,
req.environ['HTTP_X_AUTH_TOKEN'],
self.container_name, key)
sub_resp = controller.DELETE(sub_req)
status = sub_resp.status_int
if status == HTTP_NO_CONTENT or status == HTTP_NOT_FOUND:
body += get_deleted_elem(key)
else:
if status == HTTP_UNAUTHORIZED:
body += get_err_elem(key, 'AccessDenied', 'Access Denied')
else:
body += get_err_elem(key, 'InvalidURI', 'Invalid URI')
body += '</DeleteResult>\r\n'
return HTTPOk(body=body)
class PartController(Controller):
"""
Handles the following APIs:
- Upload Part
- Upload Part - Copy
Those APIs are logged as PART operations in the S3 server log.
"""
def PUT(self, req):
"""
Handles Upload Part and Upload Part Copy.
"""
# Pass it through, the s3multi upload helper will handle it.
return self.app
class UploadsController(Controller):
"""
Handles the following APIs:
- List Multipart Uploads
- Initiate Multipart Upload
Those APIs are logged as UPLOADS operations in the S3 server log.
"""
def GET(self, req):
"""
Handles List Multipart Uploads
"""
# Pass it through, the s3multi upload helper will handle it.
return self.app
def POST(self, req):
"""
Handles Initiate Multipart Upload.
"""
# Pass it through, the s3multi upload helper will handle it.
return self.app
class UploadController(Controller):
"""
Handles the following APIs:
- List Parts
- Abort Multipart Upload
- Complete Multipart Upload
Those APIs are logged as UPLOAD operations in the S3 server log.
"""
def GET(self, req):
"""
Handles List Parts.
"""
# Pass it through, the s3multi upload helper will handle it.
return self.app
def DELETE(self, req):
"""
Handles Abort Multipart Upload.
"""
# Pass it through, the s3multi upload helper will handle it.
return self.app
def POST(self, req):
"""
Handles Complete Multipart Upload.
"""
# Pass it through, the s3multi upload helper will handle it.
return self.app
class VersioningController(Controller):
"""
Handles the following APIs:
- GET Bucket versioning
- PUT Bucket versioning
Those APIs are logged as VERSIONING operations in the S3 server log.
"""
def GET(self, req):
"""
Handles GET Bucket versioning.
"""
resp = req.get_response(self.app)
status = resp.status_int
if status != HTTP_OK:
if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
return get_err_response('AccessDenied')
elif status == HTTP_NOT_FOUND:
return get_err_response('NoSuchBucket')
else:
return get_err_response('InvalidURI')
# Just report there is no versioning configured here.
body = ('<VersioningConfiguration '
'xmlns="http://s3.amazonaws.com/doc/2006-03-01/"/>')
return HTTPOk(body=body, content_type="text/plain")
def PUT(self, req):
"""
Handles PUT Bucket versioning.
"""
return get_err_response('Unsupported')
class Swift3Middleware(object):
"""Swift3 S3 compatibility midleware"""
def __init__(self, app, conf, *args, **kwargs):
@ -826,6 +1027,23 @@ class Swift3Middleware(object):
container, obj = req.split_path(0, 2, True)
d = dict(container_name=container, object_name=obj)
if 'acl' in req.params:
return AclController, d
if 'delete' in req.params:
return MultiObjectDeleteController, d
if 'location' in req.params:
return LocationController, d
if 'logging' in req.params:
return LoggingStatusController, d
if 'partNumber' in req.params:
return PartController, d
if 'uploadId' in req.params:
return UploadController, d
if 'uploads' in req.params:
return UploadsController, d
if 'versioning' in req.params:
return VersioningController, d
if container and obj:
if req.method == 'POST':
if 'uploads' in req.params or 'uploadId' in req.params: