Allow "static symlinks"
... by embedding something like `If-Match: <etag>` semantics in the symlink. When creating a symlink, users may now specify an optional X-Symlink-Target-Etag header. If present, the etag of the final object returned to the client will be checked; if it does not match the X-Symlink-Target-Etag header, a 409 Conflict error will be returned to the client. Note that, unlike "dynamic symlink" behavior, the target object must exist with the matching Etag for the "static symlink" to be created. Since we're validating the Etag anyway, we also set the content-type of the symlink to match if the client didn't otherwise specifiy and send the etag & content-length along to the container listing as well. Bonus goodness: - Tighten assertions on Content-Location - Get rid of swift.source-sniffing by making versioned_writes symlink-aware ('cause I'm going to want to make it symlink-aware later anyway) - Allow middlewares left of symlink to set their own Container-Update-Override-Etag when creating a symlink - Set dynamic symlink content type if client doesn't supply something Co-Authored-By: Clay Gerrard <clay.gerrard@gmail.com> Change-Id: I179ea6180d31146bb947061c69b1807c59529ac8
This commit is contained in:
parent
a2a1ebe7a3
commit
1abc9c4f9d
@ -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
|
||||
object in the PUT request process.
|
||||
|
||||
Symlinks must be zero-byte objects. Attempting to PUT a symlink
|
||||
with a non-empty request body will result in a 400-series error. Also, POST
|
||||
with 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
|
||||
``Content-Type`` of symlink objects to a distinct value such as
|
||||
``application/symlink``.
|
||||
Symlinks must be zero-byte objects. Attempting to PUT a symlink with a
|
||||
non-empty request body will result in a 400-series error. Also, POST with
|
||||
``X-Symlink-Target`` header always results in a 400-series error. The target
|
||||
object need not exist at symlink creation time.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
limit ``symloop_max`` an error response will be produced. The value of
|
||||
until the target is not a symlink. If the number of chained symlinks exceeds
|
||||
the limit ``symloop_max`` an error response will be produced. The value 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.
|
||||
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
|
||||
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
|
||||
@ -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``
|
||||
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
|
||||
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
|
||||
symlink because object servers cannot know for sure if the current object is a
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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``
|
||||
for each symlink object in the container listing. The ``symlink_path`` value
|
||||
is the target path of the symlink. Clients can differentiate symlinks and
|
||||
other objects by this function. Note that responses of any other format
|
||||
(e.g.``?format=xml``) won't include ``symlink_path`` info.
|
||||
other objects by this function. Note that responses in any other format
|
||||
(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
|
||||
|
||||
@ -105,7 +144,10 @@ Errors
|
||||
* GET/HEAD traversing more than ``symloop_max`` chained symlinks will
|
||||
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
|
||||
@ -160,7 +202,7 @@ import os
|
||||
from cgi import parse_header
|
||||
|
||||
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.wsgi import WSGIContext, make_subrequest
|
||||
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, \
|
||||
HTTPException, HTTPConflict, HTTPPreconditionFailed, wsgi_quote, \
|
||||
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.header_key_dict import HeaderKeyDict
|
||||
|
||||
@ -176,22 +218,33 @@ DEFAULT_SYMLOOP_MAX = 2
|
||||
# Header values for symlink target path strings will be quoted values.
|
||||
TGT_OBJ_SYMLINK_HDR = 'x-symlink-target'
|
||||
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_ACCT_SYSMETA_SYMLINK_HDR = \
|
||||
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):
|
||||
"""
|
||||
Validate that the value from x-symlink-target header is
|
||||
well formatted. We assume the caller ensures that
|
||||
Validate that the value from x-symlink-target header is well formatted
|
||||
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.
|
||||
|
||||
: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
|
||||
is not well formatted.
|
||||
:raise: HTTPBadRequest if the x-symlink-target value points to the request
|
||||
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
|
||||
# copy middleware may accept the format. In the symlink, API
|
||||
@ -228,43 +281,48 @@ def _check_symlink_header(req):
|
||||
raise HTTPBadRequest(
|
||||
body='Symlink cannot target itself',
|
||||
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):
|
||||
"""
|
||||
Helper function to translate from X-Symlink-Target and
|
||||
X-Symlink-Target-Account to X-Object-Sysmeta-Symlink-Target
|
||||
and X-Object-Sysmeta-Symlink-Target-Account.
|
||||
Helper function to translate from client-facing X-Symlink-* headers
|
||||
to cluster-facing X-Object-Sysmeta-Symlink-* headers.
|
||||
|
||||
:param headers: request headers dict. Note that the headers dict
|
||||
will be updated directly.
|
||||
"""
|
||||
# To preseve url-encoded value in the symlink header, use raw value
|
||||
if TGT_OBJ_SYMLINK_HDR in headers:
|
||||
headers[TGT_OBJ_SYSMETA_SYMLINK_HDR] = headers.pop(
|
||||
TGT_OBJ_SYMLINK_HDR)
|
||||
|
||||
if TGT_ACCT_SYMLINK_HDR in headers:
|
||||
headers[TGT_ACCT_SYSMETA_SYMLINK_HDR] = headers.pop(
|
||||
TGT_ACCT_SYMLINK_HDR)
|
||||
for user_hdr, sysmeta_hdr in (
|
||||
(TGT_OBJ_SYMLINK_HDR, TGT_OBJ_SYSMETA_SYMLINK_HDR),
|
||||
(TGT_ACCT_SYMLINK_HDR, TGT_ACCT_SYSMETA_SYMLINK_HDR)):
|
||||
if user_hdr in headers:
|
||||
headers[sysmeta_hdr] = headers.pop(user_hdr)
|
||||
|
||||
|
||||
def symlink_sysmeta_to_usermeta(headers):
|
||||
"""
|
||||
Helper function to translate from X-Object-Sysmeta-Symlink-Target and
|
||||
X-Object-Sysmeta-Symlink-Target-Account to X-Symlink-Target and
|
||||
X-Sysmeta-Symlink-Target-Account
|
||||
Helper function to translate from cluster-facing
|
||||
X-Object-Sysmeta-Symlink-* headers to client-facing X-Symlink-* headers.
|
||||
|
||||
:param headers: request headers dict. Note that the headers dict
|
||||
will be updated directly.
|
||||
"""
|
||||
if TGT_OBJ_SYSMETA_SYMLINK_HDR in headers:
|
||||
headers[TGT_OBJ_SYMLINK_HDR] = headers.pop(
|
||||
TGT_OBJ_SYSMETA_SYMLINK_HDR)
|
||||
|
||||
if TGT_ACCT_SYSMETA_SYMLINK_HDR in headers:
|
||||
headers[TGT_ACCT_SYMLINK_HDR] = headers.pop(
|
||||
TGT_ACCT_SYSMETA_SYMLINK_HDR)
|
||||
for user_hdr, sysmeta_hdr in (
|
||||
(TGT_OBJ_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),
|
||||
(TGT_BYTES_SYMLINK_HDR, TGT_BYTES_SYSMETA_SYMLINK_HDR)):
|
||||
if sysmeta_hdr in headers:
|
||||
headers[user_hdr] = headers.pop(sysmeta_hdr)
|
||||
|
||||
|
||||
class SymlinkContainerContext(WSGIContext):
|
||||
@ -308,9 +366,10 @@ class SymlinkContainerContext(WSGIContext):
|
||||
|
||||
def _extract_symlink_path_json(self, obj_dict, swift_version, account):
|
||||
"""
|
||||
Extract the symlink path from the hash value
|
||||
:return: object dictionary with additional key:value pair if object
|
||||
is a symlink. The new key is symlink_path.
|
||||
Extract the symlink info from the hash value
|
||||
:return: object dictionary with additional key:value pairs when object
|
||||
is a symlink. i.e. new symlink_path, symlink_etag and
|
||||
symlink_bytes keys
|
||||
"""
|
||||
if 'hash' in obj_dict:
|
||||
hash_value, meta = parse_header(obj_dict['hash'])
|
||||
@ -321,6 +380,10 @@ class SymlinkContainerContext(WSGIContext):
|
||||
target = meta[key]
|
||||
elif key == 'symlink_target_account':
|
||||
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:
|
||||
# make sure to add all other (key, values) back in place
|
||||
obj_dict['hash'] += '; %s=%s' % (key, meta[key])
|
||||
@ -370,10 +433,11 @@ class SymlinkObjectContext(WSGIContext):
|
||||
except LinkIterError:
|
||||
errmsg = 'Too many levels of symbolic links, ' \
|
||||
'maximum allowed is %d' % self.symloop_max
|
||||
raise HTTPConflict(
|
||||
body=errmsg, request=req, content_type='text/plain')
|
||||
raise HTTPConflict(body=errmsg, request=req,
|
||||
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)
|
||||
|
||||
def build_traversal_req(symlink_target):
|
||||
@ -396,14 +460,35 @@ class SymlinkObjectContext(WSGIContext):
|
||||
|
||||
symlink_target = self._response_header_value(
|
||||
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:
|
||||
raise LinkIterError()
|
||||
# format: /<account name>/<container name>/<object name>
|
||||
new_req = build_traversal_req(symlink_target)
|
||||
self._loop_count += 1
|
||||
return self._recursive_get_head(new_req)
|
||||
return self._recursive_get_head(new_req, target_etag=resp_etag)
|
||||
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:
|
||||
# Content-Location will be applied only when one or more
|
||||
# symlink recursion occurred.
|
||||
@ -417,6 +502,47 @@ class SymlinkObjectContext(WSGIContext):
|
||||
|
||||
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):
|
||||
"""
|
||||
Handle put request when it contains X-Symlink-Target header.
|
||||
@ -435,7 +561,13 @@ class SymlinkObjectContext(WSGIContext):
|
||||
request=req,
|
||||
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)
|
||||
# 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
|
||||
@ -445,16 +577,30 @@ class SymlinkObjectContext(WSGIContext):
|
||||
# listing result for clients.
|
||||
# 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
|
||||
# here, simply. Note that this override etag may be encrypted in the
|
||||
# container db by encryption middleware.
|
||||
# here, simply (if no other override etag was provided). Note that this
|
||||
# override etag may be encrypted in the container db by encryption
|
||||
# middleware.
|
||||
|
||||
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]
|
||||
]
|
||||
if TGT_ACCT_SYSMETA_SYMLINK_HDR in req.headers:
|
||||
etag_override.append(
|
||||
'symlink_target_account=%s' %
|
||||
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')] = \
|
||||
'; '.join(etag_override)
|
||||
|
||||
@ -495,11 +641,16 @@ class SymlinkObjectContext(WSGIContext):
|
||||
TGT_ACCT_SYSMETA_SYMLINK_HDR) or wsgi_quote(account)
|
||||
location_hdr = os.path.join(
|
||||
'/', 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
|
||||
errmsg = 'The requested POST was applied to a symlink. POST ' +\
|
||||
'directly to the target to apply requested metadata.'
|
||||
raise HTTPTemporaryRedirect(
|
||||
body=errmsg, headers={'location': location_hdr})
|
||||
body=errmsg, headers=headers)
|
||||
else:
|
||||
return resp
|
||||
|
||||
@ -512,10 +663,7 @@ class SymlinkObjectContext(WSGIContext):
|
||||
:returns: Response Iterator after start_response has been called
|
||||
"""
|
||||
if req.method in ('GET', 'HEAD'):
|
||||
# if GET request came from versioned writes, then it should get
|
||||
# the symlink only, not the referenced target
|
||||
if req.params.get('symlink') == 'get' or \
|
||||
req.environ.get('swift.source') == 'VW':
|
||||
if req.params.get('symlink') == 'get':
|
||||
resp = self.handle_get_head_symlink(req)
|
||||
else:
|
||||
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))
|
||||
if symloop_max < 1:
|
||||
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):
|
||||
return SymlinkMiddleware(app, conf, symloop_max)
|
||||
|
@ -371,7 +371,7 @@ class VersionedWritesContext(WSGIContext):
|
||||
# to container, but not READ. This was allowed in previous version
|
||||
# (i.e., before middleware) so keeping the same behavior here
|
||||
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')
|
||||
source_resp = get_req.get_response(self.app)
|
||||
|
||||
|
@ -73,8 +73,10 @@ class TestSymlinkEnv(BaseEnv):
|
||||
return (cls.link_cont, cls.tgt_cont)
|
||||
|
||||
@classmethod
|
||||
def target_content_location(cls):
|
||||
return '%s/%s' % (cls.tgt_cont, cls.tgt_obj)
|
||||
def target_content_location(cls, override_obj=None, override_account=None):
|
||||
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
|
||||
def _make_request(cls, url, token, parsed, conn, method,
|
||||
@ -102,20 +104,21 @@ class TestSymlinkEnv(BaseEnv):
|
||||
return name
|
||||
|
||||
@classmethod
|
||||
def _create_tgt_object(cls):
|
||||
def _create_tgt_object(cls, body=TARGET_BODY):
|
||||
resp = retry(cls._make_request, method='PUT',
|
||||
headers={'Content-Type': 'application/target'},
|
||||
container=cls.tgt_cont, obj=cls.tgt_obj,
|
||||
body=TARGET_BODY)
|
||||
body=body)
|
||||
if resp.status != 201:
|
||||
raise ResponseError(resp)
|
||||
|
||||
# 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')
|
||||
|
||||
resp = retry(cls._make_request, method='GET',
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
@ -176,10 +179,17 @@ class TestSymlink(Base):
|
||||
yield uuid4().hex
|
||||
|
||||
self.obj_name_gen = object_name_generator()
|
||||
self._account_name = None
|
||||
|
||||
def tearDown(self):
|
||||
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,
|
||||
container, obj='', headers=None, body=b'',
|
||||
query_args=None, allow_redirects=True):
|
||||
@ -210,22 +220,30 @@ class TestSymlink(Base):
|
||||
headers=headers)
|
||||
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(
|
||||
self, link_cont, link_obj, expected_content_location,
|
||||
use_account=1):
|
||||
resp = retry(
|
||||
self._make_request, method='GET',
|
||||
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.getheader('content-length'),
|
||||
str(self.env.tgt_length))
|
||||
self.assertEqual(resp.getheader('etag'), self.env.tgt_etag)
|
||||
self.assertIn('Content-Location', resp.headers)
|
||||
# TODO: content-location is a full path so it's better to assert
|
||||
# with the value, instead of assertIn
|
||||
self.assertIn(expected_content_location,
|
||||
resp.getheader('content-location'))
|
||||
self.assertEqual(expected_content_location,
|
||||
resp.getheader('content-location'))
|
||||
return resp
|
||||
|
||||
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
|
||||
self._assertSymlink(
|
||||
self.env.link_cont, link_obj,
|
||||
expected_content_location='%s/%s' % (
|
||||
self.env.tgt_cont, normalized_quoted_obj))
|
||||
expected_content_location=self.env.target_content_location(
|
||||
normalized_quoted_obj))
|
||||
|
||||
# create a symlink using the normalized target path
|
||||
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
|
||||
self._assertSymlink(
|
||||
self.env.link_cont, link_obj,
|
||||
expected_content_location='%s/%s' % (
|
||||
self.env.tgt_cont, normalized_quoted_obj))
|
||||
expected_content_location=self.env.target_content_location(
|
||||
normalized_quoted_obj))
|
||||
|
||||
def test_symlink_put_head_get(self):
|
||||
link_obj = uuid4().hex
|
||||
@ -322,6 +340,195 @@ class TestSymlink(Base):
|
||||
|
||||
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):
|
||||
link_obj = uuid4().hex
|
||||
|
||||
@ -353,9 +560,8 @@ class TestSymlink(Base):
|
||||
container=self.env.link_cont, obj=link_obj, use_account=1)
|
||||
self.assertEqual(resp.status, 404)
|
||||
self.assertIn('Content-Location', resp.headers)
|
||||
expected_location_hdr = "%s/%s" % (self.env.tgt_cont, target_obj)
|
||||
self.assertIn(expected_location_hdr,
|
||||
resp.getheader('content-location'))
|
||||
self.assertEqual(self.env.target_content_location(target_obj),
|
||||
resp.getheader('content-location'))
|
||||
|
||||
# HEAD on target object via symlink should return a 404 since target
|
||||
# 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('etag'), target_etag)
|
||||
self.assertIn('Content-Location', resp.headers)
|
||||
self.assertIn(expected_location_hdr,
|
||||
resp.getheader('content-location'))
|
||||
self.assertEqual(self.env.target_content_location(target_obj),
|
||||
resp.getheader('content-location'))
|
||||
|
||||
def test_symlink_chain(self):
|
||||
# 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
|
||||
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):
|
||||
if 'slo' not in cluster_info:
|
||||
raise SkipTest
|
||||
@ -557,7 +823,7 @@ class TestSymlink(Base):
|
||||
'%s/%s' % (self.env.tgt_cont, self.env.tgt_obj)}
|
||||
resp = retry(
|
||||
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.content,
|
||||
@ -636,7 +902,6 @@ class TestSymlink(Base):
|
||||
tgt_obj=self.env.tgt_obj)
|
||||
|
||||
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]
|
||||
|
||||
# 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
|
||||
# container/object in the account 2.
|
||||
# (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}
|
||||
resp = retry(self._make_request_with_symlink_get, method='PUT',
|
||||
container=self.env.link_cont, obj=link_obj2,
|
||||
@ -669,6 +934,7 @@ class TestSymlink(Base):
|
||||
# sanity: HEAD/GET on link_obj itself
|
||||
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
|
||||
for method in ('HEAD', 'GET'):
|
||||
resp = retry(
|
||||
@ -676,14 +942,15 @@ class TestSymlink(Base):
|
||||
container=self.env.link_cont, obj=link_obj2, use_account=2)
|
||||
self.assertEqual(resp.status, 404)
|
||||
self.assertIn('content-location', resp.headers)
|
||||
self.assertIn(self.env.target_content_location(),
|
||||
resp.getheader('content-location'))
|
||||
self.assertEqual(
|
||||
self.env.target_content_location(override_account=account_two),
|
||||
resp.getheader('content-location'))
|
||||
|
||||
# copy symlink itself to a different account with target account
|
||||
# the target path will be in account 1
|
||||
# the target path will have an object
|
||||
headers = {'X-Symlink-target-Account': account_one,
|
||||
'X-Copy-From-Account': account_one,
|
||||
headers = {'X-Symlink-target-Account': self.account_name,
|
||||
'X-Copy-From-Account': self.account_name,
|
||||
'X-Copy-From': copy_src}
|
||||
resp = retry(
|
||||
self._make_request_with_symlink_get, method='PUT',
|
||||
@ -780,7 +1047,8 @@ class TestSymlink(Base):
|
||||
link_obj = 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_obj=self.env.tgt_obj)
|
||||
|
||||
@ -821,6 +1089,73 @@ class TestSymlink(Base):
|
||||
# sanity: no X-Object-Meta-Alpha exists in the response header
|
||||
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):
|
||||
# POSTing to a symlink is not allowed and should return a 307
|
||||
# updating the symlink target with a POST should always fail
|
||||
@ -878,11 +1213,9 @@ class TestSymlink(Base):
|
||||
raise SkipTest
|
||||
link_obj = uuid4().hex
|
||||
|
||||
account_one = tf.parsed[0].path.split('/', 2)[2]
|
||||
|
||||
# create symlink in account 2
|
||||
# pointing to account 1
|
||||
headers = {'X-Symlink-Target-Account': account_one,
|
||||
headers = {'X-Symlink-Target-Account': self.account_name,
|
||||
'X-Symlink-Target':
|
||||
'%s/%s' % (self.env.tgt_cont, self.env.tgt_obj)}
|
||||
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)
|
||||
|
||||
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
|
||||
# permit account 2 to read account 1 tgt_cont
|
||||
@ -917,11 +1253,96 @@ class TestSymlink(Base):
|
||||
self.env.link_cont, link_obj,
|
||||
expected_content_location=self.env.target_content_location(),
|
||||
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):
|
||||
link_obj = uuid4().hex
|
||||
|
||||
self._test_put_symlink(link_cont=self.env.link_cont, link_obj=link_obj,
|
||||
tgt_cont=self.env.tgt_cont,
|
||||
tgt_obj=self.env.tgt_obj)
|
||||
@ -933,9 +1354,53 @@ class TestSymlink(Base):
|
||||
self.assertEqual(resp.status, 200)
|
||||
object_list = json.loads(resp.content)
|
||||
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(self.env.target_content_location(),
|
||||
object_list[0]['symlink_path'])
|
||||
self.assertEqual(self.env.target_content_location(),
|
||||
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):
|
||||
@ -1007,6 +1472,8 @@ class TestSymlinkSlo(Base):
|
||||
"Expected slo_enabled to be True/False, got %r" %
|
||||
(self.env.slo_enabled,))
|
||||
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):
|
||||
self.file_symlink.write(hdrs={'X-Symlink-Target':
|
||||
@ -1020,6 +1487,142 @@ class TestSymlinkSlo(Base):
|
||||
(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'])
|
||||
|
||||
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):
|
||||
self.file_symlink.write(hdrs={'X-Symlink-Target':
|
||||
'%s/%s' % (self.env.container.name,
|
||||
|
@ -24,7 +24,7 @@ from swift.common import swob
|
||||
from swift.common.middleware import symlink, copy, versioned_writes, \
|
||||
listing_formats
|
||||
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.test_versioned_writes import FakeCache
|
||||
|
||||
@ -78,6 +78,14 @@ class TestSymlinkMiddlewareBase(unittest.TestCase):
|
||||
|
||||
|
||||
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):
|
||||
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
|
||||
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)
|
||||
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/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):
|
||||
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('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):
|
||||
self.app.register('HEAD', '/v1/a/c/symlink', swob.HTTPOk,
|
||||
{'X-Object-Sysmeta-Symlink-Target': 'c1/o',
|
||||
@ -324,15 +555,21 @@ class TestSymlinkMiddleware(TestSymlinkMiddlewareBase):
|
||||
self.assertFalse(calls[2:])
|
||||
|
||||
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'})
|
||||
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'})
|
||||
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'})
|
||||
req = Request.blank('/v1/a/c/symlink', method='HEAD')
|
||||
status, headers, body = self.call_sym(req)
|
||||
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):
|
||||
# similar test to test_symlink_too_deep, but now changed the limit to 3
|
||||
@ -691,6 +928,145 @@ class SymlinkCopyingTestCase(TestSymlinkMiddlewareBase):
|
||||
self.assertEqual(
|
||||
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):
|
||||
# verify interaction of versioned_writes and symlink middlewares
|
||||
@ -819,13 +1195,16 @@ class TestSymlinkContainerContext(TestSymlinkMiddlewareBase):
|
||||
def test_extract_symlink_path_json_symlink_path(self):
|
||||
obj_dict = {"bytes": 6,
|
||||
"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",
|
||||
"content_type": "application/octet-stream"}
|
||||
obj_dict = self.context._extract_symlink_path_json(
|
||||
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_etag'], 'tgt_etag')
|
||||
self.assertEqual(obj_dict['symlink_bytes'], 8)
|
||||
|
||||
def test_extract_symlink_path_json_symlink_path_and_account(self):
|
||||
obj_dict = {
|
||||
|
@ -417,7 +417,7 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase):
|
||||
self.assertRequestEqual(req, self.authorized[1])
|
||||
self.assertEqual(3, self.app.call_count)
|
||||
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/c/o'),
|
||||
], self.app.calls)
|
||||
@ -449,7 +449,7 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase):
|
||||
self.assertRequestEqual(req, self.authorized[1])
|
||||
self.assertEqual(3, self.app.call_count)
|
||||
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/c/o'),
|
||||
], self.app.calls)
|
||||
@ -682,7 +682,7 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase):
|
||||
prefix_listing_prefix = '/v1/a/ver_cont?prefix=001o/&'
|
||||
self.assertEqual(self.app.calls, [
|
||||
('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'),
|
||||
('DELETE', '/v1/a/ver_cont/001o/2'),
|
||||
])
|
||||
@ -777,7 +777,7 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase):
|
||||
self.assertEqual(self.app.calls, [
|
||||
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
||||
('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'),
|
||||
('DELETE', '/v1/a/ver_cont/001o/1'),
|
||||
('DELETE', '/v1/a/ver_cont/001o/2'),
|
||||
@ -941,7 +941,7 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase):
|
||||
prefix_listing_prefix = '/v1/a/ver_cont?prefix=001o/&'
|
||||
self.assertEqual(self.app.calls, [
|
||||
('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'),
|
||||
('DELETE', '/v1/a/ver_cont/001o/1'),
|
||||
])
|
||||
@ -989,8 +989,8 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase):
|
||||
prefix_listing_prefix = '/v1/a/ver_cont?prefix=001o/&'
|
||||
self.assertEqual(self.app.calls, [
|
||||
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
||||
('GET', '/v1/a/ver_cont/001o/2'),
|
||||
('GET', '/v1/a/ver_cont/001o/1'),
|
||||
('GET', '/v1/a/ver_cont/001o/2?symlink=get'),
|
||||
('GET', '/v1/a/ver_cont/001o/1?symlink=get'),
|
||||
('PUT', '/v1/a/c/o'),
|
||||
('DELETE', '/v1/a/ver_cont/001o/1'),
|
||||
])
|
||||
@ -1114,7 +1114,7 @@ class VersionedWritesOldContainersTestCase(VersionedWritesBaseTestCase):
|
||||
self.assertEqual(self.app.calls, [
|
||||
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
||||
('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'),
|
||||
('DELETE', '/v1/a/ver_cont/001o/2'),
|
||||
])
|
||||
@ -1167,8 +1167,8 @@ class VersionedWritesOldContainersTestCase(VersionedWritesBaseTestCase):
|
||||
self.assertEqual(self.app.calls, [
|
||||
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
||||
('GET', prefix_listing_prefix + 'marker=001o/2'),
|
||||
('GET', '/v1/a/ver_cont/001o/2'),
|
||||
('GET', '/v1/a/ver_cont/001o/1'),
|
||||
('GET', '/v1/a/ver_cont/001o/2?symlink=get'),
|
||||
('GET', '/v1/a/ver_cont/001o/1?symlink=get'),
|
||||
('PUT', '/v1/a/c/o'),
|
||||
('DELETE', '/v1/a/ver_cont/001o/1'),
|
||||
])
|
||||
@ -1282,14 +1282,14 @@ class VersionedWritesOldContainersTestCase(VersionedWritesBaseTestCase):
|
||||
prefix_listing_prefix = '/v1/a/ver_cont?prefix=001o/&'
|
||||
self.assertEqual(self.app.calls, [
|
||||
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
||||
('GET', '/v1/a/ver_cont/001o/4'),
|
||||
('GET', '/v1/a/ver_cont/001o/3'),
|
||||
('GET', '/v1/a/ver_cont/001o/2'),
|
||||
('GET', '/v1/a/ver_cont/001o/4?symlink=get'),
|
||||
('GET', '/v1/a/ver_cont/001o/3?symlink=get'),
|
||||
('GET', '/v1/a/ver_cont/001o/2?symlink=get'),
|
||||
('GET', prefix_listing_prefix + 'marker=001o/2&reverse=on'),
|
||||
('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/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'),
|
||||
('DELETE', '/v1/a/ver_cont/001o/1'),
|
||||
])
|
||||
@ -1354,13 +1354,13 @@ class VersionedWritesOldContainersTestCase(VersionedWritesBaseTestCase):
|
||||
prefix_listing_prefix = '/v1/a/ver_cont?prefix=001o/&'
|
||||
self.assertEqual(self.app.calls, [
|
||||
('GET', prefix_listing_prefix + 'marker=&reverse=on'),
|
||||
('GET', '/v1/a/ver_cont/001o/4'),
|
||||
('GET', '/v1/a/ver_cont/001o/3'),
|
||||
('GET', '/v1/a/ver_cont/001o/4?symlink=get'),
|
||||
('GET', '/v1/a/ver_cont/001o/3?symlink=get'),
|
||||
('GET', prefix_listing_prefix + 'marker=001o/3&reverse=on'),
|
||||
('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/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'),
|
||||
('DELETE', '/v1/a/ver_cont/001o/2'),
|
||||
])
|
||||
|
Loading…
Reference in New Issue
Block a user