Merge "Allow "static symlinks""
This commit is contained in:
commit
fefa888c4b
@ -30,12 +30,20 @@ symlink, the header ``X-Symlink-Target-Account: <account>`` must be included.
|
|||||||
If omitted, it is inserted automatically with the account of the symlink
|
If omitted, it is inserted automatically with the account of the symlink
|
||||||
object in the PUT request process.
|
object in the PUT request process.
|
||||||
|
|
||||||
Symlinks must be zero-byte objects. Attempting to PUT a symlink
|
Symlinks must be zero-byte objects. Attempting to PUT a symlink with a
|
||||||
with a non-empty request body will result in a 400-series error. Also, POST
|
non-empty request body will result in a 400-series error. Also, POST with
|
||||||
with X-Symlink-Target header always results in a 400-series error. The target
|
``X-Symlink-Target`` header always results in a 400-series error. The target
|
||||||
object need not exist at symlink creation time. It is suggested to set the
|
object need not exist at symlink creation time.
|
||||||
``Content-Type`` of symlink objects to a distinct value such as
|
|
||||||
``application/symlink``.
|
Clients may optionally include a ``X-Symlink-Target-Etag: <etag>`` header
|
||||||
|
during the PUT. If present, this will create a "static symlink" instead of a
|
||||||
|
"dynamic symlink". Static symlinks point to a specific object rather than a
|
||||||
|
specific name. They do this by using the value set in their
|
||||||
|
``X-Symlink-Target-Etag`` header when created to verify it still matches the
|
||||||
|
ETag of the object they're pointing at on a GET. In contrast to a dynamic
|
||||||
|
symlink the target object referenced in the ``X-Symlink-Target`` header must
|
||||||
|
exist and its ETag must match the ``X-Symlink-Target-Etag`` or the symlink
|
||||||
|
creation will return a client error.
|
||||||
|
|
||||||
A GET/HEAD request to a symlink will result in a request to the target
|
A GET/HEAD request to a symlink will result in a request to the target
|
||||||
object referenced by the symlink's ``X-Symlink-Target-Account`` and
|
object referenced by the symlink's ``X-Symlink-Target-Account`` and
|
||||||
@ -45,12 +53,22 @@ GET/HEAD request to a symlink with the query parameter ``?symlink=get`` will
|
|||||||
result in the request targeting the symlink itself.
|
result in the request targeting the symlink itself.
|
||||||
|
|
||||||
A symlink can point to another symlink. Chained symlinks will be traversed
|
A symlink can point to another symlink. Chained symlinks will be traversed
|
||||||
until target is not a symlink. If the number of chained symlinks exceeds the
|
until the target is not a symlink. If the number of chained symlinks exceeds
|
||||||
limit ``symloop_max`` an error response will be produced. The value of
|
the limit ``symloop_max`` an error response will be produced. The value of
|
||||||
``symloop_max`` can be defined in the symlink config section of
|
``symloop_max`` can be defined in the symlink config section of
|
||||||
`proxy-server.conf`. If not specified, the default ``symloop_max`` value is 2.
|
`proxy-server.conf`. If not specified, the default ``symloop_max`` value is 2.
|
||||||
If a value less than 1 is specified, the default value will be used.
|
If a value less than 1 is specified, the default value will be used.
|
||||||
|
|
||||||
|
If a static symlink (i.e. a symlink created with a ``X-Symlink-Target-Etag``
|
||||||
|
header) targets another static symlink, both of the ``X-Symlink-Target-Etag``
|
||||||
|
headers must match the target object for the GET to succeed. If a static
|
||||||
|
symlink targets a dynamic symlink (i.e. a symlink created without a
|
||||||
|
``X-Symlink-Target-Etag`` header) then the ``X-Symlink-Target-Etag`` header of
|
||||||
|
the static symlink must be the Etag of the zero-byte object. If a symlink with
|
||||||
|
a ``X-Symlink-Target-Etag`` targets a large object manifest it must match the
|
||||||
|
ETag of the manifest (e.g. the ETag as returned by ``multipart-manifest=get``
|
||||||
|
or value in the ``X-Manifest-Etag`` header).
|
||||||
|
|
||||||
A HEAD/GET request to a symlink object behaves as a normal HEAD/GET request
|
A HEAD/GET request to a symlink object behaves as a normal HEAD/GET request
|
||||||
to the target object. Therefore issuing a HEAD request to the symlink will
|
to the target object. Therefore issuing a HEAD request to the symlink will
|
||||||
return the target metadata, and issuing a GET request to the symlink will
|
return the target metadata, and issuing a GET request to the symlink will
|
||||||
@ -58,13 +76,22 @@ return the data and metadata of the target object. To return the symlink
|
|||||||
metadata (with its empty body) a GET/HEAD request with the ``?symlink=get``
|
metadata (with its empty body) a GET/HEAD request with the ``?symlink=get``
|
||||||
query parameter must be sent to a symlink object.
|
query parameter must be sent to a symlink object.
|
||||||
|
|
||||||
A POST request to a symlink will result in a 307 TemporaryRedirect response.
|
A POST request to a symlink will result in a 307 Temporary Redirect response.
|
||||||
The response will contain a ``Location`` header with the path of the target
|
The response will contain a ``Location`` header with the path of the target
|
||||||
object as the value. The request is never redirected to the target object by
|
object as the value. The request is never redirected to the target object by
|
||||||
Swift. Nevertheless, the metadata in the POST request will be applied to the
|
Swift. Nevertheless, the metadata in the POST request will be applied to the
|
||||||
symlink because object servers cannot know for sure if the current object is a
|
symlink because object servers cannot know for sure if the current object is a
|
||||||
symlink or not in eventual consistency.
|
symlink or not in eventual consistency.
|
||||||
|
|
||||||
|
A symlink's ``Content-Type`` is completely independent from its target. As a
|
||||||
|
convenience Swift will automatically set the ``Content-Type`` on a symlink PUT
|
||||||
|
if not explicitly set by the client. If the client sends a
|
||||||
|
``X-Symlink-Target-Etag`` Swift will set the symlink's ``Content-Type`` to that
|
||||||
|
of the target, otherwise it will be set to ``application/symlink``. You can
|
||||||
|
review a symlink's ``Content-Type`` using the ``?symlink=get`` interface. You
|
||||||
|
can change a symlink's ``Content-Type`` using a POST request. The symlink's
|
||||||
|
``Content-Type`` will appear in the container listing.
|
||||||
|
|
||||||
A DELETE request to a symlink will delete the symlink itself. The target
|
A DELETE request to a symlink will delete the symlink itself. The target
|
||||||
object will not be deleted.
|
object will not be deleted.
|
||||||
|
|
||||||
@ -73,7 +100,7 @@ will copy the target object. The same request to a symlink with the query
|
|||||||
parameter ``?symlink=get`` will copy the symlink itself.
|
parameter ``?symlink=get`` will copy the symlink itself.
|
||||||
|
|
||||||
An OPTIONS request to a symlink will respond with the options for the symlink
|
An OPTIONS request to a symlink will respond with the options for the symlink
|
||||||
only, the request will not be redirected to the target object. Please note that
|
only; the request will not be redirected to the target object. Please note that
|
||||||
if the symlink's target object is in another container with CORS settings, the
|
if the symlink's target object is in another container with CORS settings, the
|
||||||
response will not reflect the settings.
|
response will not reflect the settings.
|
||||||
|
|
||||||
@ -82,7 +109,8 @@ will result in a 400-series error. The GET/HEAD tempurls honor the scope of
|
|||||||
the tempurl key. Container tempurl will only work on symlinks where the target
|
the tempurl key. Container tempurl will only work on symlinks where the target
|
||||||
container is the same as the symlink. In case a symlink targets an object
|
container is the same as the symlink. In case a symlink targets an object
|
||||||
in a different container, a GET/HEAD request will result in a 401 Unauthorized
|
in a different container, a GET/HEAD request will result in a 401 Unauthorized
|
||||||
error. The account level tempurl will allow cross container symlinks.
|
error. The account level tempurl will allow cross-container symlinks, but not
|
||||||
|
cross-account symlinks.
|
||||||
|
|
||||||
If a symlink object is overwritten while it is in a versioned container, the
|
If a symlink object is overwritten while it is in a versioned container, the
|
||||||
symlink object itself is versioned, not the referenced object.
|
symlink object itself is versioned, not the referenced object.
|
||||||
@ -91,8 +119,19 @@ A GET request with query parameter ``?format=json`` to a container which
|
|||||||
contains symlinks will respond with additional information ``symlink_path``
|
contains symlinks will respond with additional information ``symlink_path``
|
||||||
for each symlink object in the container listing. The ``symlink_path`` value
|
for each symlink object in the container listing. The ``symlink_path`` value
|
||||||
is the target path of the symlink. Clients can differentiate symlinks and
|
is the target path of the symlink. Clients can differentiate symlinks and
|
||||||
other objects by this function. Note that responses of any other format
|
other objects by this function. Note that responses in any other format
|
||||||
(e.g.``?format=xml``) won't include ``symlink_path`` info.
|
(e.g. ``?format=xml``) won't include ``symlink_path`` info. If a
|
||||||
|
``X-Symlink-Target-Etag`` header was included on the symlink, JSON container
|
||||||
|
listings will include that value in a ``symlink_etag`` key and the target
|
||||||
|
object's ``Content-Length`` will be included in the key ``symlink_bytes``.
|
||||||
|
|
||||||
|
If a static symlink targets a static large object manifest it will carry
|
||||||
|
forward the SLO's size and slo_etag in the container listing using the
|
||||||
|
``symlink_bytes`` and ``slo_etag`` keys. However, manifests created before
|
||||||
|
swift v2.12.0 (released Dec 2016) do not contain enough metadata to propagate
|
||||||
|
the extra SLO information to the listing. Clients may recreate the manifest
|
||||||
|
(COPY w/ ``?multipart-manfiest=get``) before creating a static symlink to add
|
||||||
|
the requisite metadata.
|
||||||
|
|
||||||
Errors
|
Errors
|
||||||
|
|
||||||
@ -105,7 +144,10 @@ Errors
|
|||||||
* GET/HEAD traversing more than ``symloop_max`` chained symlinks will
|
* GET/HEAD traversing more than ``symloop_max`` chained symlinks will
|
||||||
produce a 409 Conflict error.
|
produce a 409 Conflict error.
|
||||||
|
|
||||||
* POSTs will produce a 307 TemporaryRedirect error.
|
* PUT/GET/HEAD on a symlink that inclues a ``X-Symlink-Target-Etag`` header
|
||||||
|
that does not match the target will poduce a 409 Conflict error.
|
||||||
|
|
||||||
|
* POSTs will produce a 307 Temporary Redirect error.
|
||||||
|
|
||||||
----------
|
----------
|
||||||
Deployment
|
Deployment
|
||||||
@ -160,7 +202,7 @@ import os
|
|||||||
from cgi import parse_header
|
from cgi import parse_header
|
||||||
|
|
||||||
from swift.common.utils import get_logger, register_swift_info, split_path, \
|
from swift.common.utils import get_logger, register_swift_info, split_path, \
|
||||||
MD5_OF_EMPTY_STRING, closing_if_possible
|
MD5_OF_EMPTY_STRING, close_if_possible, closing_if_possible
|
||||||
from swift.common.constraints import check_account_format
|
from swift.common.constraints import check_account_format
|
||||||
from swift.common.wsgi import WSGIContext, make_subrequest
|
from swift.common.wsgi import WSGIContext, make_subrequest
|
||||||
from swift.common.request_helpers import get_sys_meta_prefix, \
|
from swift.common.request_helpers import get_sys_meta_prefix, \
|
||||||
@ -168,7 +210,7 @@ from swift.common.request_helpers import get_sys_meta_prefix, \
|
|||||||
from swift.common.swob import Request, HTTPBadRequest, HTTPTemporaryRedirect, \
|
from swift.common.swob import Request, HTTPBadRequest, HTTPTemporaryRedirect, \
|
||||||
HTTPException, HTTPConflict, HTTPPreconditionFailed, wsgi_quote, \
|
HTTPException, HTTPConflict, HTTPPreconditionFailed, wsgi_quote, \
|
||||||
wsgi_unquote
|
wsgi_unquote
|
||||||
from swift.common.http import is_success
|
from swift.common.http import is_success, HTTP_NOT_FOUND
|
||||||
from swift.common.exceptions import LinkIterError
|
from swift.common.exceptions import LinkIterError
|
||||||
from swift.common.header_key_dict import HeaderKeyDict
|
from swift.common.header_key_dict import HeaderKeyDict
|
||||||
|
|
||||||
@ -176,22 +218,33 @@ DEFAULT_SYMLOOP_MAX = 2
|
|||||||
# Header values for symlink target path strings will be quoted values.
|
# Header values for symlink target path strings will be quoted values.
|
||||||
TGT_OBJ_SYMLINK_HDR = 'x-symlink-target'
|
TGT_OBJ_SYMLINK_HDR = 'x-symlink-target'
|
||||||
TGT_ACCT_SYMLINK_HDR = 'x-symlink-target-account'
|
TGT_ACCT_SYMLINK_HDR = 'x-symlink-target-account'
|
||||||
|
TGT_ETAG_SYMLINK_HDR = 'x-symlink-target-etag'
|
||||||
|
TGT_BYTES_SYMLINK_HDR = 'x-symlink-target-bytes'
|
||||||
TGT_OBJ_SYSMETA_SYMLINK_HDR = get_sys_meta_prefix('object') + 'symlink-target'
|
TGT_OBJ_SYSMETA_SYMLINK_HDR = get_sys_meta_prefix('object') + 'symlink-target'
|
||||||
TGT_ACCT_SYSMETA_SYMLINK_HDR = \
|
TGT_ACCT_SYSMETA_SYMLINK_HDR = \
|
||||||
get_sys_meta_prefix('object') + 'symlink-target-account'
|
get_sys_meta_prefix('object') + 'symlink-target-account'
|
||||||
|
TGT_ETAG_SYSMETA_SYMLINK_HDR = \
|
||||||
|
get_sys_meta_prefix('object') + 'symlink-target-etag'
|
||||||
|
TGT_BYTES_SYSMETA_SYMLINK_HDR = \
|
||||||
|
get_sys_meta_prefix('object') + 'symlink-target-bytes'
|
||||||
|
|
||||||
|
|
||||||
def _check_symlink_header(req):
|
def _check_symlink_header(req):
|
||||||
"""
|
"""
|
||||||
Validate that the value from x-symlink-target header is
|
Validate that the value from x-symlink-target header is well formatted
|
||||||
well formatted. We assume the caller ensures that
|
and that the x-symlink-target-etag header (if present) does not contain
|
||||||
|
problematic characters. We assume the caller ensures that
|
||||||
x-symlink-target header is present in req.headers.
|
x-symlink-target header is present in req.headers.
|
||||||
|
|
||||||
:param req: HTTP request object
|
:param req: HTTP request object
|
||||||
|
:returns: a tuple, the full versioned WSGI quoted path to the object and
|
||||||
|
the value of the X-Symlink-Target-Etag header which may be None
|
||||||
:raise: HTTPPreconditionFailed if x-symlink-target value
|
:raise: HTTPPreconditionFailed if x-symlink-target value
|
||||||
is not well formatted.
|
is not well formatted.
|
||||||
:raise: HTTPBadRequest if the x-symlink-target value points to the request
|
:raise: HTTPBadRequest if the x-symlink-target value points to the request
|
||||||
path.
|
path.
|
||||||
|
:raise: HTTPBadRequest if the x-symlink-target-etag value contains
|
||||||
|
a semicolon, double-quote, or backslash.
|
||||||
"""
|
"""
|
||||||
# N.B. check_path_header doesn't assert the leading slash and
|
# N.B. check_path_header doesn't assert the leading slash and
|
||||||
# copy middleware may accept the format. In the symlink, API
|
# copy middleware may accept the format. In the symlink, API
|
||||||
@ -228,43 +281,48 @@ def _check_symlink_header(req):
|
|||||||
raise HTTPBadRequest(
|
raise HTTPBadRequest(
|
||||||
body='Symlink cannot target itself',
|
body='Symlink cannot target itself',
|
||||||
request=req, content_type='text/plain')
|
request=req, content_type='text/plain')
|
||||||
|
etag = req.headers.get(TGT_ETAG_SYMLINK_HDR, None)
|
||||||
|
if etag and any(c in etag for c in ';"\\'):
|
||||||
|
# See cgi.parse_header for why the above chars are problematic
|
||||||
|
raise HTTPBadRequest(
|
||||||
|
body='Bad %s format' % TGT_ETAG_SYMLINK_HDR.title(),
|
||||||
|
request=req, content_type='text/plain')
|
||||||
|
if not (etag or req.headers.get('Content-Type')):
|
||||||
|
req.headers['Content-Type'] = 'application/symlink'
|
||||||
|
return '/v1/%s/%s/%s' % (account, container, obj), etag
|
||||||
|
|
||||||
|
|
||||||
def symlink_usermeta_to_sysmeta(headers):
|
def symlink_usermeta_to_sysmeta(headers):
|
||||||
"""
|
"""
|
||||||
Helper function to translate from X-Symlink-Target and
|
Helper function to translate from client-facing X-Symlink-* headers
|
||||||
X-Symlink-Target-Account to X-Object-Sysmeta-Symlink-Target
|
to cluster-facing X-Object-Sysmeta-Symlink-* headers.
|
||||||
and X-Object-Sysmeta-Symlink-Target-Account.
|
|
||||||
|
|
||||||
:param headers: request headers dict. Note that the headers dict
|
:param headers: request headers dict. Note that the headers dict
|
||||||
will be updated directly.
|
will be updated directly.
|
||||||
"""
|
"""
|
||||||
# To preseve url-encoded value in the symlink header, use raw value
|
# To preseve url-encoded value in the symlink header, use raw value
|
||||||
if TGT_OBJ_SYMLINK_HDR in headers:
|
for user_hdr, sysmeta_hdr in (
|
||||||
headers[TGT_OBJ_SYSMETA_SYMLINK_HDR] = headers.pop(
|
(TGT_OBJ_SYMLINK_HDR, TGT_OBJ_SYSMETA_SYMLINK_HDR),
|
||||||
TGT_OBJ_SYMLINK_HDR)
|
(TGT_ACCT_SYMLINK_HDR, TGT_ACCT_SYSMETA_SYMLINK_HDR)):
|
||||||
|
if user_hdr in headers:
|
||||||
if TGT_ACCT_SYMLINK_HDR in headers:
|
headers[sysmeta_hdr] = headers.pop(user_hdr)
|
||||||
headers[TGT_ACCT_SYSMETA_SYMLINK_HDR] = headers.pop(
|
|
||||||
TGT_ACCT_SYMLINK_HDR)
|
|
||||||
|
|
||||||
|
|
||||||
def symlink_sysmeta_to_usermeta(headers):
|
def symlink_sysmeta_to_usermeta(headers):
|
||||||
"""
|
"""
|
||||||
Helper function to translate from X-Object-Sysmeta-Symlink-Target and
|
Helper function to translate from cluster-facing
|
||||||
X-Object-Sysmeta-Symlink-Target-Account to X-Symlink-Target and
|
X-Object-Sysmeta-Symlink-* headers to client-facing X-Symlink-* headers.
|
||||||
X-Sysmeta-Symlink-Target-Account
|
|
||||||
|
|
||||||
:param headers: request headers dict. Note that the headers dict
|
:param headers: request headers dict. Note that the headers dict
|
||||||
will be updated directly.
|
will be updated directly.
|
||||||
"""
|
"""
|
||||||
if TGT_OBJ_SYSMETA_SYMLINK_HDR in headers:
|
for user_hdr, sysmeta_hdr in (
|
||||||
headers[TGT_OBJ_SYMLINK_HDR] = headers.pop(
|
(TGT_OBJ_SYMLINK_HDR, TGT_OBJ_SYSMETA_SYMLINK_HDR),
|
||||||
TGT_OBJ_SYSMETA_SYMLINK_HDR)
|
(TGT_ACCT_SYMLINK_HDR, TGT_ACCT_SYSMETA_SYMLINK_HDR),
|
||||||
|
(TGT_ETAG_SYMLINK_HDR, TGT_ETAG_SYSMETA_SYMLINK_HDR),
|
||||||
if TGT_ACCT_SYSMETA_SYMLINK_HDR in headers:
|
(TGT_BYTES_SYMLINK_HDR, TGT_BYTES_SYSMETA_SYMLINK_HDR)):
|
||||||
headers[TGT_ACCT_SYMLINK_HDR] = headers.pop(
|
if sysmeta_hdr in headers:
|
||||||
TGT_ACCT_SYSMETA_SYMLINK_HDR)
|
headers[user_hdr] = headers.pop(sysmeta_hdr)
|
||||||
|
|
||||||
|
|
||||||
class SymlinkContainerContext(WSGIContext):
|
class SymlinkContainerContext(WSGIContext):
|
||||||
@ -308,9 +366,10 @@ class SymlinkContainerContext(WSGIContext):
|
|||||||
|
|
||||||
def _extract_symlink_path_json(self, obj_dict, swift_version, account):
|
def _extract_symlink_path_json(self, obj_dict, swift_version, account):
|
||||||
"""
|
"""
|
||||||
Extract the symlink path from the hash value
|
Extract the symlink info from the hash value
|
||||||
:return: object dictionary with additional key:value pair if object
|
:return: object dictionary with additional key:value pairs when object
|
||||||
is a symlink. The new key is symlink_path.
|
is a symlink. i.e. new symlink_path, symlink_etag and
|
||||||
|
symlink_bytes keys
|
||||||
"""
|
"""
|
||||||
if 'hash' in obj_dict:
|
if 'hash' in obj_dict:
|
||||||
hash_value, meta = parse_header(obj_dict['hash'])
|
hash_value, meta = parse_header(obj_dict['hash'])
|
||||||
@ -321,6 +380,10 @@ class SymlinkContainerContext(WSGIContext):
|
|||||||
target = meta[key]
|
target = meta[key]
|
||||||
elif key == 'symlink_target_account':
|
elif key == 'symlink_target_account':
|
||||||
account = meta[key]
|
account = meta[key]
|
||||||
|
elif key == 'symlink_target_etag':
|
||||||
|
obj_dict['symlink_etag'] = meta[key]
|
||||||
|
elif key == 'symlink_target_bytes':
|
||||||
|
obj_dict['symlink_bytes'] = int(meta[key])
|
||||||
else:
|
else:
|
||||||
# make sure to add all other (key, values) back in place
|
# make sure to add all other (key, values) back in place
|
||||||
obj_dict['hash'] += '; %s=%s' % (key, meta[key])
|
obj_dict['hash'] += '; %s=%s' % (key, meta[key])
|
||||||
@ -370,10 +433,11 @@ class SymlinkObjectContext(WSGIContext):
|
|||||||
except LinkIterError:
|
except LinkIterError:
|
||||||
errmsg = 'Too many levels of symbolic links, ' \
|
errmsg = 'Too many levels of symbolic links, ' \
|
||||||
'maximum allowed is %d' % self.symloop_max
|
'maximum allowed is %d' % self.symloop_max
|
||||||
raise HTTPConflict(
|
raise HTTPConflict(body=errmsg, request=req,
|
||||||
body=errmsg, request=req, content_type='text/plain')
|
content_type='text/plain')
|
||||||
|
|
||||||
def _recursive_get_head(self, req):
|
def _recursive_get_head(self, req, target_etag=None,
|
||||||
|
follow_softlinks=True):
|
||||||
resp = self._app_call(req.environ)
|
resp = self._app_call(req.environ)
|
||||||
|
|
||||||
def build_traversal_req(symlink_target):
|
def build_traversal_req(symlink_target):
|
||||||
@ -396,14 +460,35 @@ class SymlinkObjectContext(WSGIContext):
|
|||||||
|
|
||||||
symlink_target = self._response_header_value(
|
symlink_target = self._response_header_value(
|
||||||
TGT_OBJ_SYSMETA_SYMLINK_HDR)
|
TGT_OBJ_SYSMETA_SYMLINK_HDR)
|
||||||
if symlink_target:
|
resp_etag = self._response_header_value(
|
||||||
|
TGT_ETAG_SYSMETA_SYMLINK_HDR)
|
||||||
|
if symlink_target and (resp_etag or follow_softlinks):
|
||||||
|
close_if_possible(resp)
|
||||||
|
found_etag = resp_etag or self._response_header_value('etag')
|
||||||
|
if target_etag and target_etag != found_etag:
|
||||||
|
raise HTTPConflict(
|
||||||
|
body='X-Symlink-Target-Etag headers do not match',
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
'Content-Location': self._last_target_path})
|
||||||
if self._loop_count >= self.symloop_max:
|
if self._loop_count >= self.symloop_max:
|
||||||
raise LinkIterError()
|
raise LinkIterError()
|
||||||
# format: /<account name>/<container name>/<object name>
|
# format: /<account name>/<container name>/<object name>
|
||||||
new_req = build_traversal_req(symlink_target)
|
new_req = build_traversal_req(symlink_target)
|
||||||
self._loop_count += 1
|
self._loop_count += 1
|
||||||
return self._recursive_get_head(new_req)
|
return self._recursive_get_head(new_req, target_etag=resp_etag)
|
||||||
else:
|
else:
|
||||||
|
final_etag = self._response_header_value('etag')
|
||||||
|
if final_etag and target_etag and target_etag != final_etag:
|
||||||
|
close_if_possible(resp)
|
||||||
|
body = ('Object Etag %r does not match '
|
||||||
|
'X-Symlink-Target-Etag header %r')
|
||||||
|
raise HTTPConflict(
|
||||||
|
body=body % (final_etag, target_etag),
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
'Content-Location': self._last_target_path})
|
||||||
|
|
||||||
if self._last_target_path:
|
if self._last_target_path:
|
||||||
# Content-Location will be applied only when one or more
|
# Content-Location will be applied only when one or more
|
||||||
# symlink recursion occurred.
|
# symlink recursion occurred.
|
||||||
@ -417,6 +502,47 @@ class SymlinkObjectContext(WSGIContext):
|
|||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
def _validate_etag_and_update_sysmeta(self, req, symlink_target_path,
|
||||||
|
etag):
|
||||||
|
# next we'll make sure the E-Tag matches a real object
|
||||||
|
new_req = make_subrequest(
|
||||||
|
req.environ, path=wsgi_quote(symlink_target_path), method='HEAD',
|
||||||
|
swift_source='SYM')
|
||||||
|
self._last_target_path = symlink_target_path
|
||||||
|
resp = self._recursive_get_head(new_req, target_etag=etag,
|
||||||
|
follow_softlinks=False)
|
||||||
|
if self._get_status_int() == HTTP_NOT_FOUND:
|
||||||
|
raise HTTPConflict(
|
||||||
|
body='X-Symlink-Target does not exist',
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
'Content-Location': self._last_target_path})
|
||||||
|
if not is_success(self._get_status_int()):
|
||||||
|
return resp
|
||||||
|
response_headers = HeaderKeyDict(self._response_headers)
|
||||||
|
# carry forward any etag update params (e.g. "slo_etag"), we'll append
|
||||||
|
# symlink_target_* params to this header after this method returns
|
||||||
|
override_header = get_container_update_override_key('etag')
|
||||||
|
if override_header in response_headers and \
|
||||||
|
override_header not in req.headers:
|
||||||
|
sep, params = response_headers[override_header].partition(';')[1:]
|
||||||
|
req.headers[override_header] = MD5_OF_EMPTY_STRING + sep + params
|
||||||
|
|
||||||
|
# It's troublesome that there's so much leakage with SLO
|
||||||
|
if 'X-Object-Sysmeta-Slo-Etag' in response_headers and \
|
||||||
|
override_header not in req.headers:
|
||||||
|
req.headers[override_header] = '%s; slo_etag=%s' % (
|
||||||
|
MD5_OF_EMPTY_STRING,
|
||||||
|
response_headers['X-Object-Sysmeta-Slo-Etag'])
|
||||||
|
req.headers[TGT_BYTES_SYSMETA_SYMLINK_HDR] = (
|
||||||
|
response_headers.get('x-object-sysmeta-slo-size') or
|
||||||
|
response_headers['Content-Length'])
|
||||||
|
|
||||||
|
req.headers[TGT_ETAG_SYSMETA_SYMLINK_HDR] = etag
|
||||||
|
|
||||||
|
if not req.headers.get('Content-Type'):
|
||||||
|
req.headers['Content-Type'] = response_headers['Content-Type']
|
||||||
|
|
||||||
def handle_put(self, req):
|
def handle_put(self, req):
|
||||||
"""
|
"""
|
||||||
Handle put request when it contains X-Symlink-Target header.
|
Handle put request when it contains X-Symlink-Target header.
|
||||||
@ -435,7 +561,13 @@ class SymlinkObjectContext(WSGIContext):
|
|||||||
request=req,
|
request=req,
|
||||||
content_type='text/plain')
|
content_type='text/plain')
|
||||||
|
|
||||||
_check_symlink_header(req)
|
symlink_target_path, etag = _check_symlink_header(req)
|
||||||
|
if etag:
|
||||||
|
resp = self._validate_etag_and_update_sysmeta(
|
||||||
|
req, symlink_target_path, etag)
|
||||||
|
if resp is not None:
|
||||||
|
return resp
|
||||||
|
# N.B. TGT_ETAG_SYMLINK_HDR was converted as part of verifying it
|
||||||
symlink_usermeta_to_sysmeta(req.headers)
|
symlink_usermeta_to_sysmeta(req.headers)
|
||||||
# Store info in container update that this object is a symlink.
|
# Store info in container update that this object is a symlink.
|
||||||
# We have a design decision to use etag space to store symlink info for
|
# We have a design decision to use etag space to store symlink info for
|
||||||
@ -445,16 +577,30 @@ class SymlinkObjectContext(WSGIContext):
|
|||||||
# listing result for clients.
|
# listing result for clients.
|
||||||
# To create override etag easily, we have a constraint that the symlink
|
# To create override etag easily, we have a constraint that the symlink
|
||||||
# must be 0 byte so we can add etag of the empty string + symlink info
|
# must be 0 byte so we can add etag of the empty string + symlink info
|
||||||
# here, simply. Note that this override etag may be encrypted in the
|
# here, simply (if no other override etag was provided). Note that this
|
||||||
# container db by encryption middleware.
|
# override etag may be encrypted in the container db by encryption
|
||||||
|
# middleware.
|
||||||
|
|
||||||
etag_override = [
|
etag_override = [
|
||||||
MD5_OF_EMPTY_STRING,
|
req.headers.get(get_container_update_override_key('etag'),
|
||||||
|
MD5_OF_EMPTY_STRING),
|
||||||
'symlink_target=%s' % req.headers[TGT_OBJ_SYSMETA_SYMLINK_HDR]
|
'symlink_target=%s' % req.headers[TGT_OBJ_SYSMETA_SYMLINK_HDR]
|
||||||
]
|
]
|
||||||
if TGT_ACCT_SYSMETA_SYMLINK_HDR in req.headers:
|
if TGT_ACCT_SYSMETA_SYMLINK_HDR in req.headers:
|
||||||
etag_override.append(
|
etag_override.append(
|
||||||
'symlink_target_account=%s' %
|
'symlink_target_account=%s' %
|
||||||
req.headers[TGT_ACCT_SYSMETA_SYMLINK_HDR])
|
req.headers[TGT_ACCT_SYSMETA_SYMLINK_HDR])
|
||||||
|
if TGT_ETAG_SYSMETA_SYMLINK_HDR in req.headers:
|
||||||
|
# if _validate_etag_and_update_sysmeta or a middleware sets
|
||||||
|
# TGT_ETAG_SYSMETA_SYMLINK_HDR then they need to also set
|
||||||
|
# TGT_BYTES_SYSMETA_SYMLINK_HDR. If they forget, they get a
|
||||||
|
# KeyError traceback and client gets a ServerError
|
||||||
|
etag_override.extend([
|
||||||
|
'symlink_target_etag=%s' %
|
||||||
|
req.headers[TGT_ETAG_SYSMETA_SYMLINK_HDR],
|
||||||
|
'symlink_target_bytes=%s' %
|
||||||
|
req.headers[TGT_BYTES_SYSMETA_SYMLINK_HDR],
|
||||||
|
])
|
||||||
req.headers[get_container_update_override_key('etag')] = \
|
req.headers[get_container_update_override_key('etag')] = \
|
||||||
'; '.join(etag_override)
|
'; '.join(etag_override)
|
||||||
|
|
||||||
@ -495,11 +641,16 @@ class SymlinkObjectContext(WSGIContext):
|
|||||||
TGT_ACCT_SYSMETA_SYMLINK_HDR) or wsgi_quote(account)
|
TGT_ACCT_SYSMETA_SYMLINK_HDR) or wsgi_quote(account)
|
||||||
location_hdr = os.path.join(
|
location_hdr = os.path.join(
|
||||||
'/', version, target_acc, tgt_co)
|
'/', version, target_acc, tgt_co)
|
||||||
|
headers = {'location': location_hdr}
|
||||||
|
tgt_etag = self._response_header_value(
|
||||||
|
TGT_ETAG_SYSMETA_SYMLINK_HDR)
|
||||||
|
if tgt_etag:
|
||||||
|
headers[TGT_ETAG_SYMLINK_HDR] = tgt_etag
|
||||||
req.environ['swift.leave_relative_location'] = True
|
req.environ['swift.leave_relative_location'] = True
|
||||||
errmsg = 'The requested POST was applied to a symlink. POST ' +\
|
errmsg = 'The requested POST was applied to a symlink. POST ' +\
|
||||||
'directly to the target to apply requested metadata.'
|
'directly to the target to apply requested metadata.'
|
||||||
raise HTTPTemporaryRedirect(
|
raise HTTPTemporaryRedirect(
|
||||||
body=errmsg, headers={'location': location_hdr})
|
body=errmsg, headers=headers)
|
||||||
else:
|
else:
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
@ -512,10 +663,7 @@ class SymlinkObjectContext(WSGIContext):
|
|||||||
:returns: Response Iterator after start_response has been called
|
:returns: Response Iterator after start_response has been called
|
||||||
"""
|
"""
|
||||||
if req.method in ('GET', 'HEAD'):
|
if req.method in ('GET', 'HEAD'):
|
||||||
# if GET request came from versioned writes, then it should get
|
if req.params.get('symlink') == 'get':
|
||||||
# the symlink only, not the referenced target
|
|
||||||
if req.params.get('symlink') == 'get' or \
|
|
||||||
req.environ.get('swift.source') == 'VW':
|
|
||||||
resp = self.handle_get_head_symlink(req)
|
resp = self.handle_get_head_symlink(req)
|
||||||
else:
|
else:
|
||||||
resp = self.handle_get_head(req)
|
resp = self.handle_get_head(req)
|
||||||
@ -582,7 +730,7 @@ def filter_factory(global_conf, **local_conf):
|
|||||||
symloop_max = int(conf.get('symloop_max', DEFAULT_SYMLOOP_MAX))
|
symloop_max = int(conf.get('symloop_max', DEFAULT_SYMLOOP_MAX))
|
||||||
if symloop_max < 1:
|
if symloop_max < 1:
|
||||||
symloop_max = int(DEFAULT_SYMLOOP_MAX)
|
symloop_max = int(DEFAULT_SYMLOOP_MAX)
|
||||||
register_swift_info('symlink', symloop_max=symloop_max)
|
register_swift_info('symlink', symloop_max=symloop_max, static_links=True)
|
||||||
|
|
||||||
def symlink_mw(app):
|
def symlink_mw(app):
|
||||||
return SymlinkMiddleware(app, conf, symloop_max)
|
return SymlinkMiddleware(app, conf, symloop_max)
|
||||||
|
@ -371,7 +371,7 @@ class VersionedWritesContext(WSGIContext):
|
|||||||
# to container, but not READ. This was allowed in previous version
|
# to container, but not READ. This was allowed in previous version
|
||||||
# (i.e., before middleware) so keeping the same behavior here
|
# (i.e., before middleware) so keeping the same behavior here
|
||||||
get_req = make_pre_authed_request(
|
get_req = make_pre_authed_request(
|
||||||
req.environ, path=wsgi_quote(path_info),
|
req.environ, path=wsgi_quote(path_info) + '?symlink=get',
|
||||||
headers={'X-Newest': 'True'}, method='GET', swift_source='VW')
|
headers={'X-Newest': 'True'}, method='GET', swift_source='VW')
|
||||||
source_resp = get_req.get_response(self.app)
|
source_resp = get_req.get_response(self.app)
|
||||||
|
|
||||||
|
@ -73,8 +73,10 @@ class TestSymlinkEnv(BaseEnv):
|
|||||||
return (cls.link_cont, cls.tgt_cont)
|
return (cls.link_cont, cls.tgt_cont)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def target_content_location(cls):
|
def target_content_location(cls, override_obj=None, override_account=None):
|
||||||
return '%s/%s' % (cls.tgt_cont, cls.tgt_obj)
|
account = override_account or tf.parsed[0].path.split('/', 2)[2]
|
||||||
|
return '/v1/%s/%s/%s' % (account, cls.tgt_cont,
|
||||||
|
override_obj or cls.tgt_obj)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _make_request(cls, url, token, parsed, conn, method,
|
def _make_request(cls, url, token, parsed, conn, method,
|
||||||
@ -102,20 +104,21 @@ class TestSymlinkEnv(BaseEnv):
|
|||||||
return name
|
return name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create_tgt_object(cls):
|
def _create_tgt_object(cls, body=TARGET_BODY):
|
||||||
resp = retry(cls._make_request, method='PUT',
|
resp = retry(cls._make_request, method='PUT',
|
||||||
|
headers={'Content-Type': 'application/target'},
|
||||||
container=cls.tgt_cont, obj=cls.tgt_obj,
|
container=cls.tgt_cont, obj=cls.tgt_obj,
|
||||||
body=TARGET_BODY)
|
body=body)
|
||||||
if resp.status != 201:
|
if resp.status != 201:
|
||||||
raise ResponseError(resp)
|
raise ResponseError(resp)
|
||||||
|
|
||||||
# sanity: successful put response has content-length 0
|
# sanity: successful put response has content-length 0
|
||||||
cls.tgt_length = str(len(TARGET_BODY))
|
cls.tgt_length = str(len(body))
|
||||||
cls.tgt_etag = resp.getheader('etag')
|
cls.tgt_etag = resp.getheader('etag')
|
||||||
|
|
||||||
resp = retry(cls._make_request, method='GET',
|
resp = retry(cls._make_request, method='GET',
|
||||||
container=cls.tgt_cont, obj=cls.tgt_obj)
|
container=cls.tgt_cont, obj=cls.tgt_obj)
|
||||||
if resp.status != 200 and resp.content != TARGET_BODY:
|
if resp.status != 200 and resp.content != body:
|
||||||
raise ResponseError(resp)
|
raise ResponseError(resp)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -176,10 +179,17 @@ class TestSymlink(Base):
|
|||||||
yield uuid4().hex
|
yield uuid4().hex
|
||||||
|
|
||||||
self.obj_name_gen = object_name_generator()
|
self.obj_name_gen = object_name_generator()
|
||||||
|
self._account_name = None
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.env.tearDown()
|
self.env.tearDown()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account_name(self):
|
||||||
|
if not self._account_name:
|
||||||
|
self._account_name = tf.parsed[0].path.split('/', 2)[2]
|
||||||
|
return self._account_name
|
||||||
|
|
||||||
def _make_request(self, url, token, parsed, conn, method,
|
def _make_request(self, url, token, parsed, conn, method,
|
||||||
container, obj='', headers=None, body=b'',
|
container, obj='', headers=None, body=b'',
|
||||||
query_args=None, allow_redirects=True):
|
query_args=None, allow_redirects=True):
|
||||||
@ -210,22 +220,30 @@ class TestSymlink(Base):
|
|||||||
headers=headers)
|
headers=headers)
|
||||||
self.assertEqual(resp.status, 201)
|
self.assertEqual(resp.status, 201)
|
||||||
|
|
||||||
|
def _test_put_symlink_with_etag(self, link_cont, link_obj, tgt_cont,
|
||||||
|
tgt_obj, etag, headers=None):
|
||||||
|
headers = headers or {}
|
||||||
|
headers.update({'X-Symlink-Target': '%s/%s' % (tgt_cont, tgt_obj),
|
||||||
|
'X-Symlink-Target-Etag': etag})
|
||||||
|
resp = retry(self._make_request, method='PUT',
|
||||||
|
container=link_cont, obj=link_obj,
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(resp.status, 201, resp.content)
|
||||||
|
|
||||||
def _test_get_as_target_object(
|
def _test_get_as_target_object(
|
||||||
self, link_cont, link_obj, expected_content_location,
|
self, link_cont, link_obj, expected_content_location,
|
||||||
use_account=1):
|
use_account=1):
|
||||||
resp = retry(
|
resp = retry(
|
||||||
self._make_request, method='GET',
|
self._make_request, method='GET',
|
||||||
container=link_cont, obj=link_obj, use_account=use_account)
|
container=link_cont, obj=link_obj, use_account=use_account)
|
||||||
self.assertEqual(resp.status, 200)
|
self.assertEqual(resp.status, 200, resp.content)
|
||||||
self.assertEqual(resp.content, TARGET_BODY)
|
self.assertEqual(resp.content, TARGET_BODY)
|
||||||
self.assertEqual(resp.getheader('content-length'),
|
self.assertEqual(resp.getheader('content-length'),
|
||||||
str(self.env.tgt_length))
|
str(self.env.tgt_length))
|
||||||
self.assertEqual(resp.getheader('etag'), self.env.tgt_etag)
|
self.assertEqual(resp.getheader('etag'), self.env.tgt_etag)
|
||||||
self.assertIn('Content-Location', resp.headers)
|
self.assertIn('Content-Location', resp.headers)
|
||||||
# TODO: content-location is a full path so it's better to assert
|
self.assertEqual(expected_content_location,
|
||||||
# with the value, instead of assertIn
|
resp.getheader('content-location'))
|
||||||
self.assertIn(expected_content_location,
|
|
||||||
resp.getheader('content-location'))
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def _test_head_as_target_object(self, link_cont, link_obj, use_account=1):
|
def _test_head_as_target_object(self, link_cont, link_obj, use_account=1):
|
||||||
@ -299,8 +317,8 @@ class TestSymlink(Base):
|
|||||||
# and it's normalized
|
# and it's normalized
|
||||||
self._assertSymlink(
|
self._assertSymlink(
|
||||||
self.env.link_cont, link_obj,
|
self.env.link_cont, link_obj,
|
||||||
expected_content_location='%s/%s' % (
|
expected_content_location=self.env.target_content_location(
|
||||||
self.env.tgt_cont, normalized_quoted_obj))
|
normalized_quoted_obj))
|
||||||
|
|
||||||
# create a symlink using the normalized target path
|
# create a symlink using the normalized target path
|
||||||
self._test_put_symlink(link_cont=self.env.link_cont, link_obj=link_obj,
|
self._test_put_symlink(link_cont=self.env.link_cont, link_obj=link_obj,
|
||||||
@ -309,8 +327,8 @@ class TestSymlink(Base):
|
|||||||
# and it's ALSO normalized
|
# and it's ALSO normalized
|
||||||
self._assertSymlink(
|
self._assertSymlink(
|
||||||
self.env.link_cont, link_obj,
|
self.env.link_cont, link_obj,
|
||||||
expected_content_location='%s/%s' % (
|
expected_content_location=self.env.target_content_location(
|
||||||
self.env.tgt_cont, normalized_quoted_obj))
|
normalized_quoted_obj))
|
||||||
|
|
||||||
def test_symlink_put_head_get(self):
|
def test_symlink_put_head_get(self):
|
||||||
link_obj = uuid4().hex
|
link_obj = uuid4().hex
|
||||||
@ -322,6 +340,195 @@ class TestSymlink(Base):
|
|||||||
|
|
||||||
self._assertSymlink(self.env.link_cont, link_obj)
|
self._assertSymlink(self.env.link_cont, link_obj)
|
||||||
|
|
||||||
|
def test_symlink_with_etag_put_head_get(self):
|
||||||
|
link_obj = uuid4().hex
|
||||||
|
|
||||||
|
# PUT link_obj
|
||||||
|
self._test_put_symlink_with_etag(link_cont=self.env.link_cont,
|
||||||
|
link_obj=link_obj,
|
||||||
|
tgt_cont=self.env.tgt_cont,
|
||||||
|
tgt_obj=self.env.tgt_obj,
|
||||||
|
etag=self.env.tgt_etag)
|
||||||
|
|
||||||
|
self._assertSymlink(self.env.link_cont, link_obj)
|
||||||
|
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='GET',
|
||||||
|
container=self.env.link_cont, obj=link_obj,
|
||||||
|
headers={'If-Match': self.env.tgt_etag})
|
||||||
|
self.assertEqual(resp.status, 200)
|
||||||
|
self.assertEqual(resp.getheader('content-location'),
|
||||||
|
self.env.target_content_location())
|
||||||
|
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='GET',
|
||||||
|
container=self.env.link_cont, obj=link_obj,
|
||||||
|
headers={'If-Match': 'not-the-etag'})
|
||||||
|
self.assertEqual(resp.status, 412)
|
||||||
|
self.assertEqual(resp.getheader('content-location'),
|
||||||
|
self.env.target_content_location())
|
||||||
|
|
||||||
|
def test_static_symlink_with_bad_etag_put_head_get(self):
|
||||||
|
link_obj = uuid4().hex
|
||||||
|
|
||||||
|
# PUT link_obj
|
||||||
|
self._test_put_symlink_with_etag(link_cont=self.env.link_cont,
|
||||||
|
link_obj=link_obj,
|
||||||
|
tgt_cont=self.env.tgt_cont,
|
||||||
|
tgt_obj=self.env.tgt_obj,
|
||||||
|
etag=self.env.tgt_etag)
|
||||||
|
|
||||||
|
# overwrite tgt object
|
||||||
|
self.env._create_tgt_object(body='updated target body')
|
||||||
|
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='HEAD',
|
||||||
|
container=self.env.link_cont, obj=link_obj)
|
||||||
|
self.assertEqual(resp.status, 409)
|
||||||
|
# but we still know where it points
|
||||||
|
self.assertEqual(resp.getheader('content-location'),
|
||||||
|
self.env.target_content_location())
|
||||||
|
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='GET',
|
||||||
|
container=self.env.link_cont, obj=link_obj)
|
||||||
|
self.assertEqual(resp.status, 409)
|
||||||
|
self.assertEqual(resp.getheader('content-location'),
|
||||||
|
self.env.target_content_location())
|
||||||
|
|
||||||
|
# uses a mechanism entirely divorced from if-match
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='GET',
|
||||||
|
container=self.env.link_cont, obj=link_obj,
|
||||||
|
headers={'If-Match': self.env.tgt_etag})
|
||||||
|
self.assertEqual(resp.status, 409)
|
||||||
|
self.assertEqual(resp.getheader('content-location'),
|
||||||
|
self.env.target_content_location())
|
||||||
|
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='GET',
|
||||||
|
container=self.env.link_cont, obj=link_obj,
|
||||||
|
headers={'If-Match': 'not-the-etag'})
|
||||||
|
self.assertEqual(resp.status, 409)
|
||||||
|
self.assertEqual(resp.getheader('content-location'),
|
||||||
|
self.env.target_content_location())
|
||||||
|
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='DELETE',
|
||||||
|
container=self.env.tgt_cont, obj=self.env.tgt_obj)
|
||||||
|
|
||||||
|
# not-found-ness trumps if-match-ness
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='GET',
|
||||||
|
container=self.env.link_cont, obj=link_obj)
|
||||||
|
self.assertEqual(resp.status, 404)
|
||||||
|
self.assertEqual(resp.getheader('content-location'),
|
||||||
|
self.env.target_content_location())
|
||||||
|
|
||||||
|
def test_dynamic_link_to_static_link(self):
|
||||||
|
static_link_obj = uuid4().hex
|
||||||
|
|
||||||
|
# PUT static_link to tgt_obj
|
||||||
|
self._test_put_symlink_with_etag(link_cont=self.env.link_cont,
|
||||||
|
link_obj=static_link_obj,
|
||||||
|
tgt_cont=self.env.tgt_cont,
|
||||||
|
tgt_obj=self.env.tgt_obj,
|
||||||
|
etag=self.env.tgt_etag)
|
||||||
|
|
||||||
|
symlink_obj = uuid4().hex
|
||||||
|
|
||||||
|
# PUT symlink to static_link
|
||||||
|
self._test_put_symlink(link_cont=self.env.link_cont,
|
||||||
|
link_obj=symlink_obj,
|
||||||
|
tgt_cont=self.env.link_cont,
|
||||||
|
tgt_obj=static_link_obj)
|
||||||
|
|
||||||
|
self._test_get_as_target_object(
|
||||||
|
link_cont=self.env.link_cont, link_obj=symlink_obj,
|
||||||
|
expected_content_location=self.env.target_content_location())
|
||||||
|
|
||||||
|
def test_static_link_to_dynamic_link(self):
|
||||||
|
symlink_obj = uuid4().hex
|
||||||
|
|
||||||
|
# PUT symlink to tgt_obj
|
||||||
|
self._test_put_symlink(link_cont=self.env.link_cont,
|
||||||
|
link_obj=symlink_obj,
|
||||||
|
tgt_cont=self.env.tgt_cont,
|
||||||
|
tgt_obj=self.env.tgt_obj)
|
||||||
|
|
||||||
|
static_link_obj = uuid4().hex
|
||||||
|
|
||||||
|
# PUT a static_link to the symlink
|
||||||
|
self._test_put_symlink_with_etag(link_cont=self.env.link_cont,
|
||||||
|
link_obj=static_link_obj,
|
||||||
|
tgt_cont=self.env.link_cont,
|
||||||
|
tgt_obj=symlink_obj,
|
||||||
|
etag=MD5_OF_EMPTY_STRING)
|
||||||
|
|
||||||
|
self._test_get_as_target_object(
|
||||||
|
link_cont=self.env.link_cont, link_obj=static_link_obj,
|
||||||
|
expected_content_location=self.env.target_content_location())
|
||||||
|
|
||||||
|
def test_static_link_to_nowhere(self):
|
||||||
|
missing_obj = uuid4().hex
|
||||||
|
static_link_obj = uuid4().hex
|
||||||
|
|
||||||
|
# PUT a static_link to the missing name
|
||||||
|
headers = {
|
||||||
|
'X-Symlink-Target': '%s/%s' % (self.env.link_cont, missing_obj),
|
||||||
|
'X-Symlink-Target-Etag': MD5_OF_EMPTY_STRING}
|
||||||
|
resp = retry(self._make_request, method='PUT',
|
||||||
|
container=self.env.link_cont, obj=static_link_obj,
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(resp.status, 409)
|
||||||
|
self.assertEqual(resp.content, b'X-Symlink-Target does not exist')
|
||||||
|
|
||||||
|
def test_static_link_to_broken_symlink(self):
|
||||||
|
symlink_obj = uuid4().hex
|
||||||
|
|
||||||
|
# PUT symlink to tgt_obj
|
||||||
|
self._test_put_symlink(link_cont=self.env.link_cont,
|
||||||
|
link_obj=symlink_obj,
|
||||||
|
tgt_cont=self.env.tgt_cont,
|
||||||
|
tgt_obj=self.env.tgt_obj)
|
||||||
|
|
||||||
|
static_link_obj = uuid4().hex
|
||||||
|
|
||||||
|
# PUT a static_link to the symlink
|
||||||
|
self._test_put_symlink_with_etag(link_cont=self.env.link_cont,
|
||||||
|
link_obj=static_link_obj,
|
||||||
|
tgt_cont=self.env.link_cont,
|
||||||
|
tgt_obj=symlink_obj,
|
||||||
|
etag=MD5_OF_EMPTY_STRING)
|
||||||
|
|
||||||
|
# break the symlink
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='DELETE',
|
||||||
|
container=self.env.tgt_cont, obj=self.env.tgt_obj)
|
||||||
|
self.assertEqual(resp.status // 100, 2)
|
||||||
|
|
||||||
|
# sanity
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='GET',
|
||||||
|
container=self.env.link_cont, obj=symlink_obj)
|
||||||
|
self.assertEqual(resp.status, 404)
|
||||||
|
|
||||||
|
# static_link is broken too!
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='GET',
|
||||||
|
container=self.env.link_cont, obj=static_link_obj)
|
||||||
|
self.assertEqual(resp.status, 404)
|
||||||
|
|
||||||
|
# interestingly you may create a static_link to a broken symlink
|
||||||
|
broken_static_link_obj = uuid4().hex
|
||||||
|
|
||||||
|
# PUT a static_link to the broken symlink
|
||||||
|
self._test_put_symlink_with_etag(link_cont=self.env.link_cont,
|
||||||
|
link_obj=broken_static_link_obj,
|
||||||
|
tgt_cont=self.env.link_cont,
|
||||||
|
tgt_obj=symlink_obj,
|
||||||
|
etag=MD5_OF_EMPTY_STRING)
|
||||||
|
|
||||||
def test_symlink_get_ranged(self):
|
def test_symlink_get_ranged(self):
|
||||||
link_obj = uuid4().hex
|
link_obj = uuid4().hex
|
||||||
|
|
||||||
@ -353,9 +560,8 @@ class TestSymlink(Base):
|
|||||||
container=self.env.link_cont, obj=link_obj, use_account=1)
|
container=self.env.link_cont, obj=link_obj, use_account=1)
|
||||||
self.assertEqual(resp.status, 404)
|
self.assertEqual(resp.status, 404)
|
||||||
self.assertIn('Content-Location', resp.headers)
|
self.assertIn('Content-Location', resp.headers)
|
||||||
expected_location_hdr = "%s/%s" % (self.env.tgt_cont, target_obj)
|
self.assertEqual(self.env.target_content_location(target_obj),
|
||||||
self.assertIn(expected_location_hdr,
|
resp.getheader('content-location'))
|
||||||
resp.getheader('content-location'))
|
|
||||||
|
|
||||||
# HEAD on target object via symlink should return a 404 since target
|
# HEAD on target object via symlink should return a 404 since target
|
||||||
# object has not yet been written
|
# object has not yet been written
|
||||||
@ -396,8 +602,8 @@ class TestSymlink(Base):
|
|||||||
self.assertEqual(resp.getheader('content-length'), str(target_length))
|
self.assertEqual(resp.getheader('content-length'), str(target_length))
|
||||||
self.assertEqual(resp.getheader('etag'), target_etag)
|
self.assertEqual(resp.getheader('etag'), target_etag)
|
||||||
self.assertIn('Content-Location', resp.headers)
|
self.assertIn('Content-Location', resp.headers)
|
||||||
self.assertIn(expected_location_hdr,
|
self.assertEqual(self.env.target_content_location(target_obj),
|
||||||
resp.getheader('content-location'))
|
resp.getheader('content-location'))
|
||||||
|
|
||||||
def test_symlink_chain(self):
|
def test_symlink_chain(self):
|
||||||
# Testing to symlink chain like symlink -> symlink -> target.
|
# Testing to symlink chain like symlink -> symlink -> target.
|
||||||
@ -448,6 +654,66 @@ class TestSymlink(Base):
|
|||||||
# However, HEAD/GET to the (just) link is still ok
|
# However, HEAD/GET to the (just) link is still ok
|
||||||
self._assertLinkObject(container, too_many_chain_link)
|
self._assertLinkObject(container, too_many_chain_link)
|
||||||
|
|
||||||
|
def test_symlink_chain_with_etag(self):
|
||||||
|
# Testing to symlink chain like symlink -> symlink -> target.
|
||||||
|
symloop_max = cluster_info['symlink']['symloop_max']
|
||||||
|
|
||||||
|
# create symlink chain in a container. To simplify,
|
||||||
|
# use target container for all objects (symlinks and target) here
|
||||||
|
previous = self.env.tgt_obj
|
||||||
|
container = self.env.tgt_cont
|
||||||
|
|
||||||
|
for link_obj in itertools.islice(self.obj_name_gen, symloop_max):
|
||||||
|
# PUT link_obj point to tgt_obj
|
||||||
|
self._test_put_symlink_with_etag(link_cont=container,
|
||||||
|
link_obj=link_obj,
|
||||||
|
tgt_cont=container,
|
||||||
|
tgt_obj=previous,
|
||||||
|
etag=self.env.tgt_etag)
|
||||||
|
|
||||||
|
# set current link_obj to previous
|
||||||
|
previous = link_obj
|
||||||
|
|
||||||
|
# the last link is valid for symloop_max constraint
|
||||||
|
max_chain_link = link_obj
|
||||||
|
self._assertSymlink(link_cont=container, link_obj=max_chain_link)
|
||||||
|
|
||||||
|
# chained etag validation works as long as the target symlink works
|
||||||
|
headers = {'X-Symlink-Target': '%s/%s' % (container, max_chain_link),
|
||||||
|
'X-Symlink-Target-Etag': 'not-the-real-etag'}
|
||||||
|
resp = retry(self._make_request, method='PUT',
|
||||||
|
container=container, obj=uuid4().hex,
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(resp.status, 409)
|
||||||
|
|
||||||
|
# PUT a new link_obj pointing to the max_chain_link can validate the
|
||||||
|
# ETag but will result in 409 error on the HEAD/GET.
|
||||||
|
too_many_chain_link = next(self.obj_name_gen)
|
||||||
|
self._test_put_symlink_with_etag(
|
||||||
|
link_cont=container, link_obj=too_many_chain_link,
|
||||||
|
tgt_cont=container, tgt_obj=max_chain_link,
|
||||||
|
etag=self.env.tgt_etag)
|
||||||
|
|
||||||
|
# try to HEAD to target object via too_many_chain_link
|
||||||
|
resp = retry(self._make_request, method='HEAD',
|
||||||
|
container=container,
|
||||||
|
obj=too_many_chain_link)
|
||||||
|
self.assertEqual(resp.status, 409)
|
||||||
|
self.assertEqual(resp.content, b'')
|
||||||
|
|
||||||
|
# try to GET to target object via too_many_chain_link
|
||||||
|
resp = retry(self._make_request, method='GET',
|
||||||
|
container=container,
|
||||||
|
obj=too_many_chain_link)
|
||||||
|
self.assertEqual(resp.status, 409)
|
||||||
|
self.assertEqual(
|
||||||
|
resp.content,
|
||||||
|
b'Too many levels of symbolic links, maximum allowed is %d' %
|
||||||
|
symloop_max)
|
||||||
|
|
||||||
|
# However, HEAD/GET to the (just) link is still ok
|
||||||
|
self._assertLinkObject(container, too_many_chain_link)
|
||||||
|
|
||||||
def test_symlink_and_slo_manifest_chain(self):
|
def test_symlink_and_slo_manifest_chain(self):
|
||||||
if 'slo' not in cluster_info:
|
if 'slo' not in cluster_info:
|
||||||
raise SkipTest
|
raise SkipTest
|
||||||
@ -557,7 +823,7 @@ class TestSymlink(Base):
|
|||||||
'%s/%s' % (self.env.tgt_cont, self.env.tgt_obj)}
|
'%s/%s' % (self.env.tgt_cont, self.env.tgt_obj)}
|
||||||
resp = retry(
|
resp = retry(
|
||||||
self._make_request, method='PUT', container=self.env.link_cont,
|
self._make_request, method='PUT', container=self.env.link_cont,
|
||||||
obj=link_obj, body='non-zero-length', headers=headers)
|
obj=link_obj, body=b'non-zero-length', headers=headers)
|
||||||
|
|
||||||
self.assertEqual(resp.status, 400)
|
self.assertEqual(resp.status, 400)
|
||||||
self.assertEqual(resp.content,
|
self.assertEqual(resp.content,
|
||||||
@ -636,7 +902,6 @@ class TestSymlink(Base):
|
|||||||
tgt_obj=self.env.tgt_obj)
|
tgt_obj=self.env.tgt_obj)
|
||||||
|
|
||||||
copy_src = '%s/%s' % (self.env.link_cont, link_obj1)
|
copy_src = '%s/%s' % (self.env.link_cont, link_obj1)
|
||||||
account_one = tf.parsed[0].path.split('/', 2)[2]
|
|
||||||
perm_two = tf.swift_test_perm[1]
|
perm_two = tf.swift_test_perm[1]
|
||||||
|
|
||||||
# add X-Content-Read to account 1 link_cont and tgt_cont
|
# add X-Content-Read to account 1 link_cont and tgt_cont
|
||||||
@ -659,7 +924,7 @@ class TestSymlink(Base):
|
|||||||
# symlink to the account 2 container that points to the
|
# symlink to the account 2 container that points to the
|
||||||
# container/object in the account 2.
|
# container/object in the account 2.
|
||||||
# (the container/object is not prepared)
|
# (the container/object is not prepared)
|
||||||
headers = {'X-Copy-From-Account': account_one,
|
headers = {'X-Copy-From-Account': self.account_name,
|
||||||
'X-Copy-From': copy_src}
|
'X-Copy-From': copy_src}
|
||||||
resp = retry(self._make_request_with_symlink_get, method='PUT',
|
resp = retry(self._make_request_with_symlink_get, method='PUT',
|
||||||
container=self.env.link_cont, obj=link_obj2,
|
container=self.env.link_cont, obj=link_obj2,
|
||||||
@ -669,6 +934,7 @@ class TestSymlink(Base):
|
|||||||
# sanity: HEAD/GET on link_obj itself
|
# sanity: HEAD/GET on link_obj itself
|
||||||
self._assertLinkObject(self.env.link_cont, link_obj2, use_account=2)
|
self._assertLinkObject(self.env.link_cont, link_obj2, use_account=2)
|
||||||
|
|
||||||
|
account_two = tf.parsed[1].path.split('/', 2)[2]
|
||||||
# no target object in the account 2
|
# no target object in the account 2
|
||||||
for method in ('HEAD', 'GET'):
|
for method in ('HEAD', 'GET'):
|
||||||
resp = retry(
|
resp = retry(
|
||||||
@ -676,14 +942,15 @@ class TestSymlink(Base):
|
|||||||
container=self.env.link_cont, obj=link_obj2, use_account=2)
|
container=self.env.link_cont, obj=link_obj2, use_account=2)
|
||||||
self.assertEqual(resp.status, 404)
|
self.assertEqual(resp.status, 404)
|
||||||
self.assertIn('content-location', resp.headers)
|
self.assertIn('content-location', resp.headers)
|
||||||
self.assertIn(self.env.target_content_location(),
|
self.assertEqual(
|
||||||
resp.getheader('content-location'))
|
self.env.target_content_location(override_account=account_two),
|
||||||
|
resp.getheader('content-location'))
|
||||||
|
|
||||||
# copy symlink itself to a different account with target account
|
# copy symlink itself to a different account with target account
|
||||||
# the target path will be in account 1
|
# the target path will be in account 1
|
||||||
# the target path will have an object
|
# the target path will have an object
|
||||||
headers = {'X-Symlink-target-Account': account_one,
|
headers = {'X-Symlink-target-Account': self.account_name,
|
||||||
'X-Copy-From-Account': account_one,
|
'X-Copy-From-Account': self.account_name,
|
||||||
'X-Copy-From': copy_src}
|
'X-Copy-From': copy_src}
|
||||||
resp = retry(
|
resp = retry(
|
||||||
self._make_request_with_symlink_get, method='PUT',
|
self._make_request_with_symlink_get, method='PUT',
|
||||||
@ -780,7 +1047,8 @@ class TestSymlink(Base):
|
|||||||
link_obj = uuid4().hex
|
link_obj = uuid4().hex
|
||||||
value1 = uuid4().hex
|
value1 = uuid4().hex
|
||||||
|
|
||||||
self._test_put_symlink(link_cont=self.env.link_cont, link_obj=link_obj,
|
self._test_put_symlink(link_cont=self.env.link_cont,
|
||||||
|
link_obj=link_obj,
|
||||||
tgt_cont=self.env.tgt_cont,
|
tgt_cont=self.env.tgt_cont,
|
||||||
tgt_obj=self.env.tgt_obj)
|
tgt_obj=self.env.tgt_obj)
|
||||||
|
|
||||||
@ -821,6 +1089,73 @@ class TestSymlink(Base):
|
|||||||
# sanity: no X-Object-Meta-Alpha exists in the response header
|
# sanity: no X-Object-Meta-Alpha exists in the response header
|
||||||
self.assertNotIn('X-Object-Meta-Alpha', resp.headers)
|
self.assertNotIn('X-Object-Meta-Alpha', resp.headers)
|
||||||
|
|
||||||
|
def test_post_to_broken_dynamic_symlink(self):
|
||||||
|
# create a symlink to nowhere
|
||||||
|
link_obj = '%s-the-link' % uuid4().hex
|
||||||
|
tgt_obj = '%s-no-where' % uuid4().hex
|
||||||
|
headers = {'X-Symlink-Target': '%s/%s' % (self.env.tgt_cont, tgt_obj)}
|
||||||
|
resp = retry(self._make_request, method='PUT',
|
||||||
|
container=self.env.link_cont, obj=link_obj,
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(resp.status, 201)
|
||||||
|
# it's a real link!
|
||||||
|
self._assertLinkObject(self.env.link_cont, link_obj)
|
||||||
|
# ... it's just broken
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='GET',
|
||||||
|
container=self.env.link_cont, obj=link_obj)
|
||||||
|
self.assertEqual(resp.status, 404)
|
||||||
|
target_path = '/v1/%s/%s/%s' % (
|
||||||
|
self.account_name, self.env.tgt_cont, tgt_obj)
|
||||||
|
self.assertEqual(target_path, resp.headers['Content-Location'])
|
||||||
|
|
||||||
|
# we'll redirect with the Location header to the (invalid) target
|
||||||
|
headers = {'X-Object-Meta-Alpha': 'apple'}
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='POST', container=self.env.link_cont,
|
||||||
|
obj=link_obj, headers=headers, allow_redirects=False)
|
||||||
|
self.assertEqual(resp.status, 307)
|
||||||
|
self.assertEqual(target_path, resp.headers['Location'])
|
||||||
|
|
||||||
|
# and of course metadata *is* applied to the link
|
||||||
|
resp = retry(
|
||||||
|
self._make_request_with_symlink_get, method='HEAD',
|
||||||
|
container=self.env.link_cont, obj=link_obj)
|
||||||
|
self.assertEqual(resp.status, 200)
|
||||||
|
self.assertTrue(resp.getheader('X-Object-Meta-Alpha'), 'apple')
|
||||||
|
|
||||||
|
def test_post_to_broken_static_symlink(self):
|
||||||
|
link_obj = uuid4().hex
|
||||||
|
|
||||||
|
# PUT link_obj
|
||||||
|
self._test_put_symlink_with_etag(link_cont=self.env.link_cont,
|
||||||
|
link_obj=link_obj,
|
||||||
|
tgt_cont=self.env.tgt_cont,
|
||||||
|
tgt_obj=self.env.tgt_obj,
|
||||||
|
etag=self.env.tgt_etag)
|
||||||
|
|
||||||
|
# overwrite tgt object
|
||||||
|
old_tgt_etag = self.env.tgt_etag
|
||||||
|
self.env._create_tgt_object(body='updated target body')
|
||||||
|
|
||||||
|
# sanity
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='HEAD',
|
||||||
|
container=self.env.link_cont, obj=link_obj)
|
||||||
|
self.assertEqual(resp.status, 409)
|
||||||
|
|
||||||
|
# but POST will still 307
|
||||||
|
headers = {'X-Object-Meta-Alpha': 'apple'}
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='POST', container=self.env.link_cont,
|
||||||
|
obj=link_obj, headers=headers, allow_redirects=False)
|
||||||
|
self.assertEqual(resp.status, 307)
|
||||||
|
target_path = '/v1/%s/%s/%s' % (
|
||||||
|
self.account_name, self.env.tgt_cont, self.env.tgt_obj)
|
||||||
|
self.assertEqual(target_path, resp.headers['Location'])
|
||||||
|
# but we give you the Etag just like... FYI?
|
||||||
|
self.assertEqual(old_tgt_etag, resp.headers['X-Symlink-Target-Etag'])
|
||||||
|
|
||||||
def test_post_with_symlink_header(self):
|
def test_post_with_symlink_header(self):
|
||||||
# POSTing to a symlink is not allowed and should return a 307
|
# POSTing to a symlink is not allowed and should return a 307
|
||||||
# updating the symlink target with a POST should always fail
|
# updating the symlink target with a POST should always fail
|
||||||
@ -878,11 +1213,9 @@ class TestSymlink(Base):
|
|||||||
raise SkipTest
|
raise SkipTest
|
||||||
link_obj = uuid4().hex
|
link_obj = uuid4().hex
|
||||||
|
|
||||||
account_one = tf.parsed[0].path.split('/', 2)[2]
|
|
||||||
|
|
||||||
# create symlink in account 2
|
# create symlink in account 2
|
||||||
# pointing to account 1
|
# pointing to account 1
|
||||||
headers = {'X-Symlink-Target-Account': account_one,
|
headers = {'X-Symlink-Target-Account': self.account_name,
|
||||||
'X-Symlink-Target':
|
'X-Symlink-Target':
|
||||||
'%s/%s' % (self.env.tgt_cont, self.env.tgt_obj)}
|
'%s/%s' % (self.env.tgt_cont, self.env.tgt_obj)}
|
||||||
resp = retry(self._make_request, method='PUT',
|
resp = retry(self._make_request, method='PUT',
|
||||||
@ -900,6 +1233,9 @@ class TestSymlink(Base):
|
|||||||
container=self.env.link_cont, obj=link_obj, use_account=2)
|
container=self.env.link_cont, obj=link_obj, use_account=2)
|
||||||
|
|
||||||
self.assertEqual(resp.status, 403)
|
self.assertEqual(resp.status, 403)
|
||||||
|
# still know where it's pointing
|
||||||
|
self.assertEqual(resp.getheader('content-location'),
|
||||||
|
self.env.target_content_location())
|
||||||
|
|
||||||
# add X-Content-Read to account 1 tgt_cont
|
# add X-Content-Read to account 1 tgt_cont
|
||||||
# permit account 2 to read account 1 tgt_cont
|
# permit account 2 to read account 1 tgt_cont
|
||||||
@ -917,11 +1253,96 @@ class TestSymlink(Base):
|
|||||||
self.env.link_cont, link_obj,
|
self.env.link_cont, link_obj,
|
||||||
expected_content_location=self.env.target_content_location(),
|
expected_content_location=self.env.target_content_location(),
|
||||||
use_account=2)
|
use_account=2)
|
||||||
self.assertIn(account_one, resp.getheader('content-location'))
|
|
||||||
|
@requires_acls
|
||||||
|
def test_symlink_with_etag_put_target_account(self):
|
||||||
|
if tf.skip or tf.skip2:
|
||||||
|
raise SkipTest
|
||||||
|
link_obj = uuid4().hex
|
||||||
|
|
||||||
|
# try to create a symlink in account 2 pointing to account 1
|
||||||
|
symlink_headers = {
|
||||||
|
'X-Symlink-Target-Account': self.account_name,
|
||||||
|
'X-Symlink-Target':
|
||||||
|
'%s/%s' % (self.env.tgt_cont, self.env.tgt_obj),
|
||||||
|
'X-Symlink-Target-Etag': self.env.tgt_etag}
|
||||||
|
resp = retry(self._make_request, method='PUT',
|
||||||
|
container=self.env.link_cont, obj=link_obj,
|
||||||
|
headers=symlink_headers, use_account=2)
|
||||||
|
# since we don't have read access to verify the object we get the
|
||||||
|
# permissions error
|
||||||
|
self.assertEqual(resp.status, 403)
|
||||||
|
perm_two = tf.swift_test_perm[1]
|
||||||
|
|
||||||
|
# add X-Content-Read to account 1 tgt_cont
|
||||||
|
# permit account 2 to read account 1 tgt_cont
|
||||||
|
# add acl to allow reading from source
|
||||||
|
acl_headers = {'X-Container-Read': perm_two}
|
||||||
|
resp = retry(self._make_request, method='POST',
|
||||||
|
container=self.env.tgt_cont, headers=acl_headers)
|
||||||
|
self.assertEqual(resp.status, 204)
|
||||||
|
|
||||||
|
# now we can create the symlink
|
||||||
|
resp = retry(self._make_request, method='PUT',
|
||||||
|
container=self.env.link_cont, obj=link_obj,
|
||||||
|
headers=symlink_headers, use_account=2)
|
||||||
|
self.assertEqual(resp.status, 201)
|
||||||
|
self._assertLinkObject(self.env.link_cont, link_obj, use_account=2)
|
||||||
|
|
||||||
|
# GET to target object via symlink
|
||||||
|
resp = self._test_get_as_target_object(
|
||||||
|
self.env.link_cont, link_obj,
|
||||||
|
expected_content_location=self.env.target_content_location(),
|
||||||
|
use_account=2)
|
||||||
|
|
||||||
|
# Overwrite target
|
||||||
|
resp = retry(self._make_request, method='PUT',
|
||||||
|
container=self.env.tgt_cont, obj=self.env.tgt_obj,
|
||||||
|
body='some other content')
|
||||||
|
self.assertEqual(resp.status, 201)
|
||||||
|
|
||||||
|
# link is now broken
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='GET',
|
||||||
|
container=self.env.link_cont, obj=link_obj, use_account=2)
|
||||||
|
self.assertEqual(resp.status, 409)
|
||||||
|
|
||||||
|
# but we still know where it points
|
||||||
|
self.assertEqual(resp.getheader('content-location'),
|
||||||
|
self.env.target_content_location())
|
||||||
|
|
||||||
|
# sanity test, remove permissions
|
||||||
|
headers = {'X-Remove-Container-Read': 'remove'}
|
||||||
|
resp = retry(self._make_request, method='POST',
|
||||||
|
container=self.env.tgt_cont, headers=headers)
|
||||||
|
self.assertEqual(resp.status, 204)
|
||||||
|
# it should be ok to get the symlink itself, but not the target object
|
||||||
|
# because the read acl has been revoked
|
||||||
|
self._assertLinkObject(self.env.link_cont, link_obj, use_account=2)
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='GET',
|
||||||
|
container=self.env.link_cont, obj=link_obj, use_account=2)
|
||||||
|
self.assertEqual(resp.status, 403)
|
||||||
|
# Still know where it is, though
|
||||||
|
self.assertEqual(resp.getheader('content-location'),
|
||||||
|
self.env.target_content_location())
|
||||||
|
|
||||||
|
def test_symlink_invalid_etag(self):
|
||||||
|
link_obj = uuid4().hex
|
||||||
|
headers = {'X-Symlink-Target': '%s/%s' % (self.env.tgt_cont,
|
||||||
|
self.env.tgt_obj),
|
||||||
|
'X-Symlink-Target-Etag': 'not-the-real-etag'}
|
||||||
|
resp = retry(self._make_request, method='PUT',
|
||||||
|
container=self.env.link_cont, obj=link_obj,
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(resp.status, 409)
|
||||||
|
self.assertEqual(resp.content,
|
||||||
|
b"Object Etag 'ab706c400731332bffa67ed4bc15dcac' "
|
||||||
|
b"does not match X-Symlink-Target-Etag header "
|
||||||
|
b"'not-the-real-etag'")
|
||||||
|
|
||||||
def test_symlink_object_listing(self):
|
def test_symlink_object_listing(self):
|
||||||
link_obj = uuid4().hex
|
link_obj = uuid4().hex
|
||||||
|
|
||||||
self._test_put_symlink(link_cont=self.env.link_cont, link_obj=link_obj,
|
self._test_put_symlink(link_cont=self.env.link_cont, link_obj=link_obj,
|
||||||
tgt_cont=self.env.tgt_cont,
|
tgt_cont=self.env.tgt_cont,
|
||||||
tgt_obj=self.env.tgt_obj)
|
tgt_obj=self.env.tgt_obj)
|
||||||
@ -933,9 +1354,53 @@ class TestSymlink(Base):
|
|||||||
self.assertEqual(resp.status, 200)
|
self.assertEqual(resp.status, 200)
|
||||||
object_list = json.loads(resp.content)
|
object_list = json.loads(resp.content)
|
||||||
self.assertEqual(len(object_list), 1)
|
self.assertEqual(len(object_list), 1)
|
||||||
|
obj_info = object_list[0]
|
||||||
|
self.assertIn('symlink_path', obj_info)
|
||||||
|
self.assertEqual(self.env.target_content_location(),
|
||||||
|
obj_info['symlink_path'])
|
||||||
|
self.assertNotIn('symlink_etag', obj_info)
|
||||||
|
|
||||||
|
def test_static_link_object_listing(self):
|
||||||
|
link_obj = uuid4().hex
|
||||||
|
self._test_put_symlink_with_etag(link_cont=self.env.link_cont,
|
||||||
|
link_obj=link_obj,
|
||||||
|
tgt_cont=self.env.tgt_cont,
|
||||||
|
tgt_obj=self.env.tgt_obj,
|
||||||
|
etag=self.env.tgt_etag)
|
||||||
|
# sanity
|
||||||
|
self._assertSymlink(self.env.link_cont, link_obj)
|
||||||
|
resp = retry(self._make_request, method='GET',
|
||||||
|
container=self.env.link_cont,
|
||||||
|
query_args='format=json')
|
||||||
|
self.assertEqual(resp.status, 200)
|
||||||
|
object_list = json.loads(resp.content)
|
||||||
|
self.assertEqual(len(object_list), 1)
|
||||||
self.assertIn('symlink_path', object_list[0])
|
self.assertIn('symlink_path', object_list[0])
|
||||||
self.assertIn(self.env.target_content_location(),
|
self.assertEqual(self.env.target_content_location(),
|
||||||
object_list[0]['symlink_path'])
|
object_list[0]['symlink_path'])
|
||||||
|
obj_info = object_list[0]
|
||||||
|
self.assertIn('symlink_etag', obj_info)
|
||||||
|
self.assertEqual(self.env.tgt_etag,
|
||||||
|
obj_info['symlink_etag'])
|
||||||
|
self.assertEqual(int(self.env.tgt_length),
|
||||||
|
obj_info['symlink_bytes'])
|
||||||
|
self.assertEqual(obj_info['content_type'], 'application/target')
|
||||||
|
|
||||||
|
# POSTing to a static_link can change the listing Content-Type
|
||||||
|
headers = {'Content-Type': 'application/foo'}
|
||||||
|
resp = retry(
|
||||||
|
self._make_request, method='POST', container=self.env.link_cont,
|
||||||
|
obj=link_obj, headers=headers, allow_redirects=False)
|
||||||
|
self.assertEqual(resp.status, 307)
|
||||||
|
|
||||||
|
resp = retry(self._make_request, method='GET',
|
||||||
|
container=self.env.link_cont,
|
||||||
|
query_args='format=json')
|
||||||
|
self.assertEqual(resp.status, 200)
|
||||||
|
object_list = json.loads(resp.content)
|
||||||
|
self.assertEqual(len(object_list), 1)
|
||||||
|
obj_info = object_list[0]
|
||||||
|
self.assertEqual(obj_info['content_type'], 'application/foo')
|
||||||
|
|
||||||
|
|
||||||
class TestCrossPolicySymlinkEnv(TestSymlinkEnv):
|
class TestCrossPolicySymlinkEnv(TestSymlinkEnv):
|
||||||
@ -1007,6 +1472,8 @@ class TestSymlinkSlo(Base):
|
|||||||
"Expected slo_enabled to be True/False, got %r" %
|
"Expected slo_enabled to be True/False, got %r" %
|
||||||
(self.env.slo_enabled,))
|
(self.env.slo_enabled,))
|
||||||
self.file_symlink = self.env.container.file(uuid4().hex)
|
self.file_symlink = self.env.container.file(uuid4().hex)
|
||||||
|
self.account_name = self.env.container.conn.storage_path.rsplit(
|
||||||
|
'/', 1)[-1]
|
||||||
|
|
||||||
def test_symlink_target_slo_manifest(self):
|
def test_symlink_target_slo_manifest(self):
|
||||||
self.file_symlink.write(hdrs={'X-Symlink-Target':
|
self.file_symlink.write(hdrs={'X-Symlink-Target':
|
||||||
@ -1020,6 +1487,142 @@ class TestSymlinkSlo(Base):
|
|||||||
(b'e', 1),
|
(b'e', 1),
|
||||||
], group_by_byte(self.file_symlink.read()))
|
], group_by_byte(self.file_symlink.read()))
|
||||||
|
|
||||||
|
manifest_body = self.file_symlink.read(parms={
|
||||||
|
'multipart-manifest': 'get'})
|
||||||
|
self.assertEqual(
|
||||||
|
[seg['hash'] for seg in json.loads(manifest_body)],
|
||||||
|
[self.env.seg_info['seg_%s' % c]['etag'] for c in 'abcde'])
|
||||||
|
|
||||||
|
for obj_info in self.env.container.files(parms={'format': 'json'}):
|
||||||
|
if obj_info['name'] == self.file_symlink.name:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.fail('Unable to find file_symlink in listing.')
|
||||||
|
obj_info.pop('last_modified')
|
||||||
|
self.assertEqual(obj_info, {
|
||||||
|
'name': self.file_symlink.name,
|
||||||
|
'content_type': 'application/octet-stream',
|
||||||
|
'hash': 'd41d8cd98f00b204e9800998ecf8427e',
|
||||||
|
'bytes': 0,
|
||||||
|
'symlink_path': '/v1/%s/%s/manifest-abcde' % (
|
||||||
|
self.account_name, self.env.container.name),
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_static_link_target_slo_manifest(self):
|
||||||
|
manifest_info = self.env.container2.file(
|
||||||
|
"manifest-abcde").info(parms={
|
||||||
|
'multipart-manifest': 'get'})
|
||||||
|
manifest_etag = manifest_info['etag']
|
||||||
|
self.file_symlink.write(hdrs={
|
||||||
|
'X-Symlink-Target': '%s/%s' % (
|
||||||
|
self.env.container2.name, 'manifest-abcde'),
|
||||||
|
'X-Symlink-Target-Etag': manifest_etag,
|
||||||
|
})
|
||||||
|
self.assertEqual([
|
||||||
|
(b'a', 1024 * 1024),
|
||||||
|
(b'b', 1024 * 1024),
|
||||||
|
(b'c', 1024 * 1024),
|
||||||
|
(b'd', 1024 * 1024),
|
||||||
|
(b'e', 1),
|
||||||
|
], group_by_byte(self.file_symlink.read()))
|
||||||
|
|
||||||
|
manifest_body = self.file_symlink.read(parms={
|
||||||
|
'multipart-manifest': 'get'})
|
||||||
|
self.assertEqual(
|
||||||
|
[seg['hash'] for seg in json.loads(manifest_body)],
|
||||||
|
[self.env.seg_info['seg_%s' % c]['etag'] for c in 'abcde'])
|
||||||
|
|
||||||
|
# check listing
|
||||||
|
for obj_info in self.env.container.files(parms={'format': 'json'}):
|
||||||
|
if obj_info['name'] == self.file_symlink.name:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.fail('Unable to find file_symlink in listing.')
|
||||||
|
obj_info.pop('last_modified')
|
||||||
|
self.maxDiff = None
|
||||||
|
slo_info = self.env.container2.file("manifest-abcde").info()
|
||||||
|
self.assertEqual(obj_info, {
|
||||||
|
'name': self.file_symlink.name,
|
||||||
|
'content_type': 'application/octet-stream',
|
||||||
|
'hash': u'd41d8cd98f00b204e9800998ecf8427e',
|
||||||
|
'bytes': 0,
|
||||||
|
'slo_etag': slo_info['etag'],
|
||||||
|
'symlink_path': '/v1/%s/%s/manifest-abcde' % (
|
||||||
|
self.account_name, self.env.container2.name),
|
||||||
|
'symlink_bytes': 4 * 2 ** 20 + 1,
|
||||||
|
'symlink_etag': manifest_etag,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_static_link_target_slo_manifest_wrong_etag(self):
|
||||||
|
# try the slo "etag"
|
||||||
|
slo_etag = self.env.container2.file(
|
||||||
|
"manifest-abcde").info()['etag']
|
||||||
|
self.assertRaises(ResponseError, self.file_symlink.write, hdrs={
|
||||||
|
'X-Symlink-Target': '%s/%s' % (
|
||||||
|
self.env.container2.name, 'manifest-abcde'),
|
||||||
|
'X-Symlink-Target-Etag': slo_etag,
|
||||||
|
})
|
||||||
|
self.assert_status(400) # no quotes allowed!
|
||||||
|
|
||||||
|
# try the slo etag w/o the quotes
|
||||||
|
slo_etag = slo_etag.strip('"')
|
||||||
|
self.assertRaises(ResponseError, self.file_symlink.write, hdrs={
|
||||||
|
'X-Symlink-Target': '%s/%s' % (
|
||||||
|
self.env.container2.name, 'manifest-abcde'),
|
||||||
|
'X-Symlink-Target-Etag': slo_etag,
|
||||||
|
})
|
||||||
|
self.assert_status(409) # that just doesn't match
|
||||||
|
|
||||||
|
def test_static_link_target_symlink_to_slo_manifest(self):
|
||||||
|
# write symlink
|
||||||
|
self.file_symlink.write(hdrs={'X-Symlink-Target':
|
||||||
|
'%s/%s' % (self.env.container.name,
|
||||||
|
'manifest-abcde')})
|
||||||
|
# write static_link
|
||||||
|
file_static_link = self.env.container.file(uuid4().hex)
|
||||||
|
file_static_link.write(hdrs={
|
||||||
|
'X-Symlink-Target': '%s/%s' % (
|
||||||
|
self.file_symlink.container, self.file_symlink.name),
|
||||||
|
'X-Symlink-Target-Etag': MD5_OF_EMPTY_STRING,
|
||||||
|
})
|
||||||
|
|
||||||
|
# validate reads
|
||||||
|
self.assertEqual([
|
||||||
|
(b'a', 1024 * 1024),
|
||||||
|
(b'b', 1024 * 1024),
|
||||||
|
(b'c', 1024 * 1024),
|
||||||
|
(b'd', 1024 * 1024),
|
||||||
|
(b'e', 1),
|
||||||
|
], group_by_byte(file_static_link.read()))
|
||||||
|
|
||||||
|
manifest_body = file_static_link.read(parms={
|
||||||
|
'multipart-manifest': 'get'})
|
||||||
|
self.assertEqual(
|
||||||
|
[seg['hash'] for seg in json.loads(manifest_body)],
|
||||||
|
[self.env.seg_info['seg_%s' % c]['etag'] for c in 'abcde'])
|
||||||
|
|
||||||
|
# check listing
|
||||||
|
for obj_info in self.env.container.files(parms={'format': 'json'}):
|
||||||
|
if obj_info['name'] == file_static_link.name:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.fail('Unable to find file_symlink in listing.')
|
||||||
|
obj_info.pop('last_modified')
|
||||||
|
self.maxDiff = None
|
||||||
|
self.assertEqual(obj_info, {
|
||||||
|
'name': file_static_link.name,
|
||||||
|
'content_type': 'application/octet-stream',
|
||||||
|
'hash': 'd41d8cd98f00b204e9800998ecf8427e',
|
||||||
|
'bytes': 0,
|
||||||
|
'symlink_path': u'/v1/%s/%s/%s' % (
|
||||||
|
self.account_name, self.file_symlink.container,
|
||||||
|
self.file_symlink.name),
|
||||||
|
# the only time bytes/etag aren't the target object are when they
|
||||||
|
# validate through another static_link
|
||||||
|
'symlink_bytes': 0,
|
||||||
|
'symlink_etag': MD5_OF_EMPTY_STRING,
|
||||||
|
})
|
||||||
|
|
||||||
def test_symlink_target_slo_nested_manifest(self):
|
def test_symlink_target_slo_nested_manifest(self):
|
||||||
self.file_symlink.write(hdrs={'X-Symlink-Target':
|
self.file_symlink.write(hdrs={'X-Symlink-Target':
|
||||||
'%s/%s' % (self.env.container.name,
|
'%s/%s' % (self.env.container.name,
|
||||||
|
@ -24,7 +24,7 @@ from swift.common import swob
|
|||||||
from swift.common.middleware import symlink, copy, versioned_writes, \
|
from swift.common.middleware import symlink, copy, versioned_writes, \
|
||||||
listing_formats
|
listing_formats
|
||||||
from swift.common.swob import Request
|
from swift.common.swob import Request
|
||||||
from swift.common.utils import MD5_OF_EMPTY_STRING
|
from swift.common.utils import MD5_OF_EMPTY_STRING, get_swift_info
|
||||||
from test.unit.common.middleware.helpers import FakeSwift
|
from test.unit.common.middleware.helpers import FakeSwift
|
||||||
from test.unit.common.middleware.test_versioned_writes import FakeCache
|
from test.unit.common.middleware.test_versioned_writes import FakeCache
|
||||||
|
|
||||||
@ -78,6 +78,14 @@ class TestSymlinkMiddlewareBase(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class TestSymlinkMiddleware(TestSymlinkMiddlewareBase):
|
class TestSymlinkMiddleware(TestSymlinkMiddlewareBase):
|
||||||
|
|
||||||
|
def test_symlink_info(self):
|
||||||
|
swift_info = get_swift_info()
|
||||||
|
self.assertEqual(swift_info['symlink'], {
|
||||||
|
'symloop_max': 2,
|
||||||
|
'static_links': True,
|
||||||
|
})
|
||||||
|
|
||||||
def test_symlink_simple_put(self):
|
def test_symlink_simple_put(self):
|
||||||
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
|
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
|
||||||
req = Request.blank('/v1/a/c/symlink', method='PUT',
|
req = Request.blank('/v1/a/c/symlink', method='PUT',
|
||||||
@ -91,6 +99,171 @@ class TestSymlinkMiddleware(TestSymlinkMiddlewareBase):
|
|||||||
self.assertNotIn('X-Object-Sysmeta-Symlink-Target-Account', hdrs)
|
self.assertNotIn('X-Object-Sysmeta-Symlink-Target-Account', hdrs)
|
||||||
val = hdrs.get('X-Object-Sysmeta-Container-Update-Override-Etag')
|
val = hdrs.get('X-Object-Sysmeta-Container-Update-Override-Etag')
|
||||||
self.assertEqual(val, '%s; symlink_target=c1/o' % MD5_OF_EMPTY_STRING)
|
self.assertEqual(val, '%s; symlink_target=c1/o' % MD5_OF_EMPTY_STRING)
|
||||||
|
self.assertEqual('application/symlink', hdrs.get('Content-Type'))
|
||||||
|
|
||||||
|
def test_symlink_simple_put_with_content_type(self):
|
||||||
|
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
|
||||||
|
req = Request.blank('/v1/a/c/symlink', method='PUT',
|
||||||
|
headers={'X-Symlink-Target': 'c1/o',
|
||||||
|
'Content-Type': 'application/linkyfoo'},
|
||||||
|
body='')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '201 Created')
|
||||||
|
method, path, hdrs = self.app.calls_with_headers[0]
|
||||||
|
val = hdrs.get('X-Object-Sysmeta-Symlink-Target')
|
||||||
|
self.assertEqual(val, 'c1/o')
|
||||||
|
self.assertNotIn('X-Object-Sysmeta-Symlink-Target-Account', hdrs)
|
||||||
|
val = hdrs.get('X-Object-Sysmeta-Container-Update-Override-Etag')
|
||||||
|
self.assertEqual(val, '%s; symlink_target=c1/o' % MD5_OF_EMPTY_STRING)
|
||||||
|
self.assertEqual('application/linkyfoo', hdrs.get('Content-Type'))
|
||||||
|
|
||||||
|
def test_symlink_simple_put_with_etag(self):
|
||||||
|
self.app.register('HEAD', '/v1/a/c1/o', swob.HTTPOk, {
|
||||||
|
'Etag': 'tgt-etag', 'Content-Length': 42,
|
||||||
|
'Content-Type': 'application/foo'})
|
||||||
|
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
|
||||||
|
req = Request.blank('/v1/a/c/symlink', method='PUT',
|
||||||
|
headers={
|
||||||
|
'X-Symlink-Target': 'c1/o',
|
||||||
|
'X-Symlink-Target-Etag': 'tgt-etag',
|
||||||
|
}, body='')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '201 Created')
|
||||||
|
method, path, hdrs = self.app.calls_with_headers[1]
|
||||||
|
val = hdrs.get('X-Object-Sysmeta-Symlink-Target')
|
||||||
|
self.assertEqual(val, 'c1/o')
|
||||||
|
self.assertNotIn('X-Object-Sysmeta-Symlink-Target-Account', hdrs)
|
||||||
|
val = hdrs.get('X-Object-Sysmeta-Container-Update-Override-Etag')
|
||||||
|
self.assertEqual(val, '%s; symlink_target=c1/o; '
|
||||||
|
'symlink_target_etag=tgt-etag; '
|
||||||
|
'symlink_target_bytes=42' % MD5_OF_EMPTY_STRING)
|
||||||
|
self.assertEqual([
|
||||||
|
('HEAD', '/v1/a/c1/o'),
|
||||||
|
('PUT', '/v1/a/c/symlink'),
|
||||||
|
], self.app.calls)
|
||||||
|
self.assertEqual('application/foo',
|
||||||
|
self.app._calls[-1].headers['Content-Type'])
|
||||||
|
|
||||||
|
def test_symlink_simple_put_with_etag_target_missing_content_type(self):
|
||||||
|
self.app.register('HEAD', '/v1/a/c1/o', swob.HTTPOk, {
|
||||||
|
'Etag': 'tgt-etag', 'Content-Length': 42})
|
||||||
|
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
|
||||||
|
req = Request.blank('/v1/a/c/symlink', method='PUT',
|
||||||
|
headers={
|
||||||
|
'X-Symlink-Target': 'c1/o',
|
||||||
|
'X-Symlink-Target-Etag': 'tgt-etag',
|
||||||
|
}, body='')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '201 Created')
|
||||||
|
method, path, hdrs = self.app.calls_with_headers[1]
|
||||||
|
val = hdrs.get('X-Object-Sysmeta-Symlink-Target')
|
||||||
|
self.assertEqual(val, 'c1/o')
|
||||||
|
self.assertNotIn('X-Object-Sysmeta-Symlink-Target-Account', hdrs)
|
||||||
|
val = hdrs.get('X-Object-Sysmeta-Container-Update-Override-Etag')
|
||||||
|
self.assertEqual(val, '%s; symlink_target=c1/o; '
|
||||||
|
'symlink_target_etag=tgt-etag; '
|
||||||
|
'symlink_target_bytes=42' % MD5_OF_EMPTY_STRING)
|
||||||
|
self.assertEqual([
|
||||||
|
('HEAD', '/v1/a/c1/o'),
|
||||||
|
('PUT', '/v1/a/c/symlink'),
|
||||||
|
], self.app.calls)
|
||||||
|
# N.B. the ObjectController would call _update_content_type on PUT
|
||||||
|
# regardless, but you actually can't get a HEAD response without swob
|
||||||
|
# setting a Content-Type
|
||||||
|
self.assertEqual('text/html; charset=UTF-8',
|
||||||
|
self.app._calls[-1].headers['Content-Type'])
|
||||||
|
|
||||||
|
def test_symlink_simple_put_with_etag_explicit_content_type(self):
|
||||||
|
self.app.register('HEAD', '/v1/a/c1/o', swob.HTTPOk, {
|
||||||
|
'Etag': 'tgt-etag', 'Content-Length': 42,
|
||||||
|
'Content-Type': 'application/foo'})
|
||||||
|
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
|
||||||
|
req = Request.blank('/v1/a/c/symlink', method='PUT',
|
||||||
|
headers={
|
||||||
|
'X-Symlink-Target': 'c1/o',
|
||||||
|
'X-Symlink-Target-Etag': 'tgt-etag',
|
||||||
|
'Content-Type': 'application/bar',
|
||||||
|
}, body='')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '201 Created')
|
||||||
|
method, path, hdrs = self.app.calls_with_headers[1]
|
||||||
|
val = hdrs.get('X-Object-Sysmeta-Symlink-Target')
|
||||||
|
self.assertEqual(val, 'c1/o')
|
||||||
|
self.assertNotIn('X-Object-Sysmeta-Symlink-Target-Account', hdrs)
|
||||||
|
val = hdrs.get('X-Object-Sysmeta-Container-Update-Override-Etag')
|
||||||
|
self.assertEqual(val, '%s; symlink_target=c1/o; '
|
||||||
|
'symlink_target_etag=tgt-etag; '
|
||||||
|
'symlink_target_bytes=42' % MD5_OF_EMPTY_STRING)
|
||||||
|
self.assertEqual([
|
||||||
|
('HEAD', '/v1/a/c1/o'),
|
||||||
|
('PUT', '/v1/a/c/symlink'),
|
||||||
|
], self.app.calls)
|
||||||
|
self.assertEqual('application/bar',
|
||||||
|
self.app._calls[-1].headers['Content-Type'])
|
||||||
|
|
||||||
|
def test_symlink_simple_put_with_unmatched_etag(self):
|
||||||
|
self.app.register('HEAD', '/v1/a/c1/o', swob.HTTPOk, {
|
||||||
|
'Etag': 'tgt-etag', 'Content-Length': 42})
|
||||||
|
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
|
||||||
|
req = Request.blank('/v1/a/c/symlink', method='PUT',
|
||||||
|
headers={
|
||||||
|
'X-Symlink-Target': 'c1/o',
|
||||||
|
'X-Symlink-Target-Etag': 'not-tgt-etag',
|
||||||
|
}, body='')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '409 Conflict')
|
||||||
|
self.assertIn(('Content-Location', '/v1/a/c1/o'), headers)
|
||||||
|
self.assertEqual(body, b"Object Etag 'tgt-etag' does not match "
|
||||||
|
b"X-Symlink-Target-Etag header 'not-tgt-etag'")
|
||||||
|
|
||||||
|
def test_symlink_simple_put_to_non_existing_object(self):
|
||||||
|
self.app.register('HEAD', '/v1/a/c1/o', swob.HTTPNotFound, {})
|
||||||
|
req = Request.blank('/v1/a/c/symlink', method='PUT',
|
||||||
|
headers={
|
||||||
|
'X-Symlink-Target': 'c1/o',
|
||||||
|
'X-Symlink-Target-Etag': 'not-tgt-etag',
|
||||||
|
}, body='')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '409 Conflict')
|
||||||
|
self.assertIn(('Content-Location', '/v1/a/c1/o'), headers)
|
||||||
|
self.assertIn(b'does not exist', body)
|
||||||
|
|
||||||
|
def test_symlink_put_with_prevalidated_etag(self):
|
||||||
|
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
|
||||||
|
req = Request.blank('/v1/a/c/symlink', method='PUT', headers={
|
||||||
|
'X-Symlink-Target': 'c1/o',
|
||||||
|
'X-Object-Sysmeta-Symlink-Target-Etag': 'tgt-etag',
|
||||||
|
'X-Object-Sysmeta-Symlink-Target-Bytes': '13',
|
||||||
|
'Content-Type': 'application/foo',
|
||||||
|
}, body='')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '201 Created')
|
||||||
|
|
||||||
|
self.assertEqual([
|
||||||
|
# N.B. no HEAD!
|
||||||
|
('PUT', '/v1/a/c/symlink'),
|
||||||
|
], self.app.calls)
|
||||||
|
self.assertEqual('application/foo',
|
||||||
|
self.app._calls[-1].headers['Content-Type'])
|
||||||
|
|
||||||
|
method, path, hdrs = self.app.calls_with_headers[0]
|
||||||
|
val = hdrs.get('X-Object-Sysmeta-Symlink-Target')
|
||||||
|
self.assertEqual(val, 'c1/o')
|
||||||
|
self.assertNotIn('X-Object-Sysmeta-Symlink-Target-Account', hdrs)
|
||||||
|
val = hdrs.get('X-Object-Sysmeta-Container-Update-Override-Etag')
|
||||||
|
self.assertEqual(val, '%s; symlink_target=c1/o; '
|
||||||
|
'symlink_target_etag=tgt-etag; '
|
||||||
|
'symlink_target_bytes=13' % MD5_OF_EMPTY_STRING)
|
||||||
|
|
||||||
|
def test_symlink_put_with_prevalidated_etag_sysmeta_incomplete(self):
|
||||||
|
req = Request.blank('/v1/a/c/symlink', method='PUT', headers={
|
||||||
|
'X-Symlink-Target': 'c1/o',
|
||||||
|
'X-Object-Sysmeta-Symlink-Target-Etag': 'tgt-etag',
|
||||||
|
}, body='')
|
||||||
|
with self.assertRaises(KeyError) as cm:
|
||||||
|
self.call_sym(req)
|
||||||
|
self.assertEqual(cm.exception.args[0], swob.header_to_environ_key(
|
||||||
|
'X-Object-Sysmeta-Symlink-Target-Bytes'))
|
||||||
|
|
||||||
def test_symlink_chunked_put(self):
|
def test_symlink_chunked_put(self):
|
||||||
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
|
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
|
||||||
@ -274,6 +447,64 @@ class TestSymlinkMiddleware(TestSymlinkMiddlewareBase):
|
|||||||
self.assertNotIn('X-Symlink-Target-Account', dict(headers))
|
self.assertNotIn('X-Symlink-Target-Account', dict(headers))
|
||||||
self.assertNotIn('Content-Location', dict(headers))
|
self.assertNotIn('Content-Location', dict(headers))
|
||||||
|
|
||||||
|
def test_get_static_link_mismatched_etag(self):
|
||||||
|
self.app.register('GET', '/v1/a/c/symlink', swob.HTTPOk,
|
||||||
|
{'X-Object-Sysmeta-Symlink-Target': 'c1/o',
|
||||||
|
'X-Object-Sysmeta-Symlink-Target-Etag': 'the-etag'})
|
||||||
|
# apparently target object was overwritten
|
||||||
|
self.app.register('GET', '/v1/a/c1/o', swob.HTTPOk,
|
||||||
|
{'ETag': 'not-the-etag'}, 'resp_body')
|
||||||
|
req = Request.blank('/v1/a/c/symlink', method='GET')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '409 Conflict')
|
||||||
|
self.assertEqual(body, b"Object Etag 'not-the-etag' does not "
|
||||||
|
b"match X-Symlink-Target-Etag header 'the-etag'")
|
||||||
|
|
||||||
|
def test_get_static_link_to_symlink(self):
|
||||||
|
self.app.register('GET', '/v1/a/c/static_link', swob.HTTPOk,
|
||||||
|
{'X-Object-Sysmeta-Symlink-Target': 'c/symlink',
|
||||||
|
'X-Object-Sysmeta-Symlink-Target-Etag': 'the-etag'})
|
||||||
|
self.app.register('GET', '/v1/a/c/symlink', swob.HTTPOk,
|
||||||
|
{'ETag': 'the-etag',
|
||||||
|
'X-Object-Sysmeta-Symlink-Target': 'c1/o'})
|
||||||
|
self.app.register('GET', '/v1/a/c1/o', swob.HTTPOk,
|
||||||
|
{'ETag': 'not-the-etag'}, 'resp_body')
|
||||||
|
req = Request.blank('/v1/a/c/static_link', method='GET')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '200 OK')
|
||||||
|
|
||||||
|
def test_get_static_link_to_symlink_fails(self):
|
||||||
|
self.app.register('GET', '/v1/a/c/static_link', swob.HTTPOk,
|
||||||
|
{'X-Object-Sysmeta-Symlink-Target': 'c/symlink',
|
||||||
|
'X-Object-Sysmeta-Symlink-Target-Etag': 'the-etag'})
|
||||||
|
self.app.register('GET', '/v1/a/c/symlink', swob.HTTPOk,
|
||||||
|
{'ETag': 'not-the-etag',
|
||||||
|
'X-Object-Sysmeta-Symlink-Target': 'c1/o'})
|
||||||
|
req = Request.blank('/v1/a/c/static_link', method='GET')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '409 Conflict')
|
||||||
|
self.assertEqual(body, b"X-Symlink-Target-Etag headers do not match")
|
||||||
|
|
||||||
|
def put_static_link_to_symlink(self):
|
||||||
|
self.app.register('HEAD', '/v1/a/c/symlink', swob.HTTPOk,
|
||||||
|
{'ETag': 'symlink-etag',
|
||||||
|
'X-Object-Sysmeta-Symlink-Target': 'c/o',
|
||||||
|
'Content-Type': 'application/symlink'})
|
||||||
|
self.app.register('HEAD', '/v1/a/c/o', swob.HTTPOk,
|
||||||
|
{'ETag': 'tgt-etag',
|
||||||
|
'Content-Type': 'application/data'}, 'resp_body')
|
||||||
|
self.app.register('PUT', '/v1/a/c/static_link', swob.HTTPCreated, {})
|
||||||
|
req = Request.blank('/v1/a/c/static_link', method='PUT',
|
||||||
|
headers={
|
||||||
|
'X-Symlink-Target': 'c/symlink',
|
||||||
|
'X-Symlink-Target-Etag': 'symlink-etag',
|
||||||
|
}, body='')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '201 Created')
|
||||||
|
self.assertEqual([], self.app.calls)
|
||||||
|
self.assertEqual('application/data',
|
||||||
|
self.app._calls[-1].headers['Content-Type'])
|
||||||
|
|
||||||
def test_head_symlink(self):
|
def test_head_symlink(self):
|
||||||
self.app.register('HEAD', '/v1/a/c/symlink', swob.HTTPOk,
|
self.app.register('HEAD', '/v1/a/c/symlink', swob.HTTPOk,
|
||||||
{'X-Object-Sysmeta-Symlink-Target': 'c1/o',
|
{'X-Object-Sysmeta-Symlink-Target': 'c1/o',
|
||||||
@ -324,15 +555,21 @@ class TestSymlinkMiddleware(TestSymlinkMiddlewareBase):
|
|||||||
self.assertFalse(calls[2:])
|
self.assertFalse(calls[2:])
|
||||||
|
|
||||||
def test_symlink_too_deep(self):
|
def test_symlink_too_deep(self):
|
||||||
self.app.register('HEAD', '/v1/a/c/symlink', swob.HTTPOk,
|
self.app.register('GET', '/v1/a/c/symlink', swob.HTTPOk,
|
||||||
{'X-Object-Sysmeta-Symlink-Target': 'c/sym1'})
|
{'X-Object-Sysmeta-Symlink-Target': 'c/sym1'})
|
||||||
self.app.register('HEAD', '/v1/a/c/sym1', swob.HTTPOk,
|
self.app.register('GET', '/v1/a/c/sym1', swob.HTTPOk,
|
||||||
{'X-Object-Sysmeta-Symlink-Target': 'c/sym2'})
|
{'X-Object-Sysmeta-Symlink-Target': 'c/sym2'})
|
||||||
self.app.register('HEAD', '/v1/a/c/sym2', swob.HTTPOk,
|
self.app.register('GET', '/v1/a/c/sym2', swob.HTTPOk,
|
||||||
{'X-Object-Sysmeta-Symlink-Target': 'c/o'})
|
{'X-Object-Sysmeta-Symlink-Target': 'c/o'})
|
||||||
req = Request.blank('/v1/a/c/symlink', method='HEAD')
|
req = Request.blank('/v1/a/c/symlink', method='HEAD')
|
||||||
status, headers, body = self.call_sym(req)
|
status, headers, body = self.call_sym(req)
|
||||||
self.assertEqual(status, '409 Conflict')
|
self.assertEqual(status, '409 Conflict')
|
||||||
|
self.assertEqual(body, b'')
|
||||||
|
req = Request.blank('/v1/a/c/symlink')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '409 Conflict')
|
||||||
|
self.assertEqual(body, b'Too many levels of symbolic links, '
|
||||||
|
b'maximum allowed is 2')
|
||||||
|
|
||||||
def test_symlink_change_symloopmax(self):
|
def test_symlink_change_symloopmax(self):
|
||||||
# similar test to test_symlink_too_deep, but now changed the limit to 3
|
# similar test to test_symlink_too_deep, but now changed the limit to 3
|
||||||
@ -691,6 +928,145 @@ class SymlinkCopyingTestCase(TestSymlinkMiddlewareBase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
hdrs.get('X-Object-Sysmeta-Symlink-Target-Account'), 'a2')
|
hdrs.get('X-Object-Sysmeta-Symlink-Target-Account'), 'a2')
|
||||||
|
|
||||||
|
def test_static_link_to_new_slo_manifest(self):
|
||||||
|
self.app.register('HEAD', '/v1/a/c1/o', swob.HTTPOk, {
|
||||||
|
'X-Static-Large-Object': 'True',
|
||||||
|
'Etag': 'manifest-etag',
|
||||||
|
'X-Object-Sysmeta-Slo-Size': '1048576',
|
||||||
|
'X-Object-Sysmeta-Slo-Etag': 'this-is-not-used',
|
||||||
|
'Content-Length': 42,
|
||||||
|
'Content-Type': 'application/big-data',
|
||||||
|
'X-Object-Sysmeta-Container-Update-Override-Etag':
|
||||||
|
'956859738870e5ca6aa17eeda58e4df0; '
|
||||||
|
'slo_etag=71e938d37c1d06dc634dd24660255a88',
|
||||||
|
|
||||||
|
})
|
||||||
|
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
|
||||||
|
req = Request.blank('/v1/a/c/symlink', method='PUT',
|
||||||
|
headers={
|
||||||
|
'X-Symlink-Target': 'c1/o',
|
||||||
|
'X-Symlink-Target-Etag': 'manifest-etag',
|
||||||
|
}, body='')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '201 Created')
|
||||||
|
self.assertEqual([
|
||||||
|
('HEAD', '/v1/a/c1/o'),
|
||||||
|
('PUT', '/v1/a/c/symlink'),
|
||||||
|
], self.app.calls)
|
||||||
|
method, path, hdrs = self.app.calls_with_headers[-1]
|
||||||
|
self.assertEqual('application/big-data', hdrs['Content-Type'])
|
||||||
|
self.assertEqual(hdrs['X-Object-Sysmeta-Symlink-Target'], 'c1/o')
|
||||||
|
self.assertEqual(hdrs['X-Object-Sysmeta-Symlink-Target-Etag'],
|
||||||
|
'manifest-etag')
|
||||||
|
self.assertEqual(hdrs['X-Object-Sysmeta-Symlink-Target-Bytes'],
|
||||||
|
'1048576')
|
||||||
|
self.assertEqual(
|
||||||
|
hdrs['X-Object-Sysmeta-Container-Update-Override-Etag'],
|
||||||
|
'd41d8cd98f00b204e9800998ecf8427e; '
|
||||||
|
'slo_etag=71e938d37c1d06dc634dd24660255a88; '
|
||||||
|
'symlink_target=c1/o; '
|
||||||
|
'symlink_target_etag=manifest-etag; '
|
||||||
|
'symlink_target_bytes=1048576')
|
||||||
|
|
||||||
|
def test_static_link_to_old_slo_manifest(self):
|
||||||
|
self.app.register('HEAD', '/v1/a/c1/o', swob.HTTPOk, {
|
||||||
|
'X-Static-Large-Object': 'True',
|
||||||
|
'Etag': 'manifest-etag',
|
||||||
|
'X-Object-Sysmeta-Slo-Size': '1048576',
|
||||||
|
'X-Object-Sysmeta-Slo-Etag': '71e938d37c1d06dc634dd24660255a88',
|
||||||
|
'Content-Length': 42,
|
||||||
|
'Content-Type': 'application/big-data',
|
||||||
|
|
||||||
|
})
|
||||||
|
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
|
||||||
|
req = Request.blank('/v1/a/c/symlink', method='PUT',
|
||||||
|
headers={
|
||||||
|
'X-Symlink-Target': 'c1/o',
|
||||||
|
'X-Symlink-Target-Etag': 'manifest-etag',
|
||||||
|
}, body='')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '201 Created')
|
||||||
|
self.assertEqual([
|
||||||
|
('HEAD', '/v1/a/c1/o'),
|
||||||
|
('PUT', '/v1/a/c/symlink'),
|
||||||
|
], self.app.calls)
|
||||||
|
method, path, hdrs = self.app.calls_with_headers[-1]
|
||||||
|
self.assertEqual('application/big-data', hdrs['Content-Type'])
|
||||||
|
self.assertEqual(hdrs['X-Object-Sysmeta-Symlink-Target'], 'c1/o')
|
||||||
|
self.assertEqual(hdrs['X-Object-Sysmeta-Symlink-Target-Etag'],
|
||||||
|
'manifest-etag')
|
||||||
|
self.assertEqual(hdrs['X-Object-Sysmeta-Symlink-Target-Bytes'],
|
||||||
|
'1048576')
|
||||||
|
self.assertEqual(
|
||||||
|
hdrs['X-Object-Sysmeta-Container-Update-Override-Etag'],
|
||||||
|
'd41d8cd98f00b204e9800998ecf8427e; '
|
||||||
|
'slo_etag=71e938d37c1d06dc634dd24660255a88; '
|
||||||
|
'symlink_target=c1/o; '
|
||||||
|
'symlink_target_etag=manifest-etag; '
|
||||||
|
'symlink_target_bytes=1048576')
|
||||||
|
|
||||||
|
def test_static_link_to_really_old_slo_manifest(self):
|
||||||
|
self.app.register('HEAD', '/v1/a/c1/o', swob.HTTPOk, {
|
||||||
|
'X-Static-Large-Object': 'True',
|
||||||
|
'Etag': 'manifest-etag',
|
||||||
|
'Content-Length': 42,
|
||||||
|
'Content-Type': 'application/big-data',
|
||||||
|
|
||||||
|
})
|
||||||
|
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
|
||||||
|
req = Request.blank('/v1/a/c/symlink', method='PUT',
|
||||||
|
headers={
|
||||||
|
'X-Symlink-Target': 'c1/o',
|
||||||
|
'X-Symlink-Target-Etag': 'manifest-etag',
|
||||||
|
}, body='')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '201 Created')
|
||||||
|
self.assertEqual([
|
||||||
|
('HEAD', '/v1/a/c1/o'),
|
||||||
|
('PUT', '/v1/a/c/symlink'),
|
||||||
|
], self.app.calls)
|
||||||
|
method, path, hdrs = self.app.calls_with_headers[-1]
|
||||||
|
self.assertEqual('application/big-data', hdrs['Content-Type'])
|
||||||
|
self.assertEqual(hdrs['X-Object-Sysmeta-Symlink-Target'], 'c1/o')
|
||||||
|
self.assertEqual(hdrs['X-Object-Sysmeta-Symlink-Target-Etag'],
|
||||||
|
'manifest-etag')
|
||||||
|
# symlink m/w is doing a HEAD, it's not going to going to read the
|
||||||
|
# manifest body and sum up the bytes - so we just use manifest size
|
||||||
|
self.assertEqual(hdrs['X-Object-Sysmeta-Symlink-Target-Bytes'],
|
||||||
|
'42')
|
||||||
|
# no slo_etag, and target_bytes is manifest
|
||||||
|
self.assertEqual(
|
||||||
|
hdrs['X-Object-Sysmeta-Container-Update-Override-Etag'],
|
||||||
|
'd41d8cd98f00b204e9800998ecf8427e; '
|
||||||
|
'symlink_target=c1/o; '
|
||||||
|
'symlink_target_etag=manifest-etag; '
|
||||||
|
'symlink_target_bytes=42')
|
||||||
|
|
||||||
|
def test_static_link_to_slo_manifest_slo_etag(self):
|
||||||
|
self.app.register('HEAD', '/v1/a/c1/o', swob.HTTPOk, {
|
||||||
|
'Etag': 'manifest-etag',
|
||||||
|
'X-Object-Sysmeta-Slo-Etag': 'slo-etag',
|
||||||
|
'Content-Length': 42,
|
||||||
|
})
|
||||||
|
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
|
||||||
|
# unquoted slo-etag doesn't match
|
||||||
|
req = Request.blank('/v1/a/c/symlink', method='PUT',
|
||||||
|
headers={
|
||||||
|
'X-Symlink-Target': 'c1/o',
|
||||||
|
'X-Symlink-Target-Etag': 'slo-etag',
|
||||||
|
}, body='')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '409 Conflict')
|
||||||
|
# the quoted slo-etag is just straight up invalid
|
||||||
|
req = Request.blank('/v1/a/c/symlink', method='PUT',
|
||||||
|
headers={
|
||||||
|
'X-Symlink-Target': 'c1/o',
|
||||||
|
'X-Symlink-Target-Etag': '"slo-etag"',
|
||||||
|
}, body='')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '400 Bad Request')
|
||||||
|
self.assertEqual(b'Bad X-Symlink-Target-Etag format', body)
|
||||||
|
|
||||||
|
|
||||||
class SymlinkVersioningTestCase(TestSymlinkMiddlewareBase):
|
class SymlinkVersioningTestCase(TestSymlinkMiddlewareBase):
|
||||||
# verify interaction of versioned_writes and symlink middlewares
|
# verify interaction of versioned_writes and symlink middlewares
|
||||||
@ -819,13 +1195,16 @@ class TestSymlinkContainerContext(TestSymlinkMiddlewareBase):
|
|||||||
def test_extract_symlink_path_json_symlink_path(self):
|
def test_extract_symlink_path_json_symlink_path(self):
|
||||||
obj_dict = {"bytes": 6,
|
obj_dict = {"bytes": 6,
|
||||||
"last_modified": "1",
|
"last_modified": "1",
|
||||||
"hash": "etag; symlink_target=c/o",
|
"hash": "etag; symlink_target=c/o; something_else=foo; "
|
||||||
|
"symlink_target_etag=tgt_etag; symlink_target_bytes=8",
|
||||||
"name": "obj",
|
"name": "obj",
|
||||||
"content_type": "application/octet-stream"}
|
"content_type": "application/octet-stream"}
|
||||||
obj_dict = self.context._extract_symlink_path_json(
|
obj_dict = self.context._extract_symlink_path_json(
|
||||||
obj_dict, 'v1', 'AUTH_a')
|
obj_dict, 'v1', 'AUTH_a')
|
||||||
self.assertEqual(obj_dict['hash'], 'etag')
|
self.assertEqual(obj_dict['hash'], 'etag; something_else=foo')
|
||||||
self.assertEqual(obj_dict['symlink_path'], '/v1/AUTH_a/c/o')
|
self.assertEqual(obj_dict['symlink_path'], '/v1/AUTH_a/c/o')
|
||||||
|
self.assertEqual(obj_dict['symlink_etag'], 'tgt_etag')
|
||||||
|
self.assertEqual(obj_dict['symlink_bytes'], 8)
|
||||||
|
|
||||||
def test_extract_symlink_path_json_symlink_path_and_account(self):
|
def test_extract_symlink_path_json_symlink_path_and_account(self):
|
||||||
obj_dict = {
|
obj_dict = {
|
||||||
|
@ -417,7 +417,7 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase):
|
|||||||
self.assertRequestEqual(req, self.authorized[1])
|
self.assertRequestEqual(req, self.authorized[1])
|
||||||
self.assertEqual(3, self.app.call_count)
|
self.assertEqual(3, self.app.call_count)
|
||||||
self.assertEqual([
|
self.assertEqual([
|
||||||
('GET', '/v1/a/c/o'),
|
('GET', '/v1/a/c/o?symlink=get'),
|
||||||
('PUT', '/v1/a/ver_cont/001o/0000000060.00000'),
|
('PUT', '/v1/a/ver_cont/001o/0000000060.00000'),
|
||||||
('PUT', '/v1/a/c/o'),
|
('PUT', '/v1/a/c/o'),
|
||||||
], self.app.calls)
|
], self.app.calls)
|
||||||
@ -449,7 +449,7 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase):
|
|||||||
self.assertRequestEqual(req, self.authorized[1])
|
self.assertRequestEqual(req, self.authorized[1])
|
||||||
self.assertEqual(3, self.app.call_count)
|
self.assertEqual(3, self.app.call_count)
|
||||||
self.assertEqual([
|
self.assertEqual([
|
||||||
('GET', '/v1/a/c/o'),
|
('GET', '/v1/a/c/o?symlink=get'),
|
||||||
('PUT', '/v1/a/ver_cont/001o/0000003600.00000'),
|
('PUT', '/v1/a/ver_cont/001o/0000003600.00000'),
|
||||||
('PUT', '/v1/a/c/o'),
|
('PUT', '/v1/a/c/o'),
|
||||||
], self.app.calls)
|
], self.app.calls)
|
||||||
@ -682,7 +682,7 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase):
|
|||||||
prefix_listing_prefix = '/v1/a/ver_cont?prefix=001o/&'
|
prefix_listing_prefix = '/v1/a/ver_cont?prefix=001o/&'
|
||||||
self.assertEqual(self.app.calls, [
|
self.assertEqual(self.app.calls, [
|
||||||
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
||||||
('GET', '/v1/a/ver_cont/001o/2'),
|
('GET', '/v1/a/ver_cont/001o/2?symlink=get'),
|
||||||
('PUT', '/v1/a/c/o'),
|
('PUT', '/v1/a/c/o'),
|
||||||
('DELETE', '/v1/a/ver_cont/001o/2'),
|
('DELETE', '/v1/a/ver_cont/001o/2'),
|
||||||
])
|
])
|
||||||
@ -777,7 +777,7 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase):
|
|||||||
self.assertEqual(self.app.calls, [
|
self.assertEqual(self.app.calls, [
|
||||||
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
||||||
('HEAD', '/v1/a/c/o'),
|
('HEAD', '/v1/a/c/o'),
|
||||||
('GET', '/v1/a/ver_cont/001o/1'),
|
('GET', '/v1/a/ver_cont/001o/1?symlink=get'),
|
||||||
('PUT', '/v1/a/c/o'),
|
('PUT', '/v1/a/c/o'),
|
||||||
('DELETE', '/v1/a/ver_cont/001o/1'),
|
('DELETE', '/v1/a/ver_cont/001o/1'),
|
||||||
('DELETE', '/v1/a/ver_cont/001o/2'),
|
('DELETE', '/v1/a/ver_cont/001o/2'),
|
||||||
@ -941,7 +941,7 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase):
|
|||||||
prefix_listing_prefix = '/v1/a/ver_cont?prefix=001o/&'
|
prefix_listing_prefix = '/v1/a/ver_cont?prefix=001o/&'
|
||||||
self.assertEqual(self.app.calls, [
|
self.assertEqual(self.app.calls, [
|
||||||
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
||||||
('GET', '/v1/a/ver_cont/001o/1'),
|
('GET', '/v1/a/ver_cont/001o/1?symlink=get'),
|
||||||
('PUT', '/v1/a/c/o'),
|
('PUT', '/v1/a/c/o'),
|
||||||
('DELETE', '/v1/a/ver_cont/001o/1'),
|
('DELETE', '/v1/a/ver_cont/001o/1'),
|
||||||
])
|
])
|
||||||
@ -989,8 +989,8 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase):
|
|||||||
prefix_listing_prefix = '/v1/a/ver_cont?prefix=001o/&'
|
prefix_listing_prefix = '/v1/a/ver_cont?prefix=001o/&'
|
||||||
self.assertEqual(self.app.calls, [
|
self.assertEqual(self.app.calls, [
|
||||||
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
||||||
('GET', '/v1/a/ver_cont/001o/2'),
|
('GET', '/v1/a/ver_cont/001o/2?symlink=get'),
|
||||||
('GET', '/v1/a/ver_cont/001o/1'),
|
('GET', '/v1/a/ver_cont/001o/1?symlink=get'),
|
||||||
('PUT', '/v1/a/c/o'),
|
('PUT', '/v1/a/c/o'),
|
||||||
('DELETE', '/v1/a/ver_cont/001o/1'),
|
('DELETE', '/v1/a/ver_cont/001o/1'),
|
||||||
])
|
])
|
||||||
@ -1114,7 +1114,7 @@ class VersionedWritesOldContainersTestCase(VersionedWritesBaseTestCase):
|
|||||||
self.assertEqual(self.app.calls, [
|
self.assertEqual(self.app.calls, [
|
||||||
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
||||||
('GET', prefix_listing_prefix + 'marker=001o/2'),
|
('GET', prefix_listing_prefix + 'marker=001o/2'),
|
||||||
('GET', '/v1/a/ver_cont/001o/2'),
|
('GET', '/v1/a/ver_cont/001o/2?symlink=get'),
|
||||||
('PUT', '/v1/a/c/o'),
|
('PUT', '/v1/a/c/o'),
|
||||||
('DELETE', '/v1/a/ver_cont/001o/2'),
|
('DELETE', '/v1/a/ver_cont/001o/2'),
|
||||||
])
|
])
|
||||||
@ -1167,8 +1167,8 @@ class VersionedWritesOldContainersTestCase(VersionedWritesBaseTestCase):
|
|||||||
self.assertEqual(self.app.calls, [
|
self.assertEqual(self.app.calls, [
|
||||||
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
||||||
('GET', prefix_listing_prefix + 'marker=001o/2'),
|
('GET', prefix_listing_prefix + 'marker=001o/2'),
|
||||||
('GET', '/v1/a/ver_cont/001o/2'),
|
('GET', '/v1/a/ver_cont/001o/2?symlink=get'),
|
||||||
('GET', '/v1/a/ver_cont/001o/1'),
|
('GET', '/v1/a/ver_cont/001o/1?symlink=get'),
|
||||||
('PUT', '/v1/a/c/o'),
|
('PUT', '/v1/a/c/o'),
|
||||||
('DELETE', '/v1/a/ver_cont/001o/1'),
|
('DELETE', '/v1/a/ver_cont/001o/1'),
|
||||||
])
|
])
|
||||||
@ -1282,14 +1282,14 @@ class VersionedWritesOldContainersTestCase(VersionedWritesBaseTestCase):
|
|||||||
prefix_listing_prefix = '/v1/a/ver_cont?prefix=001o/&'
|
prefix_listing_prefix = '/v1/a/ver_cont?prefix=001o/&'
|
||||||
self.assertEqual(self.app.calls, [
|
self.assertEqual(self.app.calls, [
|
||||||
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
||||||
('GET', '/v1/a/ver_cont/001o/4'),
|
('GET', '/v1/a/ver_cont/001o/4?symlink=get'),
|
||||||
('GET', '/v1/a/ver_cont/001o/3'),
|
('GET', '/v1/a/ver_cont/001o/3?symlink=get'),
|
||||||
('GET', '/v1/a/ver_cont/001o/2'),
|
('GET', '/v1/a/ver_cont/001o/2?symlink=get'),
|
||||||
('GET', prefix_listing_prefix + 'marker=001o/2&reverse=on'),
|
('GET', prefix_listing_prefix + 'marker=001o/2&reverse=on'),
|
||||||
('GET', prefix_listing_prefix + 'marker=&end_marker=001o/2'),
|
('GET', prefix_listing_prefix + 'marker=&end_marker=001o/2'),
|
||||||
('GET', prefix_listing_prefix + 'marker=001o/0&end_marker=001o/2'),
|
('GET', prefix_listing_prefix + 'marker=001o/0&end_marker=001o/2'),
|
||||||
('GET', prefix_listing_prefix + 'marker=001o/1&end_marker=001o/2'),
|
('GET', prefix_listing_prefix + 'marker=001o/1&end_marker=001o/2'),
|
||||||
('GET', '/v1/a/ver_cont/001o/1'),
|
('GET', '/v1/a/ver_cont/001o/1?symlink=get'),
|
||||||
('PUT', '/v1/a/c/o'),
|
('PUT', '/v1/a/c/o'),
|
||||||
('DELETE', '/v1/a/ver_cont/001o/1'),
|
('DELETE', '/v1/a/ver_cont/001o/1'),
|
||||||
])
|
])
|
||||||
@ -1354,13 +1354,13 @@ class VersionedWritesOldContainersTestCase(VersionedWritesBaseTestCase):
|
|||||||
prefix_listing_prefix = '/v1/a/ver_cont?prefix=001o/&'
|
prefix_listing_prefix = '/v1/a/ver_cont?prefix=001o/&'
|
||||||
self.assertEqual(self.app.calls, [
|
self.assertEqual(self.app.calls, [
|
||||||
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
||||||
('GET', '/v1/a/ver_cont/001o/4'),
|
('GET', '/v1/a/ver_cont/001o/4?symlink=get'),
|
||||||
('GET', '/v1/a/ver_cont/001o/3'),
|
('GET', '/v1/a/ver_cont/001o/3?symlink=get'),
|
||||||
('GET', prefix_listing_prefix + 'marker=001o/3&reverse=on'),
|
('GET', prefix_listing_prefix + 'marker=001o/3&reverse=on'),
|
||||||
('GET', prefix_listing_prefix + 'marker=&end_marker=001o/3'),
|
('GET', prefix_listing_prefix + 'marker=&end_marker=001o/3'),
|
||||||
('GET', prefix_listing_prefix + 'marker=001o/1&end_marker=001o/3'),
|
('GET', prefix_listing_prefix + 'marker=001o/1&end_marker=001o/3'),
|
||||||
('GET', prefix_listing_prefix + 'marker=001o/2&end_marker=001o/3'),
|
('GET', prefix_listing_prefix + 'marker=001o/2&end_marker=001o/3'),
|
||||||
('GET', '/v1/a/ver_cont/001o/2'),
|
('GET', '/v1/a/ver_cont/001o/2?symlink=get'),
|
||||||
('PUT', '/v1/a/c/o'),
|
('PUT', '/v1/a/c/o'),
|
||||||
('DELETE', '/v1/a/ver_cont/001o/2'),
|
('DELETE', '/v1/a/ver_cont/001o/2'),
|
||||||
])
|
])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user