mpu: refactor object-versioning part 2
Give ObjectContext some variables to avoid passing them around between methods. Break out/rename some methods for clarity. Add an is_versioning_enabled helper function. Change-Id: If901c4e8fe9e23a9f6515e4626c6aac04194c98b Signed-off-by: Alistair Coles <alistairncoles@gmail.com>
This commit is contained in:
@@ -182,6 +182,11 @@ SYSMETA_PARENT_CONT = get_sys_meta_prefix('container') + 'parent-container'
|
|||||||
SYSMETA_VERSIONS_SYMLINK = get_sys_meta_prefix('object') + 'versions-symlink'
|
SYSMETA_VERSIONS_SYMLINK = get_sys_meta_prefix('object') + 'versions-symlink'
|
||||||
|
|
||||||
|
|
||||||
|
def is_versioning_enabled(container_info):
|
||||||
|
return config_true_value(container_info.get(
|
||||||
|
'sysmeta', {}).get('versions-enabled'))
|
||||||
|
|
||||||
|
|
||||||
def build_versions_container_name(container_name):
|
def build_versions_container_name(container_name):
|
||||||
"""
|
"""
|
||||||
Get the name of the versions container for given ``container_name``.
|
Get the name of the versions container for given ``container_name``.
|
||||||
@@ -312,6 +317,29 @@ class ObjectVersioningContext(WSGIContext):
|
|||||||
|
|
||||||
|
|
||||||
class ObjectContext(ObjectVersioningContext):
|
class ObjectContext(ObjectVersioningContext):
|
||||||
|
def __init__(self, wsgi_app, logger, api_version, account,
|
||||||
|
container, obj, versions_cont, is_enabled):
|
||||||
|
"""
|
||||||
|
Note that account, container, obj should be unquoted by caller
|
||||||
|
if the url path is under url encoding (e.g. %FF)
|
||||||
|
|
||||||
|
:param wsgi_app: WSGI application
|
||||||
|
:param logger: logger object
|
||||||
|
:param api_version: should be v1 unless swift bumps api version
|
||||||
|
:param account: account name string
|
||||||
|
:param container: container name string
|
||||||
|
:param obj: object name string
|
||||||
|
:param versions_cont: container holding versions of the requested obj
|
||||||
|
:param is_enabled: is versioning currently enabled
|
||||||
|
"""
|
||||||
|
super().__init__(wsgi_app, logger)
|
||||||
|
self.api_version = api_version
|
||||||
|
self.account = account
|
||||||
|
self.container = container
|
||||||
|
self.obj = obj
|
||||||
|
self.versions_cont = versions_cont
|
||||||
|
self.is_enabled = is_enabled
|
||||||
|
|
||||||
def get_version(self, req):
|
def get_version(self, req):
|
||||||
"""
|
"""
|
||||||
Get version to use for a client request.
|
Get version to use for a client request.
|
||||||
@@ -361,14 +389,13 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
return source_resp
|
return source_resp
|
||||||
|
|
||||||
def _put_versioned_obj(self, req, put_path_info, source_resp):
|
def _put_versioned_obj(self, req, put_path_info, source_resp):
|
||||||
# Create a new Request object to PUT to the versions container, copying
|
# Create a new Request object to PUT to the versions container.
|
||||||
# all headers from the source object apart from x-timestamp.
|
headers = {'X-Backend-Allow-Reserved-Names': 'true'}
|
||||||
|
headers.update(source_resp.headers)
|
||||||
put_req = make_pre_authed_request(
|
put_req = make_pre_authed_request(
|
||||||
req.environ, path=wsgi_quote(put_path_info), method='PUT',
|
req.environ, path=wsgi_quote(put_path_info), method='PUT',
|
||||||
headers={'X-Backend-Allow-Reserved-Names': 'true'},
|
headers=headers, swift_source='OV')
|
||||||
swift_source='OV')
|
|
||||||
copy_header_subset(source_resp, put_req,
|
|
||||||
lambda k: k.lower() != 'x-timestamp')
|
|
||||||
put_req.environ['wsgi.input'] = FileLikeIter(source_resp.app_iter)
|
put_req.environ['wsgi.input'] = FileLikeIter(source_resp.app_iter)
|
||||||
slo_size = put_req.headers.get('X-Object-Sysmeta-Slo-Size')
|
slo_size = put_req.headers.get('X-Object-Sysmeta-Slo-Size')
|
||||||
if slo_size:
|
if slo_size:
|
||||||
@@ -380,12 +407,10 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
close_if_possible(source_resp.app_iter)
|
close_if_possible(source_resp.app_iter)
|
||||||
return put_resp
|
return put_resp
|
||||||
|
|
||||||
def _put_versioned_obj_from_client(self, req, versions_cont, api_version,
|
def _put_versioned_obj_from_client(self, req, version):
|
||||||
account_name, object_name):
|
vers_obj_name = build_versions_object_name(self.obj, version)
|
||||||
version = self.get_version(req)
|
|
||||||
vers_obj_name = build_versions_object_name(object_name, version)
|
|
||||||
put_path_info = "/%s/%s/%s/%s" % (
|
put_path_info = "/%s/%s/%s/%s" % (
|
||||||
api_version, account_name, versions_cont, vers_obj_name)
|
self.api_version, self.account, self.versions_cont, vers_obj_name)
|
||||||
# Consciously *do not* set swift_source here -- this req is in charge
|
# Consciously *do not* set swift_source here -- this req is in charge
|
||||||
# of reading bytes from the client, don't let it look like that data
|
# of reading bytes from the client, don't let it look like that data
|
||||||
# movement is due to some internal-to-swift thing
|
# movement is due to some internal-to-swift thing
|
||||||
@@ -435,10 +460,8 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
|
|
||||||
return (put_resp, vers_obj_name, put_bytes, put_content_type)
|
return (put_resp, vers_obj_name, put_bytes, put_content_type)
|
||||||
|
|
||||||
def _put_symlink_to_version(self, req, versions_cont, put_vers_obj_name,
|
def _put_symlink_to_version(self, req, put_vers_obj_name, put_etag,
|
||||||
api_version, account_name, object_name,
|
put_bytes, put_content_type):
|
||||||
put_etag, put_bytes, put_content_type):
|
|
||||||
|
|
||||||
req.method = 'PUT'
|
req.method = 'PUT'
|
||||||
# inch x-timestamp forward, just in case
|
# inch x-timestamp forward, just in case
|
||||||
req.ensure_x_timestamp()
|
req.ensure_x_timestamp()
|
||||||
@@ -449,7 +472,7 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
# N.B. in stack mode DELETE we use content_type from listing
|
# N.B. in stack mode DELETE we use content_type from listing
|
||||||
req.headers['Content-Type'] = put_content_type
|
req.headers['Content-Type'] = put_content_type
|
||||||
req.headers[TGT_OBJ_SYMLINK_HDR] = wsgi_quote('%s/%s' % (
|
req.headers[TGT_OBJ_SYMLINK_HDR] = wsgi_quote('%s/%s' % (
|
||||||
versions_cont, put_vers_obj_name))
|
self.versions_cont, put_vers_obj_name))
|
||||||
req.headers[SYSMETA_VERSIONS_SYMLINK] = 'true'
|
req.headers[SYSMETA_VERSIONS_SYMLINK] = 'true'
|
||||||
req.headers[SYMLOOP_EXTEND] = 'true'
|
req.headers[SYMLOOP_EXTEND] = 'true'
|
||||||
req.headers[ALLOW_RESERVED_NAMES] = 'true'
|
req.headers[ALLOW_RESERVED_NAMES] = 'true'
|
||||||
@@ -488,21 +511,15 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
# could not version the data, bail
|
# could not version the data, bail
|
||||||
raise HTTPServiceUnavailable(request=req)
|
raise HTTPServiceUnavailable(request=req)
|
||||||
|
|
||||||
def _copy_current(self, req, versions_cont, api_version, account_name,
|
def _copy_current(self, req):
|
||||||
object_name):
|
"""
|
||||||
'''
|
|
||||||
Check if the current version of the object is a versions-symlink
|
Check if the current version of the object is a versions-symlink
|
||||||
if not, it's because this object was added to the container when
|
if not, it's because this object was added to the container when
|
||||||
versioning was not enabled. We'll need to copy it into the versions
|
versioning was not enabled. We'll need to copy it into the versions
|
||||||
containers now.
|
containers now.
|
||||||
|
|
||||||
:param req: original request.
|
:param req: original request.
|
||||||
:param versions_cont: container where previous versions of the object
|
"""
|
||||||
are stored.
|
|
||||||
:param api_version: api version.
|
|
||||||
:param account_name: account name.
|
|
||||||
:param object_name: name of object of original request
|
|
||||||
'''
|
|
||||||
# validate the write access to the versioned container before
|
# validate the write access to the versioned container before
|
||||||
# making any backend requests
|
# making any backend requests
|
||||||
if 'swift.authorize' in req.environ:
|
if 'swift.authorize' in req.environ:
|
||||||
@@ -518,7 +535,7 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
if get_resp.status_int == HTTP_NOT_FOUND:
|
if get_resp.status_int == HTTP_NOT_FOUND:
|
||||||
# nothing to version, proceed with original request
|
# nothing to version, proceed with original request
|
||||||
drain_and_close(get_resp)
|
drain_and_close(get_resp)
|
||||||
return get_resp
|
return
|
||||||
|
|
||||||
# check for any other errors
|
# check for any other errors
|
||||||
self._check_response_error(req, get_resp)
|
self._check_response_error(req, get_resp)
|
||||||
@@ -526,15 +543,15 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
if get_resp.headers.get(SYSMETA_VERSIONS_SYMLINK) == 'true':
|
if get_resp.headers.get(SYSMETA_VERSIONS_SYMLINK) == 'true':
|
||||||
# existing object is a VW symlink; no action required
|
# existing object is a VW symlink; no action required
|
||||||
drain_and_close(get_resp)
|
drain_and_close(get_resp)
|
||||||
return get_resp
|
return
|
||||||
|
|
||||||
# if there's an existing object, then copy it to
|
# if there's an existing object, then copy it to
|
||||||
# X-Versions-Location
|
# X-Versions-Location
|
||||||
version = self.get_null_version(get_resp)
|
version = self.get_null_version(get_resp)
|
||||||
vers_obj_name = build_versions_object_name(object_name, version)
|
get_resp.headers.pop('x-timestamp', None)
|
||||||
|
vers_obj_name = build_versions_object_name(self.obj, version)
|
||||||
put_path_info = "/%s/%s/%s/%s" % (
|
put_path_info = "/%s/%s/%s/%s" % (
|
||||||
api_version, account_name, versions_cont, vers_obj_name)
|
self.api_version, self.account, self.versions_cont, vers_obj_name)
|
||||||
put_resp = self._put_versioned_obj(req, put_path_info, get_resp)
|
put_resp = self._put_versioned_obj(req, put_path_info, get_resp)
|
||||||
|
|
||||||
if put_resp.status_int == HTTP_NOT_FOUND:
|
if put_resp.status_int == HTTP_NOT_FOUND:
|
||||||
@@ -545,8 +562,7 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
|
|
||||||
self._check_response_error(req, put_resp)
|
self._check_response_error(req, put_resp)
|
||||||
|
|
||||||
def handle_put(self, req, versions_cont, api_version,
|
def handle_put(self, req):
|
||||||
account_name, object_name, is_enabled):
|
|
||||||
"""
|
"""
|
||||||
Check if the current version of the object is a versions-symlink
|
Check if the current version of the object is a versions-symlink
|
||||||
if not, it's because this object was added to the container when
|
if not, it's because this object was added to the container when
|
||||||
@@ -557,38 +573,26 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
and add a static symlink in the versioned container.
|
and add a static symlink in the versioned container.
|
||||||
|
|
||||||
:param req: original request.
|
:param req: original request.
|
||||||
:param versions_cont: container where previous versions of the object
|
|
||||||
are stored.
|
|
||||||
:param api_version: api version.
|
|
||||||
:param account_name: account name.
|
|
||||||
:param object_name: name of object of original request
|
|
||||||
"""
|
"""
|
||||||
# handle object request for a disabled versioned container.
|
# handle object request for a disabled versioned container.
|
||||||
if not is_enabled:
|
if not self.is_enabled:
|
||||||
return req.get_response(self.app)
|
return req.get_response(self.app)
|
||||||
|
|
||||||
# attempt to copy current object to versions container
|
# attempt to copy current object to versions container
|
||||||
self._copy_current(req, versions_cont, api_version, account_name,
|
self._copy_current(req)
|
||||||
object_name)
|
|
||||||
|
|
||||||
# write client's put directly to versioned container
|
# write client's put directly to versioned container
|
||||||
req.ensure_x_timestamp()
|
req.ensure_x_timestamp()
|
||||||
|
version = self.get_version(req)
|
||||||
put_resp, put_vers_obj_name, put_bytes, put_content_type = \
|
put_resp, put_vers_obj_name, put_bytes, put_content_type = \
|
||||||
self._put_versioned_obj_from_client(req, versions_cont,
|
self._put_versioned_obj_from_client(req, version)
|
||||||
api_version, account_name,
|
|
||||||
object_name)
|
|
||||||
|
|
||||||
# and add an static symlink to original container
|
# and add an static symlink to original container
|
||||||
target_etag = put_resp.headers['Etag']
|
target_etag = put_resp.headers['Etag']
|
||||||
return self._put_symlink_to_version(req, versions_cont,
|
return self._put_symlink_to_version(
|
||||||
put_vers_obj_name, api_version,
|
req, put_vers_obj_name, target_etag, put_bytes, put_content_type)
|
||||||
account_name, object_name,
|
|
||||||
target_etag, put_bytes,
|
|
||||||
put_content_type)
|
|
||||||
|
|
||||||
def handle_delete(self, req, versions_cont, api_version,
|
def handle_delete(self, req):
|
||||||
account_name, container_name,
|
|
||||||
object_name, is_enabled):
|
|
||||||
"""
|
"""
|
||||||
Handle DELETE requests.
|
Handle DELETE requests.
|
||||||
|
|
||||||
@@ -596,24 +600,18 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
delete marker before proceeding with original request.
|
delete marker before proceeding with original request.
|
||||||
|
|
||||||
:param req: original request.
|
:param req: original request.
|
||||||
:param versions_cont: container where previous versions of the object
|
|
||||||
are stored.
|
|
||||||
:param api_version: api version.
|
|
||||||
:param account_name: account name.
|
|
||||||
:param object_name: name of object of original request
|
|
||||||
"""
|
"""
|
||||||
# handle object request for a disabled versioned container.
|
# handle object request for a disabled versioned container.
|
||||||
if not is_enabled:
|
if not self.is_enabled:
|
||||||
return req.get_response(self.app)
|
return req.get_response(self.app)
|
||||||
|
|
||||||
self._copy_current(req, versions_cont, api_version,
|
self._copy_current(req)
|
||||||
account_name, object_name)
|
|
||||||
|
|
||||||
req.ensure_x_timestamp()
|
req.ensure_x_timestamp()
|
||||||
version = self.get_version(req)
|
version = self.get_version(req)
|
||||||
marker_name = build_versions_object_name(object_name, version)
|
marker_name = build_versions_object_name(self.obj, version)
|
||||||
marker_path = "/%s/%s/%s/%s" % (
|
marker_path = "/%s/%s/%s/%s" % (
|
||||||
api_version, account_name, versions_cont, marker_name)
|
self.api_version, self.account, self.versions_cont, marker_name)
|
||||||
marker_headers = {
|
marker_headers = {
|
||||||
# Definitive source of truth is Content-Type, and since we add
|
# Definitive source of truth is Content-Type, and since we add
|
||||||
# a swift_* param, we know users haven't set it themselves.
|
# a swift_* param, we know users haven't set it themselves.
|
||||||
@@ -641,18 +639,15 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
drain_and_close(resp)
|
drain_and_close(resp)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def handle_post(self, req, versions_cont, account):
|
def handle_post(self, req):
|
||||||
'''
|
"""
|
||||||
Handle a POST request to an object in a versioned container.
|
Handle a POST request to an object in a versioned container.
|
||||||
|
|
||||||
If the response is a 307 because the POST went to a symlink,
|
If the response is a 307 because the POST went to a symlink,
|
||||||
follow the symlink and send the request to the versioned object
|
follow the symlink and send the request to the versioned object
|
||||||
|
|
||||||
:param req: original request.
|
:param req: original request.
|
||||||
:param versions_cont: container where previous versions of the object
|
"""
|
||||||
are stored.
|
|
||||||
:param account: account name.
|
|
||||||
'''
|
|
||||||
# create eventual post request before
|
# create eventual post request before
|
||||||
# encryption middleware changes the request headers
|
# encryption middleware changes the request headers
|
||||||
post_req = make_pre_authed_request(
|
post_req = make_pre_authed_request(
|
||||||
@@ -671,7 +666,7 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
|
|
||||||
# Only follow if the version container matches
|
# Only follow if the version container matches
|
||||||
if split_path(loc, 4, 4, True)[1:3] == [
|
if split_path(loc, 4, 4, True)[1:3] == [
|
||||||
account, versions_cont]:
|
self.account, self.versions_cont]:
|
||||||
drain_and_close(resp)
|
drain_and_close(resp)
|
||||||
post_req.path_info = loc
|
post_req.path_info = loc
|
||||||
resp = post_req.get_response(self.app)
|
resp = post_req.get_response(self.app)
|
||||||
@@ -700,32 +695,36 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
drain_and_close(hresp)
|
drain_and_close(hresp)
|
||||||
return head_is_tombstone, symlink_target
|
return head_is_tombstone, symlink_target
|
||||||
|
|
||||||
def handle_delete_version(self, req, versions_cont, api_version,
|
def handle_delete_with_version_id(self, req, version):
|
||||||
account_name, container_name,
|
"""
|
||||||
object_name, is_enabled, version):
|
Handle a DELETE?version_id request.
|
||||||
|
|
||||||
|
:param req: original request.
|
||||||
|
:param version: version to delete.
|
||||||
|
"""
|
||||||
if version == 'null':
|
if version == 'null':
|
||||||
# let the request go directly through to the is_latest link
|
# let the request go directly through to the is_latest link
|
||||||
return
|
return req.get_response(self.app)
|
||||||
auth_token_header = {'X-Auth-Token': req.headers.get('X-Auth-Token')}
|
auth_token_header = {'X-Auth-Token': req.headers.get('X-Auth-Token')}
|
||||||
head_is_tombstone, symlink_target = self._check_head(
|
head_is_tombstone, symlink_target = self._check_head(
|
||||||
req, auth_token_header)
|
req, auth_token_header)
|
||||||
|
|
||||||
versions_obj = build_versions_object_name(object_name, version)
|
versions_obj = build_versions_object_name(self.obj, version)
|
||||||
req_obj_path = '%s/%s' % (versions_cont, versions_obj)
|
req_obj_path = '%s/%s' % (self.versions_cont, versions_obj)
|
||||||
if head_is_tombstone or not symlink_target or (
|
if head_is_tombstone or not symlink_target or (
|
||||||
wsgi_unquote(symlink_target) != wsgi_unquote(req_obj_path)):
|
wsgi_unquote(symlink_target) != wsgi_unquote(req_obj_path)):
|
||||||
# If there's no current version (i.e., tombstone or unversioned
|
# If there's no current version (i.e., tombstone or unversioned
|
||||||
# object) or if current version links to another version, then
|
# object) or if current version links to another version, then
|
||||||
# just delete the version requested to be deleted
|
# just delete the version requested to be deleted
|
||||||
req.path_info = "/%s/%s/%s/%s" % (
|
req.path_info = "/%s/%s/%s/%s" % (
|
||||||
api_version, account_name, versions_cont, versions_obj)
|
self.api_version, self.account, self.versions_cont,
|
||||||
|
versions_obj)
|
||||||
req.headers['X-Backend-Allow-Reserved-Names'] = 'true'
|
req.headers['X-Backend-Allow-Reserved-Names'] = 'true'
|
||||||
if head_is_tombstone or not symlink_target:
|
if head_is_tombstone or not symlink_target:
|
||||||
resp_version_id = 'null'
|
resp_version_id = 'null'
|
||||||
else:
|
else:
|
||||||
_, vers_obj_name = wsgi_unquote(symlink_target).split('/', 1)
|
_, vers_obj_name = wsgi_unquote(symlink_target).split('/', 1)
|
||||||
resp_version_id = parse_versions_object_name(
|
resp_version_id = parse_versions_object_name(vers_obj_name)[1]
|
||||||
vers_obj_name)[1]
|
|
||||||
else:
|
else:
|
||||||
# if version-id is the latest version, delete the link too
|
# if version-id is the latest version, delete the link too
|
||||||
# First, kill the link...
|
# First, kill the link...
|
||||||
@@ -736,7 +735,8 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
|
|
||||||
# *then* the backing data
|
# *then* the backing data
|
||||||
req.path_info = "/%s/%s/%s/%s" % (
|
req.path_info = "/%s/%s/%s/%s" % (
|
||||||
api_version, account_name, versions_cont, versions_obj)
|
self.api_version, self.account, self.versions_cont,
|
||||||
|
versions_obj)
|
||||||
req.headers['X-Backend-Allow-Reserved-Names'] = 'true'
|
req.headers['X-Backend-Allow-Reserved-Names'] = 'true'
|
||||||
resp_version_id = 'null'
|
resp_version_id = 'null'
|
||||||
resp = req.get_response(self.app)
|
resp = req.get_response(self.app)
|
||||||
@@ -744,11 +744,13 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
resp.headers['X-Object-Current-Version-Id'] = resp_version_id
|
resp.headers['X-Object-Current-Version-Id'] = resp_version_id
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def handle_put_version(self, req, versions_cont, api_version, account_name,
|
def handle_put_with_version_id(self, req, version):
|
||||||
container, object_name, is_enabled, version):
|
|
||||||
"""
|
"""
|
||||||
Handle a PUT?version-id request and create/update the is_latest link to
|
Handle a PUT?version-id request and create/update the is_latest link to
|
||||||
point to the specific version. Expects a valid 'version' id.
|
point to the specific version. Expects a valid 'version' id.
|
||||||
|
|
||||||
|
:param req: original request.
|
||||||
|
:param version: version to make the latest.
|
||||||
"""
|
"""
|
||||||
if req.is_chunked:
|
if req.is_chunked:
|
||||||
has_body = (req.body_file.read(1) != b'')
|
has_body = (req.body_file.read(1) != b'')
|
||||||
@@ -761,9 +763,10 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
body='PUT version-id requests require a zero byte body',
|
body='PUT version-id requests require a zero byte body',
|
||||||
request=req,
|
request=req,
|
||||||
content_type='text/plain')
|
content_type='text/plain')
|
||||||
versions_obj_name = build_versions_object_name(object_name, version)
|
versions_obj_name = build_versions_object_name(self.obj, version)
|
||||||
versioned_obj_path = "/%s/%s/%s/%s" % (
|
versioned_obj_path = "/%s/%s/%s/%s" % (
|
||||||
api_version, account_name, versions_cont, versions_obj_name)
|
self.api_version, self.account, self.versions_cont,
|
||||||
|
versions_obj_name)
|
||||||
obj_head_headers = {'X-Backend-Allow-Reserved-Names': 'true'}
|
obj_head_headers = {'X-Backend-Allow-Reserved-Names': 'true'}
|
||||||
head_req = make_pre_authed_request(
|
head_req = make_pre_authed_request(
|
||||||
req.environ, path=wsgi_quote(versioned_obj_path) + '?symlink=get',
|
req.environ, path=wsgi_quote(versioned_obj_path) + '?symlink=get',
|
||||||
@@ -789,12 +792,52 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
put_bytes = head_resp.content_length
|
put_bytes = head_resp.content_length
|
||||||
put_content_type = head_resp.headers['Content-Type']
|
put_content_type = head_resp.headers['Content-Type']
|
||||||
resp = self._put_symlink_to_version(
|
resp = self._put_symlink_to_version(
|
||||||
req, versions_cont, versions_obj_name, api_version, account_name,
|
req, versions_obj_name, put_etag, put_bytes, put_content_type)
|
||||||
object_name, put_etag, put_bytes, put_content_type)
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def handle_versioned_request(self, req, versions_cont, api_version,
|
def handle_get_head_with_version_id(self, req, version):
|
||||||
account, container, obj, is_enabled, version):
|
# Re-write the path; most everything else goes through normally
|
||||||
|
req.path_info = "/%s/%s/%s/%s" % (
|
||||||
|
self.api_version, self.account, self.versions_cont,
|
||||||
|
build_versions_object_name(self.obj, version))
|
||||||
|
req.headers['X-Backend-Allow-Reserved-Names'] = 'true'
|
||||||
|
|
||||||
|
resp = req.get_response(self.app)
|
||||||
|
if resp.is_success:
|
||||||
|
resp.headers['X-Object-Version-Id'] = version
|
||||||
|
|
||||||
|
# Well, except for some delete marker business...
|
||||||
|
is_del_marker = DELETE_MARKER_CONTENT_TYPE == resp.headers.get(
|
||||||
|
'X-Backend-Content-Type', resp.headers['Content-Type'])
|
||||||
|
|
||||||
|
if req.method == 'HEAD':
|
||||||
|
drain_and_close(resp)
|
||||||
|
|
||||||
|
if is_del_marker:
|
||||||
|
hdrs = {'X-Object-Version-Id': version,
|
||||||
|
'Content-Type': DELETE_MARKER_CONTENT_TYPE}
|
||||||
|
raise HTTPNotFound(request=req, headers=hdrs)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def hande_get_head_with_null_version_id(self, req):
|
||||||
|
resp = req.get_response(self.app)
|
||||||
|
location = wsgi_unquote(resp.headers.get('Content-Location', ''))
|
||||||
|
is_version_link = get_reserved_name('versions', '') in location
|
||||||
|
if resp.is_success and not is_version_link:
|
||||||
|
# this is the de-facto null version
|
||||||
|
resp.headers['X-Object-Version-Id'] = 'null'
|
||||||
|
if req.method == 'HEAD':
|
||||||
|
drain_and_close(resp)
|
||||||
|
return resp
|
||||||
|
elif is_version_link:
|
||||||
|
# Have a latest version, but it's got a real version-id.
|
||||||
|
# Since the user specifically asked for null, return 404
|
||||||
|
close_if_possible(resp.app_iter)
|
||||||
|
raise HTTPNotFound(request=req)
|
||||||
|
else:
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def handle_request_with_version_id(self, req, version):
|
||||||
"""
|
"""
|
||||||
Handle 'version-id' request for object resource. When a request
|
Handle 'version-id' request for object resource. When a request
|
||||||
contains a ``version-id=<id>`` parameter, the request is acted upon
|
contains a ``version-id=<id>`` parameter, the request is acted upon
|
||||||
@@ -808,12 +851,6 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
the contents of the versioned object.
|
the contents of the versioned object.
|
||||||
|
|
||||||
:param req: The original request
|
:param req: The original request
|
||||||
:param versions_cont: container holding versions of the requested obj
|
|
||||||
:param api_version: should be v1 unless swift bumps api version
|
|
||||||
:param account: account name string
|
|
||||||
:param container: container name string
|
|
||||||
:param object: object name string
|
|
||||||
:param is_enabled: is versioning currently enabled
|
|
||||||
:param version: version of the object to act on
|
:param version: version of the object to act on
|
||||||
"""
|
"""
|
||||||
# ?version-id requests are allowed for GET, HEAD, DELETE reqs
|
# ?version-id requests are allowed for GET, HEAD, DELETE reqs
|
||||||
@@ -821,7 +858,7 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
raise HTTPBadRequest(
|
raise HTTPBadRequest(
|
||||||
'%s to a specific version is not allowed' % req.method,
|
'%s to a specific version is not allowed' % req.method,
|
||||||
request=req)
|
request=req)
|
||||||
elif not versions_cont and version != 'null':
|
elif not self.versions_cont and version != 'null':
|
||||||
raise HTTPBadRequest(
|
raise HTTPBadRequest(
|
||||||
'version-aware operations require that the container is '
|
'version-aware operations require that the container is '
|
||||||
'versioned', request=req)
|
'versioned', request=req)
|
||||||
@@ -829,65 +866,33 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
try:
|
try:
|
||||||
validate_version(version)
|
validate_version(version)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPBadRequest('Invalid version parameter', request=req)
|
raise HTTPBadRequest('Invalid version parameter',
|
||||||
|
request=req)
|
||||||
|
|
||||||
if req.method == 'DELETE':
|
if req.method == 'DELETE':
|
||||||
return self.handle_delete_version(
|
return self.handle_delete_with_version_id(req, version)
|
||||||
req, versions_cont, api_version, account,
|
|
||||||
container, obj, is_enabled, version)
|
|
||||||
elif req.method == 'PUT':
|
elif req.method == 'PUT':
|
||||||
return self.handle_put_version(
|
return self.handle_put_with_version_id(req, version)
|
||||||
req, versions_cont, api_version, account,
|
|
||||||
container, obj, is_enabled, version)
|
|
||||||
if version == 'null':
|
if version == 'null':
|
||||||
resp = req.get_response(self.app)
|
# try the user namespace container first...
|
||||||
if resp.is_success:
|
resp = self.hande_get_head_with_null_version_id(req)
|
||||||
if get_reserved_name('versions', '') in wsgi_unquote(
|
if resp:
|
||||||
resp.headers.get('Content-Location', '')):
|
return resp
|
||||||
# Have a latest version, but it's got a real version-id.
|
return self.handle_get_head_with_version_id(req, version)
|
||||||
# Since the user specifically asked for null, return 404
|
|
||||||
close_if_possible(resp.app_iter)
|
|
||||||
raise HTTPNotFound(request=req)
|
|
||||||
resp.headers['X-Object-Version-Id'] = 'null'
|
|
||||||
if req.method == 'HEAD':
|
|
||||||
drain_and_close(resp)
|
|
||||||
return resp
|
|
||||||
else:
|
|
||||||
# Re-write the path; most everything else goes through normally
|
|
||||||
req.path_info = "/%s/%s/%s/%s" % (
|
|
||||||
api_version, account, versions_cont,
|
|
||||||
build_versions_object_name(obj, version))
|
|
||||||
req.headers['X-Backend-Allow-Reserved-Names'] = 'true'
|
|
||||||
|
|
||||||
resp = req.get_response(self.app)
|
def handle_request_without_version_id(self, req):
|
||||||
if resp.is_success:
|
"""
|
||||||
resp.headers['X-Object-Version-Id'] = version
|
Handle request for an object resource that may require a new version to
|
||||||
|
be created.
|
||||||
|
|
||||||
# Well, except for some delete marker business...
|
:param req: original request.
|
||||||
is_del_marker = DELETE_MARKER_CONTENT_TYPE == resp.headers.get(
|
"""
|
||||||
'X-Backend-Content-Type', resp.headers['Content-Type'])
|
|
||||||
|
|
||||||
if req.method == 'HEAD':
|
|
||||||
drain_and_close(resp)
|
|
||||||
|
|
||||||
if is_del_marker:
|
|
||||||
hdrs = {'X-Object-Version-Id': version,
|
|
||||||
'Content-Type': DELETE_MARKER_CONTENT_TYPE}
|
|
||||||
raise HTTPNotFound(request=req, headers=hdrs)
|
|
||||||
return resp
|
|
||||||
|
|
||||||
def handle_request(self, req, versions_cont, api_version, account,
|
|
||||||
container, obj, is_enabled):
|
|
||||||
if req.method == 'PUT':
|
if req.method == 'PUT':
|
||||||
return self.handle_put(
|
return self.handle_put(req)
|
||||||
req, versions_cont, api_version, account, obj,
|
|
||||||
is_enabled)
|
|
||||||
elif req.method == 'POST':
|
elif req.method == 'POST':
|
||||||
return self.handle_post(req, versions_cont, account)
|
return self.handle_post(req)
|
||||||
elif req.method == 'DELETE':
|
elif req.method == 'DELETE':
|
||||||
return self.handle_delete(
|
return self.handle_delete(req)
|
||||||
req, versions_cont, api_version, account,
|
|
||||||
container, obj, is_enabled)
|
|
||||||
|
|
||||||
# GET/HEAD/OPTIONS
|
# GET/HEAD/OPTIONS
|
||||||
resp = req.get_response(self.app)
|
resp = req.get_response(self.app)
|
||||||
@@ -897,26 +902,42 @@ class ObjectContext(ObjectVersioningContext):
|
|||||||
loc = wsgi_unquote(resp.headers.get('Content-Location', ''))
|
loc = wsgi_unquote(resp.headers.get('Content-Location', ''))
|
||||||
if loc:
|
if loc:
|
||||||
_, acct, cont, version_obj = split_path(loc, 4, 4, True)
|
_, acct, cont, version_obj = split_path(loc, 4, 4, True)
|
||||||
if acct == account and cont == versions_cont:
|
if acct == self.account and cont == self.versions_cont:
|
||||||
_, version = parse_versions_object_name(version_obj)
|
_, version = parse_versions_object_name(version_obj)
|
||||||
if version is not None:
|
if version is not None:
|
||||||
resp.headers['X-Object-Version-Id'] = version
|
resp.headers['X-Object-Version-Id'] = version
|
||||||
content_loc = wsgi_quote('/%s/%s/%s/%s' % (
|
content_loc = wsgi_quote('/%s/%s/%s/%s' % (
|
||||||
api_version, account, container, obj,
|
self.api_version, self.account, self.container,
|
||||||
)) + '?version-id=%s' % (version,)
|
self.obj)) + '?version-id=%s' % (version,)
|
||||||
resp.headers['Content-Location'] = content_loc
|
resp.headers['Content-Location'] = content_loc
|
||||||
symlink_target = wsgi_unquote(resp.headers.get('X-Symlink-Target', ''))
|
symlink_target = wsgi_unquote(resp.headers.get('X-Symlink-Target', ''))
|
||||||
if symlink_target:
|
if symlink_target:
|
||||||
cont, version_obj = split_path('/%s' % symlink_target, 2, 2, True)
|
cont, version_obj = split_path('/%s' % symlink_target, 2, 2, True)
|
||||||
if cont == versions_cont:
|
if cont == self.versions_cont:
|
||||||
_, version = parse_versions_object_name(version_obj)
|
_, version = parse_versions_object_name(version_obj)
|
||||||
if version is not None:
|
if version is not None:
|
||||||
resp.headers['X-Object-Version-Id'] = version
|
resp.headers['X-Object-Version-Id'] = version
|
||||||
symlink_target = wsgi_quote('%s/%s' % (container, obj)) + \
|
symlink_target = wsgi_quote(
|
||||||
|
'%s/%s' % (self.container, self.obj)) + \
|
||||||
'?version-id=%s' % (version,)
|
'?version-id=%s' % (version,)
|
||||||
resp.headers['X-Symlink-Target'] = symlink_target
|
resp.headers['X-Symlink-Target'] = symlink_target
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
def handle_request(self, req):
|
||||||
|
"""
|
||||||
|
Handle request for an object resource.
|
||||||
|
|
||||||
|
:param req: swift.common.swob.Request instance
|
||||||
|
"""
|
||||||
|
version_id = req.params.get('version-id')
|
||||||
|
if version_id:
|
||||||
|
return self.handle_request_with_version_id(req, version_id)
|
||||||
|
elif self.versions_cont:
|
||||||
|
# handle object request for a versioned container
|
||||||
|
return self.handle_request_without_version_id(req)
|
||||||
|
else:
|
||||||
|
return self.app
|
||||||
|
|
||||||
|
|
||||||
class ContainerContext(ObjectVersioningContext):
|
class ContainerContext(ObjectVersioningContext):
|
||||||
def handle_request(self, req, start_response):
|
def handle_request(self, req, start_response):
|
||||||
@@ -1503,35 +1524,19 @@ class ObjectVersioningMiddleware(object):
|
|||||||
:param container: container name string
|
:param container: container name string
|
||||||
:param object: object name string
|
:param object: object name string
|
||||||
"""
|
"""
|
||||||
resp = None
|
|
||||||
container_info = get_container_info(
|
container_info = get_container_info(
|
||||||
req.environ, self.app, swift_source='OV')
|
req.environ, self.app, swift_source='OV')
|
||||||
|
|
||||||
versions_cont = container_info.get(
|
versions_cont = container_info.get(
|
||||||
'sysmeta', {}).get('versions-container', '')
|
'sysmeta', {}).get('versions-container', '')
|
||||||
is_enabled = config_true_value(container_info.get(
|
|
||||||
'sysmeta', {}).get('versions-enabled'))
|
|
||||||
|
|
||||||
if versions_cont:
|
if versions_cont:
|
||||||
versions_cont = wsgi_unquote(str_to_wsgi(
|
versions_cont = wsgi_unquote(str_to_wsgi(
|
||||||
versions_cont)).split('/')[0]
|
versions_cont)).split('/')[0]
|
||||||
|
is_enabled = is_versioning_enabled(container_info)
|
||||||
if req.params.get('version-id'):
|
object_ctx = ObjectContext(
|
||||||
vw_ctx = ObjectContext(self.app, self.logger)
|
self.app, self.logger, api_version, account, container, obj,
|
||||||
resp = vw_ctx.handle_versioned_request(
|
versions_cont, is_enabled)
|
||||||
req, versions_cont, api_version, account, container, obj,
|
return object_ctx.handle_request(req)
|
||||||
is_enabled, req.params['version-id'])
|
|
||||||
elif versions_cont:
|
|
||||||
# handle object request for a enabled versioned container
|
|
||||||
vw_ctx = ObjectContext(self.app, self.logger)
|
|
||||||
resp = vw_ctx.handle_request(
|
|
||||||
req, versions_cont, api_version, account, container, obj,
|
|
||||||
is_enabled)
|
|
||||||
|
|
||||||
if resp:
|
|
||||||
return resp
|
|
||||||
else:
|
|
||||||
return self.app
|
|
||||||
|
|
||||||
def __call__(self, env, start_response):
|
def __call__(self, env, start_response):
|
||||||
req = Request(env)
|
req = Request(env)
|
||||||
|
|||||||
@@ -3621,7 +3621,8 @@ class TestModuleFunctions(unittest.TestCase):
|
|||||||
class TestObjectContext(unittest.TestCase):
|
class TestObjectContext(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
app = FakeSwift()
|
app = FakeSwift()
|
||||||
self.obj_context = object_versioning.ObjectContext(app, app.logger)
|
self.obj_context = object_versioning.ObjectContext(
|
||||||
|
app, app.logger, 'v1', 'c', 'a', 'o', None, False)
|
||||||
self.ts_iter = make_timestamp_iter()
|
self.ts_iter = make_timestamp_iter()
|
||||||
|
|
||||||
def test_get_version(self):
|
def test_get_version(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user