Merge "Refactor server side copy as middleware"
This commit is contained in:
commit
b6c3ab26a1
@ -9,7 +9,7 @@ eventlet_debug = true
|
|||||||
[pipeline:main]
|
[pipeline:main]
|
||||||
# Yes, proxy-logging appears twice. This is so that
|
# Yes, proxy-logging appears twice. This is so that
|
||||||
# middleware-originated requests get logged too.
|
# middleware-originated requests get logged too.
|
||||||
pipeline = catch_errors gatekeeper healthcheck proxy-logging cache bulk tempurl ratelimit crossdomain container_sync tempauth staticweb container-quotas account-quotas slo dlo versioned_writes proxy-logging proxy-server
|
pipeline = catch_errors gatekeeper healthcheck proxy-logging cache bulk tempurl ratelimit crossdomain container_sync tempauth staticweb copy container-quotas account-quotas slo dlo versioned_writes proxy-logging proxy-server
|
||||||
|
|
||||||
[filter:catch_errors]
|
[filter:catch_errors]
|
||||||
use = egg:swift#catch_errors
|
use = egg:swift#catch_errors
|
||||||
@ -68,6 +68,9 @@ use = egg:swift#gatekeeper
|
|||||||
use = egg:swift#versioned_writes
|
use = egg:swift#versioned_writes
|
||||||
allow_versioned_writes = true
|
allow_versioned_writes = true
|
||||||
|
|
||||||
|
[filter:copy]
|
||||||
|
use = egg:swift#copy
|
||||||
|
|
||||||
[app:proxy-server]
|
[app:proxy-server]
|
||||||
use = egg:swift#proxy
|
use = egg:swift#proxy
|
||||||
allow_account_management = true
|
allow_account_management = true
|
||||||
|
@ -103,6 +103,7 @@ LE :ref:`list_endpoints`
|
|||||||
KS :ref:`keystoneauth`
|
KS :ref:`keystoneauth`
|
||||||
RL :ref:`ratelimit`
|
RL :ref:`ratelimit`
|
||||||
VW :ref:`versioned_writes`
|
VW :ref:`versioned_writes`
|
||||||
|
SSC :ref:`copy`
|
||||||
======================= =============================
|
======================= =============================
|
||||||
|
|
||||||
|
|
||||||
|
@ -187,6 +187,15 @@ Recon
|
|||||||
:members:
|
:members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. _copy:
|
||||||
|
|
||||||
|
Server Side Copy
|
||||||
|
================
|
||||||
|
|
||||||
|
.. automodule:: swift.common.middleware.copy
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
Static Large Objects
|
Static Large Objects
|
||||||
====================
|
====================
|
||||||
|
|
||||||
|
@ -79,12 +79,12 @@ bind_port = 8080
|
|||||||
[pipeline:main]
|
[pipeline:main]
|
||||||
# This sample pipeline uses tempauth and is used for SAIO dev work and
|
# This sample pipeline uses tempauth and is used for SAIO dev work and
|
||||||
# testing. See below for a pipeline using keystone.
|
# testing. See below for a pipeline using keystone.
|
||||||
pipeline = catch_errors gatekeeper healthcheck proxy-logging cache container_sync bulk tempurl ratelimit tempauth container-quotas account-quotas slo dlo versioned_writes proxy-logging proxy-server
|
pipeline = catch_errors gatekeeper healthcheck proxy-logging cache container_sync bulk tempurl ratelimit tempauth copy container-quotas account-quotas slo dlo versioned_writes proxy-logging proxy-server
|
||||||
|
|
||||||
# The following pipeline shows keystone integration. Comment out the one
|
# The following pipeline shows keystone integration. Comment out the one
|
||||||
# above and uncomment this one. Additional steps for integrating keystone are
|
# above and uncomment this one. Additional steps for integrating keystone are
|
||||||
# covered further below in the filter sections for authtoken and keystoneauth.
|
# covered further below in the filter sections for authtoken and keystoneauth.
|
||||||
#pipeline = catch_errors gatekeeper healthcheck proxy-logging cache container_sync bulk tempurl ratelimit authtoken keystoneauth container-quotas account-quotas slo dlo versioned_writes proxy-logging proxy-server
|
#pipeline = catch_errors gatekeeper healthcheck proxy-logging cache container_sync bulk tempurl ratelimit authtoken keystoneauth copy container-quotas account-quotas slo dlo versioned_writes proxy-logging proxy-server
|
||||||
|
|
||||||
[app:proxy-server]
|
[app:proxy-server]
|
||||||
use = egg:swift#proxy
|
use = egg:swift#proxy
|
||||||
@ -129,11 +129,6 @@ use = egg:swift#proxy
|
|||||||
# 'false' no one, even authorized, can.
|
# 'false' no one, even authorized, can.
|
||||||
# allow_account_management = false
|
# allow_account_management = false
|
||||||
#
|
#
|
||||||
# Set object_post_as_copy = false to turn on fast posts where only the metadata
|
|
||||||
# changes are stored anew and the original data file is kept in place. This
|
|
||||||
# makes for quicker posts.
|
|
||||||
# object_post_as_copy = true
|
|
||||||
#
|
|
||||||
# If set to 'true' authorized accounts that do not yet exist within the Swift
|
# If set to 'true' authorized accounts that do not yet exist within the Swift
|
||||||
# cluster will be automatically created.
|
# cluster will be automatically created.
|
||||||
# account_autocreate = false
|
# account_autocreate = false
|
||||||
@ -749,3 +744,14 @@ use = egg:swift#versioned_writes
|
|||||||
# in the container configuration file, which will be eventually
|
# in the container configuration file, which will be eventually
|
||||||
# deprecated. See documentation for more details.
|
# deprecated. See documentation for more details.
|
||||||
# allow_versioned_writes = false
|
# allow_versioned_writes = false
|
||||||
|
|
||||||
|
# Note: Put after auth and before dlo and slo middlewares.
|
||||||
|
# If you don't put it in the pipeline, it will be inserted for you.
|
||||||
|
[filter:copy]
|
||||||
|
use = egg:swift#copy
|
||||||
|
# Set object_post_as_copy = false to turn on fast posts where only the metadata
|
||||||
|
# changes are stored anew and the original data file is kept in place. This
|
||||||
|
# makes for quicker posts.
|
||||||
|
# When object_post_as_copy is set to True, a POST request will be transformed
|
||||||
|
# into a COPY request where source and destination objects are the same.
|
||||||
|
# object_post_as_copy = true
|
||||||
|
@ -96,6 +96,7 @@ paste.filter_factory =
|
|||||||
container_sync = swift.common.middleware.container_sync:filter_factory
|
container_sync = swift.common.middleware.container_sync:filter_factory
|
||||||
xprofile = swift.common.middleware.xprofile:filter_factory
|
xprofile = swift.common.middleware.xprofile:filter_factory
|
||||||
versioned_writes = swift.common.middleware.versioned_writes:filter_factory
|
versioned_writes = swift.common.middleware.versioned_writes:filter_factory
|
||||||
|
copy = swift.common.middleware.copy:filter_factory
|
||||||
|
|
||||||
[build_sphinx]
|
[build_sphinx]
|
||||||
all_files = 1
|
all_files = 1
|
||||||
|
@ -20,7 +20,6 @@ import time
|
|||||||
import six
|
import six
|
||||||
from six.moves.configparser import ConfigParser, NoSectionError, NoOptionError
|
from six.moves.configparser import ConfigParser, NoSectionError, NoOptionError
|
||||||
from six.moves import urllib
|
from six.moves import urllib
|
||||||
from six.moves.urllib.parse import unquote
|
|
||||||
|
|
||||||
from swift.common import utils, exceptions
|
from swift.common import utils, exceptions
|
||||||
from swift.common.swob import HTTPBadRequest, HTTPLengthRequired, \
|
from swift.common.swob import HTTPBadRequest, HTTPLengthRequired, \
|
||||||
@ -205,10 +204,6 @@ def check_object_creation(req, object_name):
|
|||||||
request=req,
|
request=req,
|
||||||
content_type='text/plain')
|
content_type='text/plain')
|
||||||
|
|
||||||
if 'X-Copy-From' in req.headers and req.content_length:
|
|
||||||
return HTTPBadRequest(body='Copy requests require a zero byte body',
|
|
||||||
request=req, content_type='text/plain')
|
|
||||||
|
|
||||||
if len(object_name) > MAX_OBJECT_NAME_LENGTH:
|
if len(object_name) > MAX_OBJECT_NAME_LENGTH:
|
||||||
return HTTPBadRequest(body='Object name length of %d longer than %d' %
|
return HTTPBadRequest(body='Object name length of %d longer than %d' %
|
||||||
(len(object_name), MAX_OBJECT_NAME_LENGTH),
|
(len(object_name), MAX_OBJECT_NAME_LENGTH),
|
||||||
@ -359,63 +354,6 @@ def check_utf8(string):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def check_path_header(req, name, length, error_msg):
|
|
||||||
"""
|
|
||||||
Validate that the value of path-like header is
|
|
||||||
well formatted. We assume the caller ensures that
|
|
||||||
specific header is present in req.headers.
|
|
||||||
|
|
||||||
:param req: HTTP request object
|
|
||||||
:param name: header name
|
|
||||||
:param length: length of path segment check
|
|
||||||
:param error_msg: error message for client
|
|
||||||
:returns: A tuple with path parts according to length
|
|
||||||
:raise: HTTPPreconditionFailed if header value
|
|
||||||
is not well formatted.
|
|
||||||
"""
|
|
||||||
src_header = unquote(req.headers.get(name))
|
|
||||||
if not src_header.startswith('/'):
|
|
||||||
src_header = '/' + src_header
|
|
||||||
try:
|
|
||||||
return utils.split_path(src_header, length, length, True)
|
|
||||||
except ValueError:
|
|
||||||
raise HTTPPreconditionFailed(
|
|
||||||
request=req,
|
|
||||||
body=error_msg)
|
|
||||||
|
|
||||||
|
|
||||||
def check_copy_from_header(req):
|
|
||||||
"""
|
|
||||||
Validate that the value from x-copy-from header is
|
|
||||||
well formatted. We assume the caller ensures that
|
|
||||||
x-copy-from header is present in req.headers.
|
|
||||||
|
|
||||||
:param req: HTTP request object
|
|
||||||
:returns: A tuple with container name and object name
|
|
||||||
:raise: HTTPPreconditionFailed if x-copy-from value
|
|
||||||
is not well formatted.
|
|
||||||
"""
|
|
||||||
return check_path_header(req, 'X-Copy-From', 2,
|
|
||||||
'X-Copy-From header must be of the form '
|
|
||||||
'<container name>/<object name>')
|
|
||||||
|
|
||||||
|
|
||||||
def check_destination_header(req):
|
|
||||||
"""
|
|
||||||
Validate that the value from destination header is
|
|
||||||
well formatted. We assume the caller ensures that
|
|
||||||
destination header is present in req.headers.
|
|
||||||
|
|
||||||
:param req: HTTP request object
|
|
||||||
:returns: A tuple with container name and object name
|
|
||||||
:raise: HTTPPreconditionFailed if destination value
|
|
||||||
is not well formatted.
|
|
||||||
"""
|
|
||||||
return check_path_header(req, 'Destination', 2,
|
|
||||||
'Destination header must be of the form '
|
|
||||||
'<container name>/<object name>')
|
|
||||||
|
|
||||||
|
|
||||||
def check_name_format(req, name, target_type):
|
def check_name_format(req, name, target_type):
|
||||||
"""
|
"""
|
||||||
Validate that the header contains valid account or container name.
|
Validate that the header contains valid account or container name.
|
||||||
|
@ -52,11 +52,10 @@ Due to the eventual consistency further uploads might be possible until the
|
|||||||
account size has been updated.
|
account size has been updated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from swift.common.constraints import check_copy_from_header
|
|
||||||
from swift.common.swob import HTTPForbidden, HTTPBadRequest, \
|
from swift.common.swob import HTTPForbidden, HTTPBadRequest, \
|
||||||
HTTPRequestEntityTooLarge, wsgify
|
HTTPRequestEntityTooLarge, wsgify
|
||||||
from swift.common.utils import register_swift_info
|
from swift.common.utils import register_swift_info
|
||||||
from swift.proxy.controllers.base import get_account_info, get_object_info
|
from swift.proxy.controllers.base import get_account_info
|
||||||
|
|
||||||
|
|
||||||
class AccountQuotaMiddleware(object):
|
class AccountQuotaMiddleware(object):
|
||||||
@ -71,7 +70,7 @@ class AccountQuotaMiddleware(object):
|
|||||||
@wsgify
|
@wsgify
|
||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
|
|
||||||
if request.method not in ("POST", "PUT", "COPY"):
|
if request.method not in ("POST", "PUT"):
|
||||||
return self.app
|
return self.app
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -106,15 +105,6 @@ class AccountQuotaMiddleware(object):
|
|||||||
if request.method == "POST" or not obj:
|
if request.method == "POST" or not obj:
|
||||||
return self.app
|
return self.app
|
||||||
|
|
||||||
if request.method == 'COPY':
|
|
||||||
copy_from = container + '/' + obj
|
|
||||||
else:
|
|
||||||
if 'x-copy-from' in request.headers:
|
|
||||||
src_cont, src_obj = check_copy_from_header(request)
|
|
||||||
copy_from = "%s/%s" % (src_cont, src_obj)
|
|
||||||
else:
|
|
||||||
copy_from = None
|
|
||||||
|
|
||||||
content_length = (request.content_length or 0)
|
content_length = (request.content_length or 0)
|
||||||
|
|
||||||
account_info = get_account_info(request.environ, self.app)
|
account_info = get_account_info(request.environ, self.app)
|
||||||
@ -127,14 +117,6 @@ class AccountQuotaMiddleware(object):
|
|||||||
if quota < 0:
|
if quota < 0:
|
||||||
return self.app
|
return self.app
|
||||||
|
|
||||||
if copy_from:
|
|
||||||
path = '/' + ver + '/' + account + '/' + copy_from
|
|
||||||
object_info = get_object_info(request.environ, self.app, path)
|
|
||||||
if not object_info or not object_info['length']:
|
|
||||||
content_length = 0
|
|
||||||
else:
|
|
||||||
content_length = int(object_info['length'])
|
|
||||||
|
|
||||||
new_size = int(account_info['bytes']) + content_length
|
new_size = int(account_info['bytes']) + content_length
|
||||||
if quota < new_size:
|
if quota < new_size:
|
||||||
resp = HTTPRequestEntityTooLarge(body='Upload exceeds quota.')
|
resp = HTTPRequestEntityTooLarge(body='Upload exceeds quota.')
|
||||||
|
@ -51,13 +51,11 @@ For example::
|
|||||||
[filter:container_quotas]
|
[filter:container_quotas]
|
||||||
use = egg:swift#container_quotas
|
use = egg:swift#container_quotas
|
||||||
"""
|
"""
|
||||||
from swift.common.constraints import check_copy_from_header, \
|
|
||||||
check_account_format, check_destination_header
|
|
||||||
from swift.common.http import is_success
|
from swift.common.http import is_success
|
||||||
from swift.common.swob import HTTPRequestEntityTooLarge, HTTPBadRequest, \
|
from swift.common.swob import HTTPRequestEntityTooLarge, HTTPBadRequest, \
|
||||||
wsgify
|
wsgify
|
||||||
from swift.common.utils import register_swift_info
|
from swift.common.utils import register_swift_info
|
||||||
from swift.proxy.controllers.base import get_container_info, get_object_info
|
from swift.proxy.controllers.base import get_container_info
|
||||||
|
|
||||||
|
|
||||||
class ContainerQuotaMiddleware(object):
|
class ContainerQuotaMiddleware(object):
|
||||||
@ -91,25 +89,9 @@ class ContainerQuotaMiddleware(object):
|
|||||||
return HTTPBadRequest(body='Invalid count quota.')
|
return HTTPBadRequest(body='Invalid count quota.')
|
||||||
|
|
||||||
# check user uploads against quotas
|
# check user uploads against quotas
|
||||||
elif obj and req.method in ('PUT', 'COPY'):
|
elif obj and req.method in ('PUT'):
|
||||||
container_info = None
|
container_info = get_container_info(
|
||||||
if req.method == 'PUT':
|
req.environ, self.app, swift_source='CQ')
|
||||||
container_info = get_container_info(
|
|
||||||
req.environ, self.app, swift_source='CQ')
|
|
||||||
if req.method == 'COPY' and 'Destination' in req.headers:
|
|
||||||
dest_account = account
|
|
||||||
if 'Destination-Account' in req.headers:
|
|
||||||
dest_account = req.headers.get('Destination-Account')
|
|
||||||
dest_account = check_account_format(req, dest_account)
|
|
||||||
dest_container, dest_object = check_destination_header(req)
|
|
||||||
path_info = req.environ['PATH_INFO']
|
|
||||||
req.environ['PATH_INFO'] = "/%s/%s/%s/%s" % (
|
|
||||||
version, dest_account, dest_container, dest_object)
|
|
||||||
try:
|
|
||||||
container_info = get_container_info(
|
|
||||||
req.environ, self.app, swift_source='CQ')
|
|
||||||
finally:
|
|
||||||
req.environ['PATH_INFO'] = path_info
|
|
||||||
if not container_info or not is_success(container_info['status']):
|
if not container_info or not is_success(container_info['status']):
|
||||||
# this will hopefully 404 later
|
# this will hopefully 404 later
|
||||||
return self.app
|
return self.app
|
||||||
@ -118,16 +100,6 @@ class ContainerQuotaMiddleware(object):
|
|||||||
'bytes' in container_info and \
|
'bytes' in container_info and \
|
||||||
container_info['meta']['quota-bytes'].isdigit():
|
container_info['meta']['quota-bytes'].isdigit():
|
||||||
content_length = (req.content_length or 0)
|
content_length = (req.content_length or 0)
|
||||||
if 'x-copy-from' in req.headers or req.method == 'COPY':
|
|
||||||
if 'x-copy-from' in req.headers:
|
|
||||||
container, obj = check_copy_from_header(req)
|
|
||||||
path = '/%s/%s/%s/%s' % (version, account,
|
|
||||||
container, obj)
|
|
||||||
object_info = get_object_info(req.environ, self.app, path)
|
|
||||||
if not object_info or not object_info['length']:
|
|
||||||
content_length = 0
|
|
||||||
else:
|
|
||||||
content_length = int(object_info['length'])
|
|
||||||
new_size = int(container_info['bytes']) + content_length
|
new_size = int(container_info['bytes']) + content_length
|
||||||
if int(container_info['meta']['quota-bytes']) < new_size:
|
if int(container_info['meta']['quota-bytes']) < new_size:
|
||||||
return self.bad_response(req, container_info)
|
return self.bad_response(req, container_info)
|
||||||
|
522
swift/common/middleware/copy.py
Normal file
522
swift/common/middleware/copy.py
Normal file
@ -0,0 +1,522 @@
|
|||||||
|
# Copyright (c) 2015 OpenStack Foundation
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""
|
||||||
|
Server side copy is a feature that enables users/clients to COPY objects
|
||||||
|
between accounts and containers without the need to download and then
|
||||||
|
re-upload objects, thus eliminating additional bandwidth consumption and
|
||||||
|
also saving time. This may be used when renaming/moving an object which
|
||||||
|
in Swift is a (COPY + DELETE) operation.
|
||||||
|
|
||||||
|
The server side copy middleware should be inserted in the pipeline after auth
|
||||||
|
and before the quotas and large object middlewares. If it is not present in the
|
||||||
|
pipeline in the proxy-server configuration file, it will be inserted
|
||||||
|
automatically. There is no configurable option provided to turn off server
|
||||||
|
side copy.
|
||||||
|
|
||||||
|
--------
|
||||||
|
Metadata
|
||||||
|
--------
|
||||||
|
* All metadata of source object is preserved during object copy.
|
||||||
|
* One can also provide additional metadata during PUT/COPY request. This will
|
||||||
|
over-write any existing conflicting keys.
|
||||||
|
* Server side copy can also be used to change content-type of an existing
|
||||||
|
object.
|
||||||
|
|
||||||
|
-----------
|
||||||
|
Object Copy
|
||||||
|
-----------
|
||||||
|
* The destination container must exist before requesting copy of the object.
|
||||||
|
* When several replicas exist, the system copies from the most recent replica.
|
||||||
|
That is, the copy operation behaves as though the X-Newest header is in the
|
||||||
|
request.
|
||||||
|
* The request to copy an object should have no body (i.e. content-length of the
|
||||||
|
request must be zero).
|
||||||
|
|
||||||
|
There are two ways in which an object can be copied:
|
||||||
|
|
||||||
|
1. Send a PUT request to the new object (destination/target) with an additional
|
||||||
|
header named ``X-Copy-From`` specifying the source object
|
||||||
|
(in '/container/object' format). Example::
|
||||||
|
|
||||||
|
curl -i -X PUT http://<storage_url>/container1/destination_obj
|
||||||
|
-H 'X-Auth-Token: <token>'
|
||||||
|
-H 'X-Copy-From: /container2/source_obj'
|
||||||
|
-H 'Content-Length: 0'
|
||||||
|
|
||||||
|
2. Send a COPY request with an existing object in URL with an additional header
|
||||||
|
named ``Destination`` specifying the destination/target object
|
||||||
|
(in '/container/object' format). Example::
|
||||||
|
|
||||||
|
curl -i -X COPY http://<storage_url>/container2/source_obj
|
||||||
|
-H 'X-Auth-Token: <token>'
|
||||||
|
-H 'Destination: /container1/destination_obj'
|
||||||
|
-H 'Content-Length: 0'
|
||||||
|
|
||||||
|
Note that if the incoming request has some conditional headers (e.g. ``Range``,
|
||||||
|
``If-Match``), the *source* object will be evaluated for these headers (i.e. if
|
||||||
|
PUT with both ``X-Copy-From`` and ``Range``, Swift will make a partial copy to
|
||||||
|
the destination object).
|
||||||
|
|
||||||
|
-------------------------
|
||||||
|
Cross Account Object Copy
|
||||||
|
-------------------------
|
||||||
|
Objects can also be copied from one account to another account if the user
|
||||||
|
has the necessary permissions (i.e. permission to read from container
|
||||||
|
in source account and permission to write to container in destination account).
|
||||||
|
|
||||||
|
Similar to examples mentioned above, there are two ways to copy objects across
|
||||||
|
accounts:
|
||||||
|
|
||||||
|
1. Like the example above, send PUT request to copy object but with an
|
||||||
|
additional header named ``X-Copy-From-Account`` specifying the source
|
||||||
|
account. Example::
|
||||||
|
|
||||||
|
curl -i -X PUT http://<host>:<port>/v1/AUTH_test1/container/destination_obj
|
||||||
|
-H 'X-Auth-Token: <token>'
|
||||||
|
-H 'X-Copy-From: /container/source_obj'
|
||||||
|
-H 'X-Copy-From-Account: AUTH_test2'
|
||||||
|
-H 'Content-Length: 0'
|
||||||
|
|
||||||
|
2. Like the previous example, send a COPY request but with an additional header
|
||||||
|
named ``Destination-Account`` specifying the name of destination account.
|
||||||
|
Example::
|
||||||
|
|
||||||
|
curl -i -X COPY http://<host>:<port>/v1/AUTH_test2/container/source_obj
|
||||||
|
-H 'X-Auth-Token: <token>'
|
||||||
|
-H 'Destination: /container/destination_obj'
|
||||||
|
-H 'Destination-Account: AUTH_test1'
|
||||||
|
-H 'Content-Length: 0'
|
||||||
|
|
||||||
|
-------------------
|
||||||
|
Large Object Copy
|
||||||
|
-------------------
|
||||||
|
The best option to copy a large option is to copy segments individually.
|
||||||
|
To copy the manifest object of a large object, add the query parameter to
|
||||||
|
the copy request::
|
||||||
|
|
||||||
|
?multipart-manifest=get
|
||||||
|
|
||||||
|
If a request is sent without the query parameter, an attempt will be made to
|
||||||
|
copy the whole object but will fail if the object size is
|
||||||
|
greater than 5GB.
|
||||||
|
|
||||||
|
-------------------
|
||||||
|
Object Post as Copy
|
||||||
|
-------------------
|
||||||
|
Historically, this has been a feature (and a configurable option with default
|
||||||
|
set to True) in proxy server configuration. This has been moved to server side
|
||||||
|
copy middleware.
|
||||||
|
|
||||||
|
When ``object_post_as_copy`` is set to ``true`` (default value), an incoming
|
||||||
|
POST request is morphed into a COPY request where source and destination
|
||||||
|
objects are same.
|
||||||
|
|
||||||
|
This feature was necessary because of a previous behavior where POSTS would
|
||||||
|
update the metadata on the object but not on the container. As a result,
|
||||||
|
features like container sync would not work correctly. This is no longer the
|
||||||
|
case and the plan is to deprecate this option. It is being kept now for
|
||||||
|
backwards compatibility. At first chance, set ``object_post_as_copy`` to
|
||||||
|
``false``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from urllib import quote
|
||||||
|
from ConfigParser import ConfigParser, NoSectionError, NoOptionError
|
||||||
|
from six.moves.urllib.parse import unquote
|
||||||
|
|
||||||
|
from swift.common import utils
|
||||||
|
from swift.common.utils import get_logger, \
|
||||||
|
config_true_value, FileLikeIter, read_conf_dir, close_if_possible
|
||||||
|
from swift.common.swob import Request, HTTPPreconditionFailed, \
|
||||||
|
HTTPRequestEntityTooLarge, HTTPBadRequest
|
||||||
|
from swift.common.http import HTTP_MULTIPLE_CHOICES, HTTP_CREATED, \
|
||||||
|
is_success
|
||||||
|
from swift.common.constraints import check_account_format, MAX_FILE_SIZE
|
||||||
|
from swift.common.request_helpers import copy_header_subset, remove_items, \
|
||||||
|
is_sys_meta, is_sys_or_user_meta
|
||||||
|
from swift.common.wsgi import WSGIContext, make_subrequest
|
||||||
|
|
||||||
|
|
||||||
|
def _check_path_header(req, name, length, error_msg):
|
||||||
|
"""
|
||||||
|
Validate that the value of path-like header is
|
||||||
|
well formatted. We assume the caller ensures that
|
||||||
|
specific header is present in req.headers.
|
||||||
|
|
||||||
|
:param req: HTTP request object
|
||||||
|
:param name: header name
|
||||||
|
:param length: length of path segment check
|
||||||
|
:param error_msg: error message for client
|
||||||
|
:returns: A tuple with path parts according to length
|
||||||
|
:raise: HTTPPreconditionFailed if header value
|
||||||
|
is not well formatted.
|
||||||
|
"""
|
||||||
|
src_header = unquote(req.headers.get(name))
|
||||||
|
if not src_header.startswith('/'):
|
||||||
|
src_header = '/' + src_header
|
||||||
|
try:
|
||||||
|
return utils.split_path(src_header, length, length, True)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPPreconditionFailed(
|
||||||
|
request=req,
|
||||||
|
body=error_msg)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_copy_from_header(req):
|
||||||
|
"""
|
||||||
|
Validate that the value from x-copy-from header is
|
||||||
|
well formatted. We assume the caller ensures that
|
||||||
|
x-copy-from header is present in req.headers.
|
||||||
|
|
||||||
|
:param req: HTTP request object
|
||||||
|
:returns: A tuple with container name and object name
|
||||||
|
:raise: HTTPPreconditionFailed if x-copy-from value
|
||||||
|
is not well formatted.
|
||||||
|
"""
|
||||||
|
return _check_path_header(req, 'X-Copy-From', 2,
|
||||||
|
'X-Copy-From header must be of the form '
|
||||||
|
'<container name>/<object name>')
|
||||||
|
|
||||||
|
|
||||||
|
def _check_destination_header(req):
|
||||||
|
"""
|
||||||
|
Validate that the value from destination header is
|
||||||
|
well formatted. We assume the caller ensures that
|
||||||
|
destination header is present in req.headers.
|
||||||
|
|
||||||
|
:param req: HTTP request object
|
||||||
|
:returns: A tuple with container name and object name
|
||||||
|
:raise: HTTPPreconditionFailed if destination value
|
||||||
|
is not well formatted.
|
||||||
|
"""
|
||||||
|
return _check_path_header(req, 'Destination', 2,
|
||||||
|
'Destination header must be of the form '
|
||||||
|
'<container name>/<object name>')
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_headers_into(from_r, to_r):
|
||||||
|
"""
|
||||||
|
Will copy desired headers from from_r to to_r
|
||||||
|
:params from_r: a swob Request or Response
|
||||||
|
:params to_r: a swob Request or Response
|
||||||
|
"""
|
||||||
|
pass_headers = ['x-delete-at']
|
||||||
|
for k, v in from_r.headers.items():
|
||||||
|
if is_sys_or_user_meta('object', k) or k.lower() in pass_headers:
|
||||||
|
to_r.headers[k] = v
|
||||||
|
|
||||||
|
|
||||||
|
class ServerSideCopyWebContext(WSGIContext):
|
||||||
|
|
||||||
|
def __init__(self, app, logger):
|
||||||
|
super(ServerSideCopyWebContext, self).__init__(app)
|
||||||
|
self.app = app
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
def get_source_resp(self, req):
|
||||||
|
sub_req = make_subrequest(
|
||||||
|
req.environ, path=req.path_info, headers=req.headers,
|
||||||
|
swift_source='SSC')
|
||||||
|
return sub_req.get_response(self.app)
|
||||||
|
|
||||||
|
def send_put_req(self, req, additional_resp_headers, start_response):
|
||||||
|
app_resp = self._app_call(req.environ)
|
||||||
|
self._adjust_put_response(req, additional_resp_headers)
|
||||||
|
start_response(self._response_status,
|
||||||
|
self._response_headers,
|
||||||
|
self._response_exc_info)
|
||||||
|
return app_resp
|
||||||
|
|
||||||
|
def _adjust_put_response(self, req, additional_resp_headers):
|
||||||
|
if 'swift.post_as_copy' in req.environ:
|
||||||
|
# Older editions returned 202 Accepted on object POSTs, so we'll
|
||||||
|
# convert any 201 Created responses to that for compatibility with
|
||||||
|
# picky clients.
|
||||||
|
if self._get_status_int() == HTTP_CREATED:
|
||||||
|
self._response_status = '202 Accepted'
|
||||||
|
elif is_success(self._get_status_int()):
|
||||||
|
for header, value in additional_resp_headers.items():
|
||||||
|
self._response_headers.append((header, value))
|
||||||
|
|
||||||
|
def handle_OPTIONS_request(self, req, start_response):
|
||||||
|
app_resp = self._app_call(req.environ)
|
||||||
|
if is_success(self._get_status_int()):
|
||||||
|
for i, (header, value) in enumerate(self._response_headers):
|
||||||
|
if header.lower() == 'allow' and 'COPY' not in value:
|
||||||
|
self._response_headers[i] = ('Allow', value + ', COPY')
|
||||||
|
if header.lower() == 'access-control-allow-methods' and \
|
||||||
|
'COPY' not in value:
|
||||||
|
self._response_headers[i] = \
|
||||||
|
('Access-Control-Allow-Methods', value + ', COPY')
|
||||||
|
start_response(self._response_status,
|
||||||
|
self._response_headers,
|
||||||
|
self._response_exc_info)
|
||||||
|
return app_resp
|
||||||
|
|
||||||
|
|
||||||
|
class ServerSideCopyMiddleware(object):
|
||||||
|
|
||||||
|
def __init__(self, app, conf):
|
||||||
|
self.app = app
|
||||||
|
self.logger = get_logger(conf, log_route="copy")
|
||||||
|
# Read the old object_post_as_copy option from Proxy app just in case
|
||||||
|
# someone has set it to false (non default). This wouldn't cause
|
||||||
|
# problems during upgrade.
|
||||||
|
self._load_object_post_as_copy_conf(conf)
|
||||||
|
self.object_post_as_copy = \
|
||||||
|
config_true_value(conf.get('object_post_as_copy', 'true'))
|
||||||
|
|
||||||
|
def _load_object_post_as_copy_conf(self, conf):
|
||||||
|
if ('object_post_as_copy' in conf or '__file__' not in conf):
|
||||||
|
# Option is explicitly set in middleware conf. In that case,
|
||||||
|
# we assume operator knows what he's doing.
|
||||||
|
# This takes preference over the one set in proxy app
|
||||||
|
return
|
||||||
|
|
||||||
|
cp = ConfigParser()
|
||||||
|
if os.path.isdir(conf['__file__']):
|
||||||
|
read_conf_dir(cp, conf['__file__'])
|
||||||
|
else:
|
||||||
|
cp.read(conf['__file__'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
pipe = cp.get("pipeline:main", "pipeline")
|
||||||
|
except (NoSectionError, NoOptionError):
|
||||||
|
return
|
||||||
|
|
||||||
|
proxy_name = pipe.rsplit(None, 1)[-1]
|
||||||
|
proxy_section = "app:" + proxy_name
|
||||||
|
|
||||||
|
try:
|
||||||
|
conf['object_post_as_copy'] = cp.get(proxy_section,
|
||||||
|
'object_post_as_copy')
|
||||||
|
except (NoSectionError, NoOptionError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __call__(self, env, start_response):
|
||||||
|
req = Request(env)
|
||||||
|
try:
|
||||||
|
(version, account, container, obj) = req.split_path(4, 4, True)
|
||||||
|
except ValueError:
|
||||||
|
# If obj component is not present in req, do not proceed further.
|
||||||
|
return self.app(env, start_response)
|
||||||
|
|
||||||
|
self.account_name = account
|
||||||
|
self.container_name = container
|
||||||
|
self.object_name = obj
|
||||||
|
|
||||||
|
# Save off original request method (COPY/POST) in case it gets mutated
|
||||||
|
# into PUT during handling. This way logging can display the method
|
||||||
|
# the client actually sent.
|
||||||
|
req.environ['swift.orig_req_method'] = req.method
|
||||||
|
|
||||||
|
if req.method == 'PUT' and req.headers.get('X-Copy-From'):
|
||||||
|
return self.handle_PUT(req, start_response)
|
||||||
|
elif req.method == 'COPY':
|
||||||
|
return self.handle_COPY(req, start_response)
|
||||||
|
elif req.method == 'POST' and self.object_post_as_copy:
|
||||||
|
return self.handle_object_post_as_copy(req, start_response)
|
||||||
|
elif req.method == 'OPTIONS':
|
||||||
|
# Does not interfere with OPTIONS response from (account,container)
|
||||||
|
# servers and /info response.
|
||||||
|
return self.handle_OPTIONS(req, start_response)
|
||||||
|
|
||||||
|
return self.app(env, start_response)
|
||||||
|
|
||||||
|
def handle_object_post_as_copy(self, req, start_response):
|
||||||
|
req.method = 'PUT'
|
||||||
|
req.path_info = '/v1/%s/%s/%s' % (
|
||||||
|
self.account_name, self.container_name, self.object_name)
|
||||||
|
req.headers['Content-Length'] = 0
|
||||||
|
req.headers.pop('Range', None)
|
||||||
|
req.headers['X-Copy-From'] = quote('/%s/%s' % (self.container_name,
|
||||||
|
self.object_name))
|
||||||
|
req.environ['swift.post_as_copy'] = True
|
||||||
|
params = req.params
|
||||||
|
# for post-as-copy always copy the manifest itself if source is *LO
|
||||||
|
params['multipart-manifest'] = 'get'
|
||||||
|
req.params = params
|
||||||
|
return self.handle_PUT(req, start_response)
|
||||||
|
|
||||||
|
def handle_COPY(self, req, start_response):
|
||||||
|
if not req.headers.get('Destination'):
|
||||||
|
return HTTPPreconditionFailed(request=req,
|
||||||
|
body='Destination header required'
|
||||||
|
)(req.environ, start_response)
|
||||||
|
dest_account = self.account_name
|
||||||
|
if 'Destination-Account' in req.headers:
|
||||||
|
dest_account = req.headers.get('Destination-Account')
|
||||||
|
dest_account = check_account_format(req, dest_account)
|
||||||
|
req.headers['X-Copy-From-Account'] = self.account_name
|
||||||
|
self.account_name = dest_account
|
||||||
|
del req.headers['Destination-Account']
|
||||||
|
dest_container, dest_object = _check_destination_header(req)
|
||||||
|
source = '/%s/%s' % (self.container_name, self.object_name)
|
||||||
|
self.container_name = dest_container
|
||||||
|
self.object_name = dest_object
|
||||||
|
# re-write the existing request as a PUT instead of creating a new one
|
||||||
|
req.method = 'PUT'
|
||||||
|
# As this the path info is updated with destination container,
|
||||||
|
# the proxy server app will use the right object controller
|
||||||
|
# implementation corresponding to the container's policy type.
|
||||||
|
ver, _junk = req.split_path(1, 2, rest_with_last=True)
|
||||||
|
req.path_info = '/%s/%s/%s/%s' % \
|
||||||
|
(ver, dest_account, dest_container, dest_object)
|
||||||
|
req.headers['Content-Length'] = 0
|
||||||
|
req.headers['X-Copy-From'] = quote(source)
|
||||||
|
del req.headers['Destination']
|
||||||
|
return self.handle_PUT(req, start_response)
|
||||||
|
|
||||||
|
def _get_source_object(self, ssc_ctx, source_path, req):
|
||||||
|
source_req = req.copy_get()
|
||||||
|
|
||||||
|
# make sure the source request uses it's container_info
|
||||||
|
source_req.headers.pop('X-Backend-Storage-Policy-Index', None)
|
||||||
|
source_req.path_info = quote(source_path)
|
||||||
|
source_req.headers['X-Newest'] = 'true'
|
||||||
|
if 'swift.post_as_copy' in req.environ:
|
||||||
|
# We're COPYing one object over itself because of a POST; rely on
|
||||||
|
# the PUT for write authorization, don't require read authorization
|
||||||
|
source_req.environ['swift.authorize'] = lambda req: None
|
||||||
|
source_req.environ['swift.authorize_override'] = True
|
||||||
|
|
||||||
|
# in case we are copying an SLO manifest, set format=raw parameter
|
||||||
|
params = source_req.params
|
||||||
|
if params.get('multipart-manifest') == 'get':
|
||||||
|
params['format'] = 'raw'
|
||||||
|
source_req.params = params
|
||||||
|
|
||||||
|
source_resp = ssc_ctx.get_source_resp(source_req)
|
||||||
|
|
||||||
|
if source_resp.content_length is None:
|
||||||
|
# This indicates a transfer-encoding: chunked source object,
|
||||||
|
# which currently only happens because there are more than
|
||||||
|
# CONTAINER_LISTING_LIMIT segments in a segmented object. In
|
||||||
|
# this case, we're going to refuse to do the server-side copy.
|
||||||
|
return HTTPRequestEntityTooLarge(request=req)
|
||||||
|
|
||||||
|
if source_resp.content_length > MAX_FILE_SIZE:
|
||||||
|
return HTTPRequestEntityTooLarge(request=req)
|
||||||
|
|
||||||
|
return source_resp
|
||||||
|
|
||||||
|
def _create_response_headers(self, source_path, source_resp, sink_req):
|
||||||
|
resp_headers = dict()
|
||||||
|
acct, path = source_path.split('/', 3)[2:4]
|
||||||
|
resp_headers['X-Copied-From-Account'] = quote(acct)
|
||||||
|
resp_headers['X-Copied-From'] = quote(path)
|
||||||
|
if 'last-modified' in source_resp.headers:
|
||||||
|
resp_headers['X-Copied-From-Last-Modified'] = \
|
||||||
|
source_resp.headers['last-modified']
|
||||||
|
# Existing sys and user meta of source object is added to response
|
||||||
|
# headers in addition to the new ones.
|
||||||
|
for k, v in sink_req.headers.items():
|
||||||
|
if is_sys_or_user_meta('object', k) or k.lower() == 'x-delete-at':
|
||||||
|
resp_headers[k] = v
|
||||||
|
return resp_headers
|
||||||
|
|
||||||
|
def handle_PUT(self, req, start_response):
|
||||||
|
if req.content_length:
|
||||||
|
return HTTPBadRequest(body='Copy requests require a zero byte '
|
||||||
|
'body', request=req,
|
||||||
|
content_type='text/plain')(req.environ,
|
||||||
|
start_response)
|
||||||
|
|
||||||
|
# Form the path of source object to be fetched
|
||||||
|
ver, acct, _rest = req.split_path(2, 3, True)
|
||||||
|
src_account_name = req.headers.get('X-Copy-From-Account')
|
||||||
|
if src_account_name:
|
||||||
|
src_account_name = check_account_format(req, src_account_name)
|
||||||
|
else:
|
||||||
|
src_account_name = acct
|
||||||
|
src_container_name, src_obj_name = _check_copy_from_header(req)
|
||||||
|
source_path = '/%s/%s/%s/%s' % (ver, src_account_name,
|
||||||
|
src_container_name, src_obj_name)
|
||||||
|
|
||||||
|
if req.environ.get('swift.orig_req_method', req.method) != 'POST':
|
||||||
|
self.logger.info("Copying object from %s to %s" %
|
||||||
|
(source_path, req.path))
|
||||||
|
|
||||||
|
# GET the source object, bail out on error
|
||||||
|
ssc_ctx = ServerSideCopyWebContext(self.app, self.logger)
|
||||||
|
source_resp = self._get_source_object(ssc_ctx, source_path, req)
|
||||||
|
if source_resp.status_int >= HTTP_MULTIPLE_CHOICES:
|
||||||
|
close_if_possible(source_resp.app_iter)
|
||||||
|
return source_resp(source_resp.environ, start_response)
|
||||||
|
|
||||||
|
# Create a new Request object based on the original req instance.
|
||||||
|
# This will preserve env and headers.
|
||||||
|
sink_req = Request.blank(req.path_info,
|
||||||
|
environ=req.environ, headers=req.headers)
|
||||||
|
|
||||||
|
params = sink_req.params
|
||||||
|
if params.get('multipart-manifest') == 'get':
|
||||||
|
if 'X-Static-Large-Object' in source_resp.headers:
|
||||||
|
params['multipart-manifest'] = 'put'
|
||||||
|
if 'X-Object-Manifest' in source_resp.headers:
|
||||||
|
del params['multipart-manifest']
|
||||||
|
sink_req.headers['X-Object-Manifest'] = \
|
||||||
|
source_resp.headers['X-Object-Manifest']
|
||||||
|
sink_req.params = params
|
||||||
|
|
||||||
|
# Set data source, content length and etag for the PUT request
|
||||||
|
sink_req.environ['wsgi.input'] = FileLikeIter(source_resp.app_iter)
|
||||||
|
sink_req.content_length = source_resp.content_length
|
||||||
|
sink_req.etag = source_resp.etag
|
||||||
|
|
||||||
|
# We no longer need these headers
|
||||||
|
sink_req.headers.pop('X-Copy-From', None)
|
||||||
|
sink_req.headers.pop('X-Copy-From-Account', None)
|
||||||
|
# If the copy request does not explicitly override content-type,
|
||||||
|
# use the one present in the source object.
|
||||||
|
if not req.headers.get('content-type'):
|
||||||
|
sink_req.headers['Content-Type'] = \
|
||||||
|
source_resp.headers['Content-Type']
|
||||||
|
|
||||||
|
fresh_meta_flag = config_true_value(
|
||||||
|
sink_req.headers.get('x-fresh-metadata', 'false'))
|
||||||
|
|
||||||
|
if fresh_meta_flag or 'swift.post_as_copy' in sink_req.environ:
|
||||||
|
# Post-as-copy: ignore new sysmeta, copy existing sysmeta
|
||||||
|
condition = lambda k: is_sys_meta('object', k)
|
||||||
|
remove_items(sink_req.headers, condition)
|
||||||
|
copy_header_subset(source_resp, sink_req, condition)
|
||||||
|
else:
|
||||||
|
# Copy/update existing sysmeta and user meta
|
||||||
|
_copy_headers_into(source_resp, sink_req)
|
||||||
|
# Copy/update new metadata provided in request if any
|
||||||
|
_copy_headers_into(req, sink_req)
|
||||||
|
|
||||||
|
# Create response headers for PUT response
|
||||||
|
resp_headers = self._create_response_headers(source_path,
|
||||||
|
source_resp, sink_req)
|
||||||
|
|
||||||
|
put_resp = ssc_ctx.send_put_req(sink_req, resp_headers, start_response)
|
||||||
|
close_if_possible(source_resp.app_iter)
|
||||||
|
return put_resp
|
||||||
|
|
||||||
|
def handle_OPTIONS(self, req, start_response):
|
||||||
|
return ServerSideCopyWebContext(self.app, self.logger).\
|
||||||
|
handle_OPTIONS_request(req, start_response)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_factory(global_conf, **local_conf):
|
||||||
|
conf = global_conf.copy()
|
||||||
|
conf.update(local_conf)
|
||||||
|
|
||||||
|
def copy_filter(app):
|
||||||
|
return ServerSideCopyMiddleware(app, conf)
|
||||||
|
|
||||||
|
return copy_filter
|
@ -405,11 +405,6 @@ class DynamicLargeObject(object):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return self.app(env, start_response)
|
return self.app(env, start_response)
|
||||||
|
|
||||||
# install our COPY-callback hook
|
|
||||||
env['swift.copy_hook'] = self.copy_hook(
|
|
||||||
env.get('swift.copy_hook',
|
|
||||||
lambda src_req, src_resp, sink_req: src_resp))
|
|
||||||
|
|
||||||
if ((req.method == 'GET' or req.method == 'HEAD') and
|
if ((req.method == 'GET' or req.method == 'HEAD') and
|
||||||
req.params.get('multipart-manifest') != 'get'):
|
req.params.get('multipart-manifest') != 'get'):
|
||||||
return GetContext(self, self.logger).\
|
return GetContext(self, self.logger).\
|
||||||
@ -438,24 +433,6 @@ class DynamicLargeObject(object):
|
|||||||
body=('X-Object-Manifest must be in the '
|
body=('X-Object-Manifest must be in the '
|
||||||
'format container/prefix'))
|
'format container/prefix'))
|
||||||
|
|
||||||
def copy_hook(self, inner_hook):
|
|
||||||
|
|
||||||
def dlo_copy_hook(source_req, source_resp, sink_req):
|
|
||||||
x_o_m = source_resp.headers.get('X-Object-Manifest')
|
|
||||||
if x_o_m:
|
|
||||||
if source_req.params.get('multipart-manifest') == 'get':
|
|
||||||
# To copy the manifest, we let the copy proceed as normal,
|
|
||||||
# but ensure that X-Object-Manifest is set on the new
|
|
||||||
# object.
|
|
||||||
sink_req.headers['X-Object-Manifest'] = x_o_m
|
|
||||||
else:
|
|
||||||
ctx = GetContext(self, self.logger)
|
|
||||||
source_resp = ctx.get_or_head_response(
|
|
||||||
source_req, x_o_m, source_resp.headers.items())
|
|
||||||
return inner_hook(source_req, source_resp, sink_req)
|
|
||||||
|
|
||||||
return dlo_copy_hook
|
|
||||||
|
|
||||||
|
|
||||||
def filter_factory(global_conf, **local_conf):
|
def filter_factory(global_conf, **local_conf):
|
||||||
conf = global_conf.copy()
|
conf = global_conf.copy()
|
||||||
|
@ -798,20 +798,6 @@ class StaticLargeObject(object):
|
|||||||
"""
|
"""
|
||||||
return SloGetContext(self).handle_slo_get_or_head(req, start_response)
|
return SloGetContext(self).handle_slo_get_or_head(req, start_response)
|
||||||
|
|
||||||
def copy_hook(self, inner_hook):
|
|
||||||
|
|
||||||
def slo_hook(source_req, source_resp, sink_req):
|
|
||||||
x_slo = source_resp.headers.get('X-Static-Large-Object')
|
|
||||||
if (config_true_value(x_slo)
|
|
||||||
and source_req.params.get('multipart-manifest') != 'get'
|
|
||||||
and 'swift.post_as_copy' not in source_req.environ):
|
|
||||||
source_resp = SloGetContext(self).get_or_head_response(
|
|
||||||
source_req, source_resp.headers.items(),
|
|
||||||
source_resp.app_iter)
|
|
||||||
return inner_hook(source_req, source_resp, sink_req)
|
|
||||||
|
|
||||||
return slo_hook
|
|
||||||
|
|
||||||
def handle_multipart_put(self, req, start_response):
|
def handle_multipart_put(self, req, start_response):
|
||||||
"""
|
"""
|
||||||
Will handle the PUT of a SLO manifest.
|
Will handle the PUT of a SLO manifest.
|
||||||
@ -1058,11 +1044,6 @@ class StaticLargeObject(object):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return self.app(env, start_response)
|
return self.app(env, start_response)
|
||||||
|
|
||||||
# install our COPY-callback hook
|
|
||||||
env['swift.copy_hook'] = self.copy_hook(
|
|
||||||
env.get('swift.copy_hook',
|
|
||||||
lambda src_req, src_resp, sink_req: src_resp))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if req.method == 'PUT' and \
|
if req.method == 'PUT' and \
|
||||||
req.params.get('multipart-manifest') == 'put':
|
req.params.get('multipart-manifest') == 'put':
|
||||||
|
@ -127,9 +127,7 @@ from swift.common.request_helpers import get_sys_meta_prefix, \
|
|||||||
from swift.common.wsgi import WSGIContext, make_pre_authed_request
|
from swift.common.wsgi import WSGIContext, make_pre_authed_request
|
||||||
from swift.common.swob import (
|
from swift.common.swob import (
|
||||||
Request, HTTPException, HTTPRequestEntityTooLarge)
|
Request, HTTPException, HTTPRequestEntityTooLarge)
|
||||||
from swift.common.constraints import (
|
from swift.common.constraints import check_container_format, MAX_FILE_SIZE
|
||||||
check_account_format, check_container_format, check_destination_header,
|
|
||||||
MAX_FILE_SIZE)
|
|
||||||
from swift.proxy.controllers.base import get_container_info
|
from swift.proxy.controllers.base import get_container_info
|
||||||
from swift.common.http import (
|
from swift.common.http import (
|
||||||
is_success, is_client_error, HTTP_NOT_FOUND)
|
is_success, is_client_error, HTTP_NOT_FOUND)
|
||||||
@ -493,24 +491,10 @@ class VersionedWritesMiddleware(object):
|
|||||||
account_name = unquote(account)
|
account_name = unquote(account)
|
||||||
container_name = unquote(container)
|
container_name = unquote(container)
|
||||||
object_name = unquote(obj)
|
object_name = unquote(obj)
|
||||||
container_info = None
|
|
||||||
resp = None
|
resp = None
|
||||||
is_enabled = config_true_value(allow_versioned_writes)
|
is_enabled = config_true_value(allow_versioned_writes)
|
||||||
if req.method in ('PUT', 'DELETE'):
|
container_info = get_container_info(
|
||||||
container_info = get_container_info(
|
req.environ, self.app)
|
||||||
req.environ, self.app)
|
|
||||||
elif req.method == 'COPY' and 'Destination' in req.headers:
|
|
||||||
if 'Destination-Account' in req.headers:
|
|
||||||
account_name = req.headers.get('Destination-Account')
|
|
||||||
account_name = check_account_format(req, account_name)
|
|
||||||
container_name, object_name = check_destination_header(req)
|
|
||||||
req.environ['PATH_INFO'] = "/%s/%s/%s/%s" % (
|
|
||||||
api_version, account_name, container_name, object_name)
|
|
||||||
container_info = get_container_info(
|
|
||||||
req.environ, self.app)
|
|
||||||
|
|
||||||
if not container_info:
|
|
||||||
return self.app
|
|
||||||
|
|
||||||
# To maintain backwards compatibility, container version
|
# To maintain backwards compatibility, container version
|
||||||
# location could be stored as sysmeta or not, need to check both.
|
# location could be stored as sysmeta or not, need to check both.
|
||||||
@ -530,7 +514,7 @@ class VersionedWritesMiddleware(object):
|
|||||||
if is_enabled and versions_cont:
|
if is_enabled and versions_cont:
|
||||||
versions_cont = unquote(versions_cont).split('/')[0]
|
versions_cont = unquote(versions_cont).split('/')[0]
|
||||||
vw_ctx = VersionedWritesContext(self.app, self.logger)
|
vw_ctx = VersionedWritesContext(self.app, self.logger)
|
||||||
if req.method in ('PUT', 'COPY'):
|
if req.method == 'PUT':
|
||||||
resp = vw_ctx.handle_obj_versions_put(
|
resp = vw_ctx.handle_obj_versions_put(
|
||||||
req, versions_cont, api_version, account_name,
|
req, versions_cont, api_version, account_name,
|
||||||
object_name)
|
object_name)
|
||||||
@ -545,10 +529,7 @@ class VersionedWritesMiddleware(object):
|
|||||||
return self.app
|
return self.app
|
||||||
|
|
||||||
def __call__(self, env, start_response):
|
def __call__(self, env, start_response):
|
||||||
# making a duplicate, because if this is a COPY request, we will
|
req = Request(env)
|
||||||
# modify the PATH_INFO to find out if the 'Destination' is in a
|
|
||||||
# versioned container
|
|
||||||
req = Request(env.copy())
|
|
||||||
try:
|
try:
|
||||||
(api_version, account, container, obj) = req.split_path(3, 4, True)
|
(api_version, account, container, obj) = req.split_path(3, 4, True)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -576,7 +557,8 @@ class VersionedWritesMiddleware(object):
|
|||||||
allow_versioned_writes)
|
allow_versioned_writes)
|
||||||
except HTTPException as error_response:
|
except HTTPException as error_response:
|
||||||
return error_response(env, start_response)
|
return error_response(env, start_response)
|
||||||
elif obj and req.method in ('PUT', 'COPY', 'DELETE'):
|
elif (obj and req.method in ('PUT', 'DELETE') and
|
||||||
|
not req.environ.get('swift.post_as_copy')):
|
||||||
try:
|
try:
|
||||||
return self.object_request(
|
return self.object_request(
|
||||||
req, api_version, account, container, obj,
|
req, api_version, account, container, obj,
|
||||||
|
@ -888,6 +888,11 @@ class Request(object):
|
|||||||
return self._params_cache
|
return self._params_cache
|
||||||
str_params = params
|
str_params = params
|
||||||
|
|
||||||
|
@params.setter
|
||||||
|
def params(self, param_pairs):
|
||||||
|
self._params_cache = None
|
||||||
|
self.query_string = urllib.parse.urlencode(param_pairs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timestamp(self):
|
def timestamp(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1100,7 +1100,7 @@ def make_env(env, method=None, path=None, agent='Swift', query_string=None,
|
|||||||
'SERVER_PROTOCOL', 'swift.cache', 'swift.source',
|
'SERVER_PROTOCOL', 'swift.cache', 'swift.source',
|
||||||
'swift.trans_id', 'swift.authorize_override',
|
'swift.trans_id', 'swift.authorize_override',
|
||||||
'swift.authorize', 'HTTP_X_USER_ID', 'HTTP_X_PROJECT_ID',
|
'swift.authorize', 'HTTP_X_USER_ID', 'HTTP_X_PROJECT_ID',
|
||||||
'HTTP_REFERER'):
|
'HTTP_REFERER', 'swift.orig_req_method', 'swift.log_info'):
|
||||||
if name in env:
|
if name in env:
|
||||||
newenv[name] = env[name]
|
newenv[name] = env[name]
|
||||||
if method:
|
if method:
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
# collected. We've seen objects hang around forever otherwise.
|
# collected. We've seen objects hang around forever otherwise.
|
||||||
|
|
||||||
import six
|
import six
|
||||||
from six.moves.urllib.parse import unquote, quote
|
from six.moves.urllib.parse import unquote
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import itertools
|
import itertools
|
||||||
@ -49,9 +49,7 @@ from swift.common.utils import (
|
|||||||
document_iters_to_http_response_body, parse_content_range,
|
document_iters_to_http_response_body, parse_content_range,
|
||||||
quorum_size, reiterate, close_if_possible)
|
quorum_size, reiterate, close_if_possible)
|
||||||
from swift.common.bufferedhttp import http_connect
|
from swift.common.bufferedhttp import http_connect
|
||||||
from swift.common.constraints import check_metadata, check_object_creation, \
|
from swift.common.constraints import check_metadata, check_object_creation
|
||||||
check_copy_from_header, check_destination_header, \
|
|
||||||
check_account_format
|
|
||||||
from swift.common import constraints
|
from swift.common import constraints
|
||||||
from swift.common.exceptions import ChunkReadTimeout, \
|
from swift.common.exceptions import ChunkReadTimeout, \
|
||||||
ChunkWriteTimeout, ConnectionTimeout, ResponseTimeout, \
|
ChunkWriteTimeout, ConnectionTimeout, ResponseTimeout, \
|
||||||
@ -60,33 +58,19 @@ from swift.common.exceptions import ChunkReadTimeout, \
|
|||||||
from swift.common.header_key_dict import HeaderKeyDict
|
from swift.common.header_key_dict import HeaderKeyDict
|
||||||
from swift.common.http import (
|
from swift.common.http import (
|
||||||
is_informational, is_success, is_client_error, is_server_error,
|
is_informational, is_success, is_client_error, is_server_error,
|
||||||
is_redirection, HTTP_CONTINUE, HTTP_CREATED, HTTP_MULTIPLE_CHOICES,
|
is_redirection, HTTP_CONTINUE, HTTP_INTERNAL_SERVER_ERROR,
|
||||||
HTTP_INTERNAL_SERVER_ERROR, HTTP_SERVICE_UNAVAILABLE,
|
HTTP_SERVICE_UNAVAILABLE, HTTP_INSUFFICIENT_STORAGE,
|
||||||
HTTP_INSUFFICIENT_STORAGE, HTTP_PRECONDITION_FAILED, HTTP_CONFLICT,
|
HTTP_PRECONDITION_FAILED, HTTP_CONFLICT, HTTP_UNPROCESSABLE_ENTITY,
|
||||||
HTTP_UNPROCESSABLE_ENTITY, HTTP_REQUESTED_RANGE_NOT_SATISFIABLE)
|
HTTP_REQUESTED_RANGE_NOT_SATISFIABLE)
|
||||||
from swift.common.storage_policy import (POLICIES, REPL_POLICY, EC_POLICY,
|
from swift.common.storage_policy import (POLICIES, REPL_POLICY, EC_POLICY,
|
||||||
ECDriverError, PolicyError)
|
ECDriverError, PolicyError)
|
||||||
from swift.proxy.controllers.base import Controller, delay_denial, \
|
from swift.proxy.controllers.base import Controller, delay_denial, \
|
||||||
cors_validation, ResumingGetter
|
cors_validation, ResumingGetter
|
||||||
from swift.common.swob import HTTPAccepted, HTTPBadRequest, HTTPNotFound, \
|
from swift.common.swob import HTTPAccepted, HTTPBadRequest, HTTPNotFound, \
|
||||||
HTTPPreconditionFailed, HTTPRequestEntityTooLarge, HTTPRequestTimeout, \
|
HTTPPreconditionFailed, HTTPRequestEntityTooLarge, HTTPRequestTimeout, \
|
||||||
HTTPServerError, HTTPServiceUnavailable, Request, \
|
HTTPServerError, HTTPServiceUnavailable, HTTPClientDisconnect, \
|
||||||
HTTPClientDisconnect, HTTPUnprocessableEntity, Response, HTTPException, \
|
HTTPUnprocessableEntity, Response, HTTPException, \
|
||||||
HTTPRequestedRangeNotSatisfiable, Range, HTTPInternalServerError
|
HTTPRequestedRangeNotSatisfiable, Range, HTTPInternalServerError
|
||||||
from swift.common.request_helpers import is_sys_or_user_meta, is_sys_meta, \
|
|
||||||
remove_items, copy_header_subset
|
|
||||||
|
|
||||||
|
|
||||||
def copy_headers_into(from_r, to_r):
|
|
||||||
"""
|
|
||||||
Will copy desired headers from from_r to to_r
|
|
||||||
:params from_r: a swob Request or Response
|
|
||||||
:params to_r: a swob Request or Response
|
|
||||||
"""
|
|
||||||
pass_headers = ['x-delete-at']
|
|
||||||
for k, v in from_r.headers.items():
|
|
||||||
if is_sys_or_user_meta('object', k) or k.lower() in pass_headers:
|
|
||||||
to_r.headers[k] = v
|
|
||||||
|
|
||||||
|
|
||||||
def check_content_type(req):
|
def check_content_type(req):
|
||||||
@ -200,8 +184,7 @@ class BaseObjectController(Controller):
|
|||||||
self.account_name, self.container_name, self.object_name)
|
self.account_name, self.container_name, self.object_name)
|
||||||
node_iter = self.app.iter_nodes(obj_ring, partition)
|
node_iter = self.app.iter_nodes(obj_ring, partition)
|
||||||
|
|
||||||
resp = self._reroute(policy)._get_or_head_response(
|
resp = self._get_or_head_response(req, node_iter, partition, policy)
|
||||||
req, node_iter, partition, policy)
|
|
||||||
|
|
||||||
if ';' in resp.headers.get('content-type', ''):
|
if ';' in resp.headers.get('content-type', ''):
|
||||||
resp.content_type = clean_content_type(
|
resp.content_type = clean_content_type(
|
||||||
@ -227,55 +210,38 @@ class BaseObjectController(Controller):
|
|||||||
@delay_denial
|
@delay_denial
|
||||||
def POST(self, req):
|
def POST(self, req):
|
||||||
"""HTTP POST request handler."""
|
"""HTTP POST request handler."""
|
||||||
if self.app.object_post_as_copy:
|
error_response = check_metadata(req, 'object')
|
||||||
req.method = 'PUT'
|
if error_response:
|
||||||
req.path_info = '/v1/%s/%s/%s' % (
|
return error_response
|
||||||
self.account_name, self.container_name, self.object_name)
|
container_info = self.container_info(
|
||||||
req.headers['Content-Length'] = 0
|
self.account_name, self.container_name, req)
|
||||||
req.headers['X-Copy-From'] = quote('/%s/%s' % (self.container_name,
|
container_partition = container_info['partition']
|
||||||
self.object_name))
|
containers = container_info['nodes']
|
||||||
req.environ['swift.post_as_copy'] = True
|
req.acl = container_info['write_acl']
|
||||||
req.environ['swift_versioned_copy'] = True
|
if 'swift.authorize' in req.environ:
|
||||||
resp = self.PUT(req)
|
aresp = req.environ['swift.authorize'](req)
|
||||||
# Older editions returned 202 Accepted on object POSTs, so we'll
|
if aresp:
|
||||||
# convert any 201 Created responses to that for compatibility with
|
return aresp
|
||||||
# picky clients.
|
if not containers:
|
||||||
if resp.status_int != HTTP_CREATED:
|
return HTTPNotFound(request=req)
|
||||||
return resp
|
|
||||||
return HTTPAccepted(request=req)
|
|
||||||
else:
|
|
||||||
error_response = check_metadata(req, 'object')
|
|
||||||
if error_response:
|
|
||||||
return error_response
|
|
||||||
container_info = self.container_info(
|
|
||||||
self.account_name, self.container_name, req)
|
|
||||||
container_partition = container_info['partition']
|
|
||||||
containers = container_info['nodes']
|
|
||||||
req.acl = container_info['write_acl']
|
|
||||||
if 'swift.authorize' in req.environ:
|
|
||||||
aresp = req.environ['swift.authorize'](req)
|
|
||||||
if aresp:
|
|
||||||
return aresp
|
|
||||||
if not containers:
|
|
||||||
return HTTPNotFound(request=req)
|
|
||||||
|
|
||||||
req, delete_at_container, delete_at_part, \
|
req, delete_at_container, delete_at_part, \
|
||||||
delete_at_nodes = self._config_obj_expiration(req)
|
delete_at_nodes = self._config_obj_expiration(req)
|
||||||
|
|
||||||
# pass the policy index to storage nodes via req header
|
# pass the policy index to storage nodes via req header
|
||||||
policy_index = req.headers.get('X-Backend-Storage-Policy-Index',
|
policy_index = req.headers.get('X-Backend-Storage-Policy-Index',
|
||||||
container_info['storage_policy'])
|
container_info['storage_policy'])
|
||||||
obj_ring = self.app.get_object_ring(policy_index)
|
obj_ring = self.app.get_object_ring(policy_index)
|
||||||
req.headers['X-Backend-Storage-Policy-Index'] = policy_index
|
req.headers['X-Backend-Storage-Policy-Index'] = policy_index
|
||||||
partition, nodes = obj_ring.get_nodes(
|
partition, nodes = obj_ring.get_nodes(
|
||||||
self.account_name, self.container_name, self.object_name)
|
self.account_name, self.container_name, self.object_name)
|
||||||
|
|
||||||
req.headers['X-Timestamp'] = Timestamp(time.time()).internal
|
req.headers['X-Timestamp'] = Timestamp(time.time()).internal
|
||||||
|
|
||||||
headers = self._backend_requests(
|
headers = self._backend_requests(
|
||||||
req, len(nodes), container_partition, containers,
|
req, len(nodes), container_partition, containers,
|
||||||
delete_at_container, delete_at_part, delete_at_nodes)
|
delete_at_container, delete_at_part, delete_at_nodes)
|
||||||
return self._post_object(req, obj_ring, partition, headers)
|
return self._post_object(req, obj_ring, partition, headers)
|
||||||
|
|
||||||
def _backend_requests(self, req, n_outgoing,
|
def _backend_requests(self, req, n_outgoing,
|
||||||
container_partition, containers,
|
container_partition, containers,
|
||||||
@ -414,133 +380,8 @@ class BaseObjectController(Controller):
|
|||||||
|
|
||||||
return req, delete_at_container, delete_at_part, delete_at_nodes
|
return req, delete_at_container, delete_at_part, delete_at_nodes
|
||||||
|
|
||||||
def _handle_copy_request(self, req):
|
|
||||||
"""
|
|
||||||
This method handles copying objects based on values set in the headers
|
|
||||||
'X-Copy-From' and 'X-Copy-From-Account'
|
|
||||||
|
|
||||||
Note that if the incomming request has some conditional headers (e.g.
|
|
||||||
'Range', 'If-Match'), *source* object will be evaluated for these
|
|
||||||
headers. i.e. if PUT with both 'X-Copy-From' and 'Range', Swift will
|
|
||||||
make a partial copy as a new object.
|
|
||||||
|
|
||||||
This method was added as part of the refactoring of the PUT method and
|
|
||||||
the functionality is expected to be moved to middleware
|
|
||||||
"""
|
|
||||||
if req.environ.get('swift.orig_req_method', req.method) != 'POST':
|
|
||||||
req.environ.setdefault('swift.log_info', []).append(
|
|
||||||
'x-copy-from:%s' % req.headers['X-Copy-From'])
|
|
||||||
ver, acct, _rest = req.split_path(2, 3, True)
|
|
||||||
src_account_name = req.headers.get('X-Copy-From-Account', None)
|
|
||||||
if src_account_name:
|
|
||||||
src_account_name = check_account_format(req, src_account_name)
|
|
||||||
else:
|
|
||||||
src_account_name = acct
|
|
||||||
src_container_name, src_obj_name = check_copy_from_header(req)
|
|
||||||
source_header = '/%s/%s/%s/%s' % (
|
|
||||||
ver, src_account_name, src_container_name, src_obj_name)
|
|
||||||
source_req = req.copy_get()
|
|
||||||
|
|
||||||
# make sure the source request uses it's container_info
|
|
||||||
source_req.headers.pop('X-Backend-Storage-Policy-Index', None)
|
|
||||||
source_req.path_info = source_header
|
|
||||||
source_req.headers['X-Newest'] = 'true'
|
|
||||||
if 'swift.post_as_copy' in req.environ:
|
|
||||||
# We're COPYing one object over itself because of a POST; rely on
|
|
||||||
# the PUT for write authorization, don't require read authorization
|
|
||||||
source_req.environ['swift.authorize'] = lambda req: None
|
|
||||||
source_req.environ['swift.authorize_override'] = True
|
|
||||||
|
|
||||||
orig_obj_name = self.object_name
|
|
||||||
orig_container_name = self.container_name
|
|
||||||
orig_account_name = self.account_name
|
|
||||||
sink_req = Request.blank(req.path_info,
|
|
||||||
environ=req.environ, headers=req.headers)
|
|
||||||
|
|
||||||
self.object_name = src_obj_name
|
|
||||||
self.container_name = src_container_name
|
|
||||||
self.account_name = src_account_name
|
|
||||||
|
|
||||||
source_resp = self.GET(source_req)
|
|
||||||
|
|
||||||
# This gives middlewares a way to change the source; for example,
|
|
||||||
# this lets you COPY a SLO manifest and have the new object be the
|
|
||||||
# concatenation of the segments (like what a GET request gives
|
|
||||||
# the client), not a copy of the manifest file.
|
|
||||||
hook = req.environ.get(
|
|
||||||
'swift.copy_hook',
|
|
||||||
(lambda source_req, source_resp, sink_req: source_resp))
|
|
||||||
source_resp = hook(source_req, source_resp, sink_req)
|
|
||||||
|
|
||||||
# reset names
|
|
||||||
self.object_name = orig_obj_name
|
|
||||||
self.container_name = orig_container_name
|
|
||||||
self.account_name = orig_account_name
|
|
||||||
|
|
||||||
if source_resp.status_int >= HTTP_MULTIPLE_CHOICES:
|
|
||||||
# this is a bit of ugly code, but I'm willing to live with it
|
|
||||||
# until copy request handling moves to middleware
|
|
||||||
return source_resp, None, None, None
|
|
||||||
if source_resp.content_length is None:
|
|
||||||
# This indicates a transfer-encoding: chunked source object,
|
|
||||||
# which currently only happens because there are more than
|
|
||||||
# CONTAINER_LISTING_LIMIT segments in a segmented object. In
|
|
||||||
# this case, we're going to refuse to do the server-side copy.
|
|
||||||
raise HTTPRequestEntityTooLarge(request=req)
|
|
||||||
if source_resp.content_length > constraints.MAX_FILE_SIZE:
|
|
||||||
raise HTTPRequestEntityTooLarge(request=req)
|
|
||||||
|
|
||||||
data_source = iter(source_resp.app_iter)
|
|
||||||
sink_req.content_length = source_resp.content_length
|
|
||||||
sink_req.etag = source_resp.etag
|
|
||||||
|
|
||||||
# we no longer need the X-Copy-From header
|
|
||||||
del sink_req.headers['X-Copy-From']
|
|
||||||
if 'X-Copy-From-Account' in sink_req.headers:
|
|
||||||
del sink_req.headers['X-Copy-From-Account']
|
|
||||||
if not req.content_type_manually_set:
|
|
||||||
sink_req.headers['Content-Type'] = \
|
|
||||||
source_resp.headers['Content-Type']
|
|
||||||
|
|
||||||
fresh_meta_flag = config_true_value(
|
|
||||||
sink_req.headers.get('x-fresh-metadata', 'false'))
|
|
||||||
|
|
||||||
if fresh_meta_flag or 'swift.post_as_copy' in sink_req.environ:
|
|
||||||
# post-as-copy: ignore new sysmeta, copy existing sysmeta
|
|
||||||
condition = lambda k: is_sys_meta('object', k)
|
|
||||||
remove_items(sink_req.headers, condition)
|
|
||||||
copy_header_subset(source_resp, sink_req, condition)
|
|
||||||
else:
|
|
||||||
# copy/update existing sysmeta and user meta
|
|
||||||
copy_headers_into(source_resp, sink_req)
|
|
||||||
copy_headers_into(req, sink_req)
|
|
||||||
|
|
||||||
# copy over x-static-large-object for POSTs and manifest copies
|
|
||||||
if 'X-Static-Large-Object' in source_resp.headers and \
|
|
||||||
(req.params.get('multipart-manifest') == 'get' or
|
|
||||||
'swift.post_as_copy' in req.environ):
|
|
||||||
sink_req.headers['X-Static-Large-Object'] = \
|
|
||||||
source_resp.headers['X-Static-Large-Object']
|
|
||||||
|
|
||||||
req = sink_req
|
|
||||||
|
|
||||||
def update_response(req, resp):
|
|
||||||
acct, path = source_resp.environ['PATH_INFO'].split('/', 3)[2:4]
|
|
||||||
resp.headers['X-Copied-From-Account'] = quote(acct)
|
|
||||||
resp.headers['X-Copied-From'] = quote(path)
|
|
||||||
if 'last-modified' in source_resp.headers:
|
|
||||||
resp.headers['X-Copied-From-Last-Modified'] = \
|
|
||||||
source_resp.headers['last-modified']
|
|
||||||
copy_headers_into(req, resp)
|
|
||||||
return resp
|
|
||||||
|
|
||||||
# this is a bit of ugly code, but I'm willing to live with it
|
|
||||||
# until copy request handling moves to middleware
|
|
||||||
return None, req, data_source, update_response
|
|
||||||
|
|
||||||
def _update_content_type(self, req):
|
def _update_content_type(self, req):
|
||||||
# Sometimes the 'content-type' header exists, but is set to None.
|
# Sometimes the 'content-type' header exists, but is set to None.
|
||||||
req.content_type_manually_set = True
|
|
||||||
detect_content_type = \
|
detect_content_type = \
|
||||||
config_true_value(req.headers.get('x-detect-content-type'))
|
config_true_value(req.headers.get('x-detect-content-type'))
|
||||||
if detect_content_type or not req.headers.get('content-type'):
|
if detect_content_type or not req.headers.get('content-type'):
|
||||||
@ -549,8 +390,6 @@ class BaseObjectController(Controller):
|
|||||||
'application/octet-stream'
|
'application/octet-stream'
|
||||||
if detect_content_type:
|
if detect_content_type:
|
||||||
req.headers.pop('x-detect-content-type')
|
req.headers.pop('x-detect-content-type')
|
||||||
else:
|
|
||||||
req.content_type_manually_set = False
|
|
||||||
|
|
||||||
def _update_x_timestamp(self, req):
|
def _update_x_timestamp(self, req):
|
||||||
# Used by container sync feature
|
# Used by container sync feature
|
||||||
@ -744,22 +583,13 @@ class BaseObjectController(Controller):
|
|||||||
|
|
||||||
self._update_x_timestamp(req)
|
self._update_x_timestamp(req)
|
||||||
|
|
||||||
# check if request is a COPY of an existing object
|
def reader():
|
||||||
source_header = req.headers.get('X-Copy-From')
|
try:
|
||||||
if source_header:
|
return req.environ['wsgi.input'].read(
|
||||||
error_response, req, data_source, update_response = \
|
self.app.client_chunk_size)
|
||||||
self._handle_copy_request(req)
|
except (ValueError, IOError) as e:
|
||||||
if error_response:
|
raise ChunkReadError(str(e))
|
||||||
return error_response
|
data_source = iter(reader, '')
|
||||||
else:
|
|
||||||
def reader():
|
|
||||||
try:
|
|
||||||
return req.environ['wsgi.input'].read(
|
|
||||||
self.app.client_chunk_size)
|
|
||||||
except (ValueError, IOError) as e:
|
|
||||||
raise ChunkReadError(str(e))
|
|
||||||
data_source = iter(reader, '')
|
|
||||||
update_response = lambda req, resp: resp
|
|
||||||
|
|
||||||
# check if object is set to be automatically deleted (i.e. expired)
|
# check if object is set to be automatically deleted (i.e. expired)
|
||||||
req, delete_at_container, delete_at_part, \
|
req, delete_at_container, delete_at_part, \
|
||||||
@ -773,7 +603,7 @@ class BaseObjectController(Controller):
|
|||||||
# send object to storage nodes
|
# send object to storage nodes
|
||||||
resp = self._store_object(
|
resp = self._store_object(
|
||||||
req, data_source, nodes, partition, outgoing_headers)
|
req, data_source, nodes, partition, outgoing_headers)
|
||||||
return update_response(req, resp)
|
return resp
|
||||||
|
|
||||||
@public
|
@public
|
||||||
@cors_validation
|
@cors_validation
|
||||||
@ -817,63 +647,6 @@ class BaseObjectController(Controller):
|
|||||||
req, len(nodes), container_partition, containers)
|
req, len(nodes), container_partition, containers)
|
||||||
return self._delete_object(req, obj_ring, partition, headers)
|
return self._delete_object(req, obj_ring, partition, headers)
|
||||||
|
|
||||||
def _reroute(self, policy):
|
|
||||||
"""
|
|
||||||
For COPY requests we need to make sure the controller instance the
|
|
||||||
request is routed through is the correct type for the policy.
|
|
||||||
"""
|
|
||||||
if not policy:
|
|
||||||
raise HTTPServiceUnavailable('Unknown Storage Policy')
|
|
||||||
if policy.policy_type != self.policy_type:
|
|
||||||
controller = self.app.obj_controller_router[policy](
|
|
||||||
self.app, self.account_name, self.container_name,
|
|
||||||
self.object_name)
|
|
||||||
else:
|
|
||||||
controller = self
|
|
||||||
return controller
|
|
||||||
|
|
||||||
@public
|
|
||||||
@cors_validation
|
|
||||||
@delay_denial
|
|
||||||
def COPY(self, req):
|
|
||||||
"""HTTP COPY request handler."""
|
|
||||||
if not req.headers.get('Destination'):
|
|
||||||
return HTTPPreconditionFailed(request=req,
|
|
||||||
body='Destination header required')
|
|
||||||
dest_account = self.account_name
|
|
||||||
if 'Destination-Account' in req.headers:
|
|
||||||
dest_account = req.headers.get('Destination-Account')
|
|
||||||
dest_account = check_account_format(req, dest_account)
|
|
||||||
req.headers['X-Copy-From-Account'] = self.account_name
|
|
||||||
self.account_name = dest_account
|
|
||||||
del req.headers['Destination-Account']
|
|
||||||
dest_container, dest_object = check_destination_header(req)
|
|
||||||
|
|
||||||
source = '/%s/%s' % (self.container_name, self.object_name)
|
|
||||||
self.container_name = dest_container
|
|
||||||
self.object_name = dest_object
|
|
||||||
# re-write the existing request as a PUT instead of creating a new one
|
|
||||||
# since this one is already attached to the posthooklogger
|
|
||||||
# TODO: Swift now has proxy-logging middleware instead of
|
|
||||||
# posthooklogger used in before. i.e. we don't have to
|
|
||||||
# keep the code depends on evnetlet.posthooks sequence, IMHO.
|
|
||||||
# However, creating a new sub request might
|
|
||||||
# cause the possibility to hide some bugs behindes the request
|
|
||||||
# so that we should discuss whichi is suitable (new-sub-request
|
|
||||||
# vs re-write-existing-request) for Swift. [kota_]
|
|
||||||
req.method = 'PUT'
|
|
||||||
req.path_info = '/v1/%s/%s/%s' % \
|
|
||||||
(dest_account, dest_container, dest_object)
|
|
||||||
req.headers['Content-Length'] = 0
|
|
||||||
req.headers['X-Copy-From'] = quote(source)
|
|
||||||
del req.headers['Destination']
|
|
||||||
|
|
||||||
container_info = self.container_info(
|
|
||||||
dest_account, dest_container, req)
|
|
||||||
dest_policy = POLICIES.get_by_index(container_info['storage_policy'])
|
|
||||||
|
|
||||||
return self._reroute(dest_policy).PUT(req)
|
|
||||||
|
|
||||||
|
|
||||||
@ObjectControllerRouter.register(REPL_POLICY)
|
@ObjectControllerRouter.register(REPL_POLICY)
|
||||||
class ReplicatedObjectController(BaseObjectController):
|
class ReplicatedObjectController(BaseObjectController):
|
||||||
|
@ -64,10 +64,14 @@ required_filters = [
|
|||||||
if pipe.startswith('catch_errors')
|
if pipe.startswith('catch_errors')
|
||||||
else [])},
|
else [])},
|
||||||
{'name': 'dlo', 'after_fn': lambda _junk: [
|
{'name': 'dlo', 'after_fn': lambda _junk: [
|
||||||
'staticweb', 'tempauth', 'keystoneauth',
|
'copy', 'staticweb', 'tempauth', 'keystoneauth',
|
||||||
'catch_errors', 'gatekeeper', 'proxy_logging']},
|
'catch_errors', 'gatekeeper', 'proxy_logging']},
|
||||||
{'name': 'versioned_writes', 'after_fn': lambda _junk: [
|
{'name': 'versioned_writes', 'after_fn': lambda _junk: [
|
||||||
'slo', 'dlo', 'staticweb', 'tempauth', 'keystoneauth',
|
'slo', 'dlo', 'copy', 'staticweb', 'tempauth',
|
||||||
|
'keystoneauth', 'catch_errors', 'gatekeeper', 'proxy_logging']},
|
||||||
|
# Put copy before dlo, slo and versioned_writes
|
||||||
|
{'name': 'copy', 'after_fn': lambda _junk: [
|
||||||
|
'staticweb', 'tempauth', 'keystoneauth',
|
||||||
'catch_errors', 'gatekeeper', 'proxy_logging']}]
|
'catch_errors', 'gatekeeper', 'proxy_logging']}]
|
||||||
|
|
||||||
|
|
||||||
@ -107,8 +111,6 @@ class Application(object):
|
|||||||
int(conf.get('recheck_account_existence', 60))
|
int(conf.get('recheck_account_existence', 60))
|
||||||
self.allow_account_management = \
|
self.allow_account_management = \
|
||||||
config_true_value(conf.get('allow_account_management', 'no'))
|
config_true_value(conf.get('allow_account_management', 'no'))
|
||||||
self.object_post_as_copy = \
|
|
||||||
config_true_value(conf.get('object_post_as_copy', 'true'))
|
|
||||||
self.container_ring = container_ring or Ring(swift_dir,
|
self.container_ring = container_ring or Ring(swift_dir,
|
||||||
ring_name='container')
|
ring_name='container')
|
||||||
self.account_ring = account_ring or Ring(swift_dir,
|
self.account_ring = account_ring or Ring(swift_dir,
|
||||||
@ -392,8 +394,7 @@ class Application(object):
|
|||||||
# controller's method indicates it'd like to gather more
|
# controller's method indicates it'd like to gather more
|
||||||
# information and try again later.
|
# information and try again later.
|
||||||
resp = req.environ['swift.authorize'](req)
|
resp = req.environ['swift.authorize'](req)
|
||||||
if not resp and not req.headers.get('X-Copy-From-Account') \
|
if not resp:
|
||||||
and not req.headers.get('Destination-Account'):
|
|
||||||
# No resp means authorized, no delayed recheck required.
|
# No resp means authorized, no delayed recheck required.
|
||||||
old_authorize = req.environ['swift.authorize']
|
old_authorize = req.environ['swift.authorize']
|
||||||
else:
|
else:
|
||||||
@ -404,7 +405,7 @@ class Application(object):
|
|||||||
# Save off original request method (GET, POST, etc.) in case it
|
# Save off original request method (GET, POST, etc.) in case it
|
||||||
# gets mutated during handling. This way logging can display the
|
# gets mutated during handling. This way logging can display the
|
||||||
# method the client actually sent.
|
# method the client actually sent.
|
||||||
req.environ['swift.orig_req_method'] = req.method
|
req.environ.setdefault('swift.orig_req_method', req.method)
|
||||||
try:
|
try:
|
||||||
if old_authorize:
|
if old_authorize:
|
||||||
req.environ.pop('swift.authorize', None)
|
req.environ.pop('swift.authorize', None)
|
||||||
|
@ -1306,12 +1306,10 @@ class TestFile(Base):
|
|||||||
acct,
|
acct,
|
||||||
'%s%s' % (prefix, self.env.container),
|
'%s%s' % (prefix, self.env.container),
|
||||||
Utils.create_name()))
|
Utils.create_name()))
|
||||||
if acct == acct2:
|
# there is no such source container but user has
|
||||||
# there is no such source container
|
# permissions to do a GET (done internally via COPY) for
|
||||||
# and foreign user can have no permission to read it
|
# objects in his own account.
|
||||||
self.assert_status(403)
|
self.assert_status(404)
|
||||||
else:
|
|
||||||
self.assert_status(404)
|
|
||||||
|
|
||||||
self.assertFalse(file_item.copy_account(
|
self.assertFalse(file_item.copy_account(
|
||||||
acct,
|
acct,
|
||||||
@ -1325,12 +1323,10 @@ class TestFile(Base):
|
|||||||
acct,
|
acct,
|
||||||
'%s%s' % (prefix, self.env.container),
|
'%s%s' % (prefix, self.env.container),
|
||||||
Utils.create_name()))
|
Utils.create_name()))
|
||||||
if acct == acct2:
|
# there is no such source container but user has
|
||||||
# there is no such object
|
# permissions to do a GET (done internally via COPY) for
|
||||||
# and foreign user can have no permission to read it
|
# objects in his own account.
|
||||||
self.assert_status(403)
|
self.assert_status(404)
|
||||||
else:
|
|
||||||
self.assert_status(404)
|
|
||||||
|
|
||||||
self.assertFalse(file_item.copy_account(
|
self.assertFalse(file_item.copy_account(
|
||||||
acct,
|
acct,
|
||||||
@ -2677,6 +2673,23 @@ class TestFileComparisonUTF8(Base2, TestFileComparison):
|
|||||||
class TestSloEnv(object):
|
class TestSloEnv(object):
|
||||||
slo_enabled = None # tri-state: None initially, then True/False
|
slo_enabled = None # tri-state: None initially, then True/False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_segments(cls, container):
|
||||||
|
seg_info = {}
|
||||||
|
for letter, size in (('a', 1024 * 1024),
|
||||||
|
('b', 1024 * 1024),
|
||||||
|
('c', 1024 * 1024),
|
||||||
|
('d', 1024 * 1024),
|
||||||
|
('e', 1)):
|
||||||
|
seg_name = "seg_%s" % letter
|
||||||
|
file_item = container.file(seg_name)
|
||||||
|
file_item.write(letter * size)
|
||||||
|
seg_info[seg_name] = {
|
||||||
|
'size_bytes': size,
|
||||||
|
'etag': file_item.md5,
|
||||||
|
'path': '/%s/%s' % (container.name, seg_name)}
|
||||||
|
return seg_info
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUp(cls):
|
def setUp(cls):
|
||||||
cls.conn = Connection(tf.config)
|
cls.conn = Connection(tf.config)
|
||||||
@ -2711,19 +2724,7 @@ class TestSloEnv(object):
|
|||||||
if not cont.create():
|
if not cont.create():
|
||||||
raise ResponseError(cls.conn.response)
|
raise ResponseError(cls.conn.response)
|
||||||
|
|
||||||
cls.seg_info = seg_info = {}
|
cls.seg_info = seg_info = cls.create_segments(cls.container)
|
||||||
for letter, size in (('a', 1024 * 1024),
|
|
||||||
('b', 1024 * 1024),
|
|
||||||
('c', 1024 * 1024),
|
|
||||||
('d', 1024 * 1024),
|
|
||||||
('e', 1)):
|
|
||||||
seg_name = "seg_%s" % letter
|
|
||||||
file_item = cls.container.file(seg_name)
|
|
||||||
file_item.write(letter * size)
|
|
||||||
seg_info[seg_name] = {
|
|
||||||
'size_bytes': size,
|
|
||||||
'etag': file_item.md5,
|
|
||||||
'path': '/%s/%s' % (cls.container.name, seg_name)}
|
|
||||||
|
|
||||||
file_item = cls.container.file("manifest-abcde")
|
file_item = cls.container.file("manifest-abcde")
|
||||||
file_item.write(
|
file_item.write(
|
||||||
@ -3125,8 +3126,9 @@ class TestSlo(Base):
|
|||||||
|
|
||||||
def test_slo_copy_the_manifest(self):
|
def test_slo_copy_the_manifest(self):
|
||||||
file_item = self.env.container.file("manifest-abcde")
|
file_item = self.env.container.file("manifest-abcde")
|
||||||
file_item.copy(self.env.container.name, "copied-abcde-manifest-only",
|
self.assertTrue(file_item.copy(self.env.container.name,
|
||||||
parms={'multipart-manifest': 'get'})
|
"copied-abcde-manifest-only",
|
||||||
|
parms={'multipart-manifest': 'get'}))
|
||||||
|
|
||||||
copied = self.env.container.file("copied-abcde-manifest-only")
|
copied = self.env.container.file("copied-abcde-manifest-only")
|
||||||
copied_contents = copied.read(parms={'multipart-manifest': 'get'})
|
copied_contents = copied.read(parms={'multipart-manifest': 'get'})
|
||||||
@ -3157,10 +3159,40 @@ class TestSlo(Base):
|
|||||||
self.assertTrue(dest_cont.create(hdrs={
|
self.assertTrue(dest_cont.create(hdrs={
|
||||||
'X-Container-Write': self.env.conn.user_acl
|
'X-Container-Write': self.env.conn.user_acl
|
||||||
}))
|
}))
|
||||||
file_item.copy_account(acct,
|
|
||||||
dest_cont,
|
# manifest copy will fail because there is no read access to segments
|
||||||
"copied-abcde-manifest-only",
|
# in destination account
|
||||||
parms={'multipart-manifest': 'get'})
|
file_item.copy_account(
|
||||||
|
acct, dest_cont, "copied-abcde-manifest-only",
|
||||||
|
parms={'multipart-manifest': 'get'})
|
||||||
|
self.assertEqual(400, file_item.conn.response.status)
|
||||||
|
resp_body = file_item.conn.response.read()
|
||||||
|
self.assertEqual(5, resp_body.count('403 Forbidden'),
|
||||||
|
'Unexpected response body %r' % resp_body)
|
||||||
|
|
||||||
|
# create segments container in account2 with read access for account1
|
||||||
|
segs_container = self.env.account2.container(self.env.container.name)
|
||||||
|
self.assertTrue(segs_container.create(hdrs={
|
||||||
|
'X-Container-Read': self.env.conn.user_acl
|
||||||
|
}))
|
||||||
|
|
||||||
|
# manifest copy will still fail because there are no segments in
|
||||||
|
# destination account
|
||||||
|
file_item.copy_account(
|
||||||
|
acct, dest_cont, "copied-abcde-manifest-only",
|
||||||
|
parms={'multipart-manifest': 'get'})
|
||||||
|
self.assertEqual(400, file_item.conn.response.status)
|
||||||
|
resp_body = file_item.conn.response.read()
|
||||||
|
self.assertEqual(5, resp_body.count('404 Not Found'),
|
||||||
|
'Unexpected response body %r' % resp_body)
|
||||||
|
|
||||||
|
# create segments in account2 container with same name as in account1,
|
||||||
|
# manifest copy now succeeds
|
||||||
|
self.env.create_segments(segs_container)
|
||||||
|
|
||||||
|
self.assertTrue(file_item.copy_account(
|
||||||
|
acct, dest_cont, "copied-abcde-manifest-only",
|
||||||
|
parms={'multipart-manifest': 'get'}))
|
||||||
|
|
||||||
copied = dest_cont.file("copied-abcde-manifest-only")
|
copied = dest_cont.file("copied-abcde-manifest-only")
|
||||||
copied_contents = copied.read(parms={'multipart-manifest': 'get'})
|
copied_contents = copied.read(parms={'multipart-manifest': 'get'})
|
||||||
|
@ -20,6 +20,7 @@ from copy import deepcopy
|
|||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
from swift.common import swob
|
from swift.common import swob
|
||||||
from swift.common.header_key_dict import HeaderKeyDict
|
from swift.common.header_key_dict import HeaderKeyDict
|
||||||
|
from swift.common.swob import HTTPNotImplemented
|
||||||
from swift.common.utils import split_path
|
from swift.common.utils import split_path
|
||||||
|
|
||||||
from test.unit import FakeLogger, FakeRing
|
from test.unit import FakeLogger, FakeRing
|
||||||
@ -43,6 +44,8 @@ class FakeSwift(object):
|
|||||||
"""
|
"""
|
||||||
A good-enough fake Swift proxy server to use in testing middleware.
|
A good-enough fake Swift proxy server to use in testing middleware.
|
||||||
"""
|
"""
|
||||||
|
ALLOWED_METHODS = [
|
||||||
|
'PUT', 'POST', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'REPLICATE']
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._calls = []
|
self._calls = []
|
||||||
@ -71,6 +74,9 @@ class FakeSwift(object):
|
|||||||
|
|
||||||
def __call__(self, env, start_response):
|
def __call__(self, env, start_response):
|
||||||
method = env['REQUEST_METHOD']
|
method = env['REQUEST_METHOD']
|
||||||
|
if method not in self.ALLOWED_METHODS:
|
||||||
|
raise HTTPNotImplemented()
|
||||||
|
|
||||||
path = env['PATH_INFO']
|
path = env['PATH_INFO']
|
||||||
_, acc, cont, obj = split_path(env['PATH_INFO'], 0, 4,
|
_, acc, cont, obj = split_path(env['PATH_INFO'], 0, 4,
|
||||||
rest_with_last=True)
|
rest_with_last=True)
|
||||||
|
@ -13,9 +13,10 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from swift.common.swob import Request, wsgify, HTTPForbidden
|
from swift.common.swob import Request, wsgify, HTTPForbidden, \
|
||||||
|
HTTPException
|
||||||
|
|
||||||
from swift.common.middleware import account_quotas
|
from swift.common.middleware import account_quotas, copy
|
||||||
|
|
||||||
from swift.proxy.controllers.base import _get_cache_key, \
|
from swift.proxy.controllers.base import _get_cache_key, \
|
||||||
headers_to_account_info, get_object_env_key, \
|
headers_to_account_info, get_object_env_key, \
|
||||||
@ -245,84 +246,6 @@ class TestAccountQuota(unittest.TestCase):
|
|||||||
res = req.get_response(app)
|
res = req.get_response(app)
|
||||||
self.assertEqual(res.status_int, 200)
|
self.assertEqual(res.status_int, 200)
|
||||||
|
|
||||||
def test_exceed_bytes_quota_copy_from(self):
|
|
||||||
headers = [('x-account-bytes-used', '500'),
|
|
||||||
('x-account-meta-quota-bytes', '1000'),
|
|
||||||
('content-length', '1000')]
|
|
||||||
app = account_quotas.AccountQuotaMiddleware(FakeApp(headers))
|
|
||||||
cache = FakeCache(None)
|
|
||||||
req = Request.blank('/v1/a/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'PUT',
|
|
||||||
'swift.cache': cache},
|
|
||||||
headers={'x-copy-from': '/c2/o2'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 413)
|
|
||||||
self.assertEqual(res.body, 'Upload exceeds quota.')
|
|
||||||
|
|
||||||
def test_exceed_bytes_quota_copy_verb(self):
|
|
||||||
headers = [('x-account-bytes-used', '500'),
|
|
||||||
('x-account-meta-quota-bytes', '1000'),
|
|
||||||
('content-length', '1000')]
|
|
||||||
app = account_quotas.AccountQuotaMiddleware(FakeApp(headers))
|
|
||||||
cache = FakeCache(None)
|
|
||||||
req = Request.blank('/v1/a/c2/o2',
|
|
||||||
environ={'REQUEST_METHOD': 'COPY',
|
|
||||||
'swift.cache': cache},
|
|
||||||
headers={'Destination': '/c/o'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 413)
|
|
||||||
self.assertEqual(res.body, 'Upload exceeds quota.')
|
|
||||||
|
|
||||||
def test_not_exceed_bytes_quota_copy_from(self):
|
|
||||||
headers = [('x-account-bytes-used', '0'),
|
|
||||||
('x-account-meta-quota-bytes', '1000'),
|
|
||||||
('content-length', '1000')]
|
|
||||||
app = account_quotas.AccountQuotaMiddleware(FakeApp(headers))
|
|
||||||
cache = FakeCache(None)
|
|
||||||
req = Request.blank('/v1/a/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'PUT',
|
|
||||||
'swift.cache': cache},
|
|
||||||
headers={'x-copy-from': '/c2/o2'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 200)
|
|
||||||
|
|
||||||
def test_not_exceed_bytes_quota_copy_verb(self):
|
|
||||||
headers = [('x-account-bytes-used', '0'),
|
|
||||||
('x-account-meta-quota-bytes', '1000'),
|
|
||||||
('content-length', '1000')]
|
|
||||||
app = account_quotas.AccountQuotaMiddleware(FakeApp(headers))
|
|
||||||
cache = FakeCache(None)
|
|
||||||
req = Request.blank('/v1/a/c2/o2',
|
|
||||||
environ={'REQUEST_METHOD': 'COPY',
|
|
||||||
'swift.cache': cache},
|
|
||||||
headers={'Destination': '/c/o'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 200)
|
|
||||||
|
|
||||||
def test_quota_copy_from_no_src(self):
|
|
||||||
headers = [('x-account-bytes-used', '0'),
|
|
||||||
('x-account-meta-quota-bytes', '1000')]
|
|
||||||
app = account_quotas.AccountQuotaMiddleware(FakeApp(headers))
|
|
||||||
cache = FakeCache(None)
|
|
||||||
req = Request.blank('/v1/a/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'PUT',
|
|
||||||
'swift.cache': cache},
|
|
||||||
headers={'x-copy-from': '/c2/o3'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 200)
|
|
||||||
|
|
||||||
def test_quota_copy_from_bad_src(self):
|
|
||||||
headers = [('x-account-bytes-used', '0'),
|
|
||||||
('x-account-meta-quota-bytes', '1000')]
|
|
||||||
app = account_quotas.AccountQuotaMiddleware(FakeApp(headers))
|
|
||||||
cache = FakeCache(None)
|
|
||||||
req = Request.blank('/v1/a/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'PUT',
|
|
||||||
'swift.cache': cache},
|
|
||||||
headers={'x-copy-from': 'bad_path'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 412)
|
|
||||||
|
|
||||||
def test_exceed_bytes_quota_reseller(self):
|
def test_exceed_bytes_quota_reseller(self):
|
||||||
headers = [('x-account-bytes-used', '1000'),
|
headers = [('x-account-bytes-used', '1000'),
|
||||||
('x-account-meta-quota-bytes', '0')]
|
('x-account-meta-quota-bytes', '0')]
|
||||||
@ -485,5 +408,91 @@ class TestAccountQuota(unittest.TestCase):
|
|||||||
self.assertEqual(res.status_int, 200)
|
self.assertEqual(res.status_int, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountQuotaCopyingTestCases(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.app = FakeApp()
|
||||||
|
self.aq_filter = account_quotas.filter_factory({})(self.app)
|
||||||
|
self.copy_filter = copy.filter_factory({})(self.aq_filter)
|
||||||
|
|
||||||
|
def test_exceed_bytes_quota_copy_from(self):
|
||||||
|
headers = [('x-account-bytes-used', '500'),
|
||||||
|
('x-account-meta-quota-bytes', '1000'),
|
||||||
|
('content-length', '1000')]
|
||||||
|
self.app.headers = headers
|
||||||
|
cache = FakeCache(None)
|
||||||
|
req = Request.blank('/v1/a/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT',
|
||||||
|
'swift.cache': cache},
|
||||||
|
headers={'x-copy-from': '/c2/o2'})
|
||||||
|
res = req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(res.status_int, 413)
|
||||||
|
self.assertEqual(res.body, 'Upload exceeds quota.')
|
||||||
|
|
||||||
|
def test_exceed_bytes_quota_copy_verb(self):
|
||||||
|
headers = [('x-account-bytes-used', '500'),
|
||||||
|
('x-account-meta-quota-bytes', '1000'),
|
||||||
|
('content-length', '1000')]
|
||||||
|
self.app.headers = headers
|
||||||
|
cache = FakeCache(None)
|
||||||
|
req = Request.blank('/v1/a/c2/o2',
|
||||||
|
environ={'REQUEST_METHOD': 'COPY',
|
||||||
|
'swift.cache': cache},
|
||||||
|
headers={'Destination': '/c/o'})
|
||||||
|
res = req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(res.status_int, 413)
|
||||||
|
self.assertEqual(res.body, 'Upload exceeds quota.')
|
||||||
|
|
||||||
|
def test_not_exceed_bytes_quota_copy_from(self):
|
||||||
|
headers = [('x-account-bytes-used', '0'),
|
||||||
|
('x-account-meta-quota-bytes', '1000'),
|
||||||
|
('content-length', '1000')]
|
||||||
|
self.app.headers = headers
|
||||||
|
cache = FakeCache(None)
|
||||||
|
req = Request.blank('/v1/a/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT',
|
||||||
|
'swift.cache': cache},
|
||||||
|
headers={'x-copy-from': '/c2/o2'})
|
||||||
|
res = req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(res.status_int, 200)
|
||||||
|
|
||||||
|
def test_not_exceed_bytes_quota_copy_verb(self):
|
||||||
|
headers = [('x-account-bytes-used', '0'),
|
||||||
|
('x-account-meta-quota-bytes', '1000'),
|
||||||
|
('content-length', '1000')]
|
||||||
|
self.app.headers = headers
|
||||||
|
cache = FakeCache(None)
|
||||||
|
req = Request.blank('/v1/a/c2/o2',
|
||||||
|
environ={'REQUEST_METHOD': 'COPY',
|
||||||
|
'swift.cache': cache},
|
||||||
|
headers={'Destination': '/c/o'})
|
||||||
|
res = req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(res.status_int, 200)
|
||||||
|
|
||||||
|
def test_quota_copy_from_no_src(self):
|
||||||
|
headers = [('x-account-bytes-used', '0'),
|
||||||
|
('x-account-meta-quota-bytes', '1000')]
|
||||||
|
self.app.headers = headers
|
||||||
|
cache = FakeCache(None)
|
||||||
|
req = Request.blank('/v1/a/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT',
|
||||||
|
'swift.cache': cache},
|
||||||
|
headers={'x-copy-from': '/c2/o3'})
|
||||||
|
res = req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(res.status_int, 200)
|
||||||
|
|
||||||
|
def test_quota_copy_from_bad_src(self):
|
||||||
|
headers = [('x-account-bytes-used', '0'),
|
||||||
|
('x-account-meta-quota-bytes', '1000')]
|
||||||
|
self.app.headers = headers
|
||||||
|
cache = FakeCache(None)
|
||||||
|
req = Request.blank('/v1/a/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT',
|
||||||
|
'swift.cache': cache},
|
||||||
|
headers={'x-copy-from': 'bad_path'})
|
||||||
|
with self.assertRaises(HTTPException) as catcher:
|
||||||
|
req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(412, catcher.exception.status_int)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
1183
test/unit/common/middleware/test_copy.py
Normal file
1183
test/unit/common/middleware/test_copy.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -803,107 +803,6 @@ class TestDloGetManifest(DloTestCase):
|
|||||||
self.assertTrue(auth_got_called[0] > 1)
|
self.assertTrue(auth_got_called[0] > 1)
|
||||||
|
|
||||||
|
|
||||||
def fake_start_response(*args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TestDloCopyHook(DloTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestDloCopyHook, self).setUp()
|
|
||||||
|
|
||||||
self.app.register(
|
|
||||||
'GET', '/v1/AUTH_test/c/o1', swob.HTTPOk,
|
|
||||||
{'Content-Length': '10', 'Etag': 'o1-etag'},
|
|
||||||
"aaaaaaaaaa")
|
|
||||||
self.app.register(
|
|
||||||
'GET', '/v1/AUTH_test/c/o2', swob.HTTPOk,
|
|
||||||
{'Content-Length': '10', 'Etag': 'o2-etag'},
|
|
||||||
"bbbbbbbbbb")
|
|
||||||
self.app.register(
|
|
||||||
'GET', '/v1/AUTH_test/c/man',
|
|
||||||
swob.HTTPOk, {'X-Object-Manifest': 'c/o'},
|
|
||||||
"manifest-contents")
|
|
||||||
|
|
||||||
lm = '2013-11-22T02:42:13.781760'
|
|
||||||
ct = 'application/octet-stream'
|
|
||||||
segs = [{"hash": "o1-etag", "bytes": 10, "name": "o1",
|
|
||||||
"last_modified": lm, "content_type": ct},
|
|
||||||
{"hash": "o2-etag", "bytes": 5, "name": "o2",
|
|
||||||
"last_modified": lm, "content_type": ct}]
|
|
||||||
|
|
||||||
self.app.register(
|
|
||||||
'GET', '/v1/AUTH_test/c?format=json&prefix=o',
|
|
||||||
swob.HTTPOk, {'Content-Type': 'application/json; charset=utf-8'},
|
|
||||||
json.dumps(segs))
|
|
||||||
|
|
||||||
copy_hook = [None]
|
|
||||||
|
|
||||||
# slip this guy in there to pull out the hook
|
|
||||||
def extract_copy_hook(env, sr):
|
|
||||||
copy_hook[0] = env.get('swift.copy_hook')
|
|
||||||
return self.app(env, sr)
|
|
||||||
|
|
||||||
self.dlo = dlo.filter_factory({})(extract_copy_hook)
|
|
||||||
|
|
||||||
req = swob.Request.blank('/v1/AUTH_test/c/o1',
|
|
||||||
environ={'REQUEST_METHOD': 'GET'})
|
|
||||||
self.dlo(req.environ, fake_start_response)
|
|
||||||
self.copy_hook = copy_hook[0]
|
|
||||||
|
|
||||||
self.assertTrue(self.copy_hook is not None) # sanity check
|
|
||||||
|
|
||||||
def test_copy_hook_passthrough(self):
|
|
||||||
source_req = swob.Request.blank(
|
|
||||||
'/v1/AUTH_test/c/man',
|
|
||||||
environ={'REQUEST_METHOD': 'GET'})
|
|
||||||
sink_req = swob.Request.blank(
|
|
||||||
'/v1/AUTH_test/c/man',
|
|
||||||
environ={'REQUEST_METHOD': 'PUT'})
|
|
||||||
source_resp = swob.Response(request=source_req, status=200)
|
|
||||||
|
|
||||||
# no X-Object-Manifest header, so do nothing
|
|
||||||
modified_resp = self.copy_hook(source_req, source_resp, sink_req)
|
|
||||||
self.assertTrue(modified_resp is source_resp)
|
|
||||||
|
|
||||||
def test_copy_hook_manifest(self):
|
|
||||||
source_req = swob.Request.blank(
|
|
||||||
'/v1/AUTH_test/c/man',
|
|
||||||
environ={'REQUEST_METHOD': 'GET'})
|
|
||||||
sink_req = swob.Request.blank(
|
|
||||||
'/v1/AUTH_test/c/man',
|
|
||||||
environ={'REQUEST_METHOD': 'PUT'})
|
|
||||||
source_resp = swob.Response(
|
|
||||||
request=source_req, status=200,
|
|
||||||
headers={"X-Object-Manifest": "c/o"},
|
|
||||||
app_iter=["manifest"])
|
|
||||||
|
|
||||||
# it's a manifest, so copy the segments to make a normal object
|
|
||||||
modified_resp = self.copy_hook(source_req, source_resp, sink_req)
|
|
||||||
self.assertTrue(modified_resp is not source_resp)
|
|
||||||
self.assertEqual(modified_resp.etag,
|
|
||||||
hashlib.md5("o1-etago2-etag").hexdigest())
|
|
||||||
self.assertEqual(sink_req.headers.get('X-Object-Manifest'), None)
|
|
||||||
|
|
||||||
def test_copy_hook_manifest_with_multipart_manifest_get(self):
|
|
||||||
source_req = swob.Request.blank(
|
|
||||||
'/v1/AUTH_test/c/man',
|
|
||||||
environ={'REQUEST_METHOD': 'GET',
|
|
||||||
'QUERY_STRING': 'multipart-manifest=get'})
|
|
||||||
sink_req = swob.Request.blank(
|
|
||||||
'/v1/AUTH_test/c/man',
|
|
||||||
environ={'REQUEST_METHOD': 'PUT'})
|
|
||||||
source_resp = swob.Response(
|
|
||||||
request=source_req, status=200,
|
|
||||||
headers={"X-Object-Manifest": "c/o"},
|
|
||||||
app_iter=["manifest"])
|
|
||||||
|
|
||||||
# make sure the sink request (the backend PUT) gets X-Object-Manifest
|
|
||||||
# on it, but that's all
|
|
||||||
modified_resp = self.copy_hook(source_req, source_resp, sink_req)
|
|
||||||
self.assertTrue(modified_resp is source_resp)
|
|
||||||
self.assertEqual(sink_req.headers.get('X-Object-Manifest'), 'c/o')
|
|
||||||
|
|
||||||
|
|
||||||
class TestDloConfiguration(unittest.TestCase):
|
class TestDloConfiguration(unittest.TestCase):
|
||||||
"""
|
"""
|
||||||
For backwards compatibility, we will read a couple of values out of the
|
For backwards compatibility, we will read a couple of values out of the
|
||||||
|
@ -15,8 +15,9 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from swift.common.swob import Request, HTTPUnauthorized
|
from swift.common.swob import Request, HTTPUnauthorized, HTTPOk, HTTPException
|
||||||
from swift.common.middleware import container_quotas
|
from swift.common.middleware import container_quotas, copy
|
||||||
|
from test.unit.common.middleware.helpers import FakeSwift
|
||||||
|
|
||||||
|
|
||||||
class FakeCache(object):
|
class FakeCache(object):
|
||||||
@ -95,32 +96,6 @@ class TestContainerQuotas(unittest.TestCase):
|
|||||||
self.assertEqual(res.status_int, 413)
|
self.assertEqual(res.status_int, 413)
|
||||||
self.assertEqual(res.body, 'Upload exceeds quota.')
|
self.assertEqual(res.body, 'Upload exceeds quota.')
|
||||||
|
|
||||||
def test_exceed_bytes_quota_copy_from(self):
|
|
||||||
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
|
||||||
cache = FakeCache({'bytes': 0, 'meta': {'quota-bytes': '2'}})
|
|
||||||
|
|
||||||
req = Request.blank('/v1/a/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'PUT',
|
|
||||||
'swift.object/a/c2/o2': {'length': 10},
|
|
||||||
'swift.cache': cache},
|
|
||||||
headers={'x-copy-from': '/c2/o2'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 413)
|
|
||||||
self.assertEqual(res.body, 'Upload exceeds quota.')
|
|
||||||
|
|
||||||
def test_exceed_bytes_quota_copy_verb(self):
|
|
||||||
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
|
||||||
cache = FakeCache({'bytes': 0, 'meta': {'quota-bytes': '2'}})
|
|
||||||
|
|
||||||
req = Request.blank('/v1/a/c2/o2',
|
|
||||||
environ={'REQUEST_METHOD': 'COPY',
|
|
||||||
'swift.object/a/c2/o2': {'length': 10},
|
|
||||||
'swift.cache': cache},
|
|
||||||
headers={'Destination': '/c/o'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 413)
|
|
||||||
self.assertEqual(res.body, 'Upload exceeds quota.')
|
|
||||||
|
|
||||||
def test_not_exceed_bytes_quota(self):
|
def test_not_exceed_bytes_quota(self):
|
||||||
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
||||||
cache = FakeCache({'bytes': 0, 'meta': {'quota-bytes': '100'}})
|
cache = FakeCache({'bytes': 0, 'meta': {'quota-bytes': '100'}})
|
||||||
@ -131,60 +106,6 @@ class TestContainerQuotas(unittest.TestCase):
|
|||||||
res = req.get_response(app)
|
res = req.get_response(app)
|
||||||
self.assertEqual(res.status_int, 200)
|
self.assertEqual(res.status_int, 200)
|
||||||
|
|
||||||
def test_not_exceed_bytes_quota_copy_from(self):
|
|
||||||
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
|
||||||
cache = FakeCache({'bytes': 0, 'meta': {'quota-bytes': '100'}})
|
|
||||||
req = Request.blank('/v1/a/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'PUT',
|
|
||||||
'swift.object/a/c2/o2': {'length': 10},
|
|
||||||
'swift.cache': cache},
|
|
||||||
headers={'x-copy-from': '/c2/o2'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 200)
|
|
||||||
|
|
||||||
def test_not_exceed_bytes_quota_copy_verb(self):
|
|
||||||
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
|
||||||
cache = FakeCache({'bytes': 0, 'meta': {'quota-bytes': '100'}})
|
|
||||||
req = Request.blank('/v1/a/c2/o2',
|
|
||||||
environ={'REQUEST_METHOD': 'COPY',
|
|
||||||
'swift.object/a/c2/o2': {'length': 10},
|
|
||||||
'swift.cache': cache},
|
|
||||||
headers={'Destination': '/c/o'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 200)
|
|
||||||
|
|
||||||
def test_bytes_quota_copy_from_no_src(self):
|
|
||||||
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
|
||||||
cache = FakeCache({'bytes': 0, 'meta': {'quota-bytes': '100'}})
|
|
||||||
req = Request.blank('/v1/a/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'PUT',
|
|
||||||
'swift.object/a/c2/o2': {'length': 10},
|
|
||||||
'swift.cache': cache},
|
|
||||||
headers={'x-copy-from': '/c2/o3'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 200)
|
|
||||||
|
|
||||||
def test_bytes_quota_copy_from_bad_src(self):
|
|
||||||
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
|
||||||
cache = FakeCache({'bytes': 0, 'meta': {'quota-bytes': '100'}})
|
|
||||||
req = Request.blank('/v1/a/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'PUT',
|
|
||||||
'swift.cache': cache},
|
|
||||||
headers={'x-copy-from': 'bad_path'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 412)
|
|
||||||
|
|
||||||
def test_bytes_quota_copy_verb_no_src(self):
|
|
||||||
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
|
||||||
cache = FakeCache({'bytes': 0, 'meta': {'quota-bytes': '100'}})
|
|
||||||
req = Request.blank('/v1/a/c2/o3',
|
|
||||||
environ={'REQUEST_METHOD': 'COPY',
|
|
||||||
'swift.object/a/c2/o2': {'length': 10},
|
|
||||||
'swift.cache': cache},
|
|
||||||
headers={'Destination': '/c/o'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 200)
|
|
||||||
|
|
||||||
def test_exceed_counts_quota(self):
|
def test_exceed_counts_quota(self):
|
||||||
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
||||||
cache = FakeCache({'object_count': 1, 'meta': {'quota-count': '1'}})
|
cache = FakeCache({'object_count': 1, 'meta': {'quota-count': '1'}})
|
||||||
@ -196,61 +117,6 @@ class TestContainerQuotas(unittest.TestCase):
|
|||||||
self.assertEqual(res.status_int, 413)
|
self.assertEqual(res.status_int, 413)
|
||||||
self.assertEqual(res.body, 'Upload exceeds quota.')
|
self.assertEqual(res.body, 'Upload exceeds quota.')
|
||||||
|
|
||||||
def test_exceed_counts_quota_copy_from(self):
|
|
||||||
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
|
||||||
cache = FakeCache({'object_count': 1, 'meta': {'quota-count': '1'}})
|
|
||||||
req = Request.blank('/v1/a/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'PUT',
|
|
||||||
'swift.object/a/c2/o2': {'length': 10},
|
|
||||||
'swift.cache': cache},
|
|
||||||
headers={'x-copy-from': '/c2/o2'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 413)
|
|
||||||
self.assertEqual(res.body, 'Upload exceeds quota.')
|
|
||||||
|
|
||||||
def test_exceed_counts_quota_copy_verb(self):
|
|
||||||
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
|
||||||
cache = FakeCache({'object_count': 1, 'meta': {'quota-count': '1'}})
|
|
||||||
req = Request.blank('/v1/a/c2/o2',
|
|
||||||
environ={'REQUEST_METHOD': 'COPY',
|
|
||||||
'swift.cache': cache},
|
|
||||||
headers={'Destination': '/c/o'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 413)
|
|
||||||
self.assertEqual(res.body, 'Upload exceeds quota.')
|
|
||||||
|
|
||||||
def test_exceed_counts_quota_copy_cross_account_verb(self):
|
|
||||||
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
|
||||||
a_c_cache = {'storage_policy': '0', 'meta': {'quota-count': '2'},
|
|
||||||
'status': 200, 'object_count': 1}
|
|
||||||
a2_c_cache = {'storage_policy': '0', 'meta': {'quota-count': '1'},
|
|
||||||
'status': 200, 'object_count': 1}
|
|
||||||
req = Request.blank('/v1/a/c2/o2',
|
|
||||||
environ={'REQUEST_METHOD': 'COPY',
|
|
||||||
'swift.container/a/c': a_c_cache,
|
|
||||||
'swift.container/a2/c': a2_c_cache},
|
|
||||||
headers={'Destination': '/c/o',
|
|
||||||
'Destination-Account': 'a2'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 413)
|
|
||||||
self.assertEqual(res.body, 'Upload exceeds quota.')
|
|
||||||
|
|
||||||
def test_exceed_counts_quota_copy_cross_account_PUT_verb(self):
|
|
||||||
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
|
||||||
a_c_cache = {'storage_policy': '0', 'meta': {'quota-count': '2'},
|
|
||||||
'status': 200, 'object_count': 1}
|
|
||||||
a2_c_cache = {'storage_policy': '0', 'meta': {'quota-count': '1'},
|
|
||||||
'status': 200, 'object_count': 1}
|
|
||||||
req = Request.blank('/v1/a2/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'PUT',
|
|
||||||
'swift.container/a/c': a_c_cache,
|
|
||||||
'swift.container/a2/c': a2_c_cache},
|
|
||||||
headers={'X-Copy-From': '/c2/o2',
|
|
||||||
'X-Copy-From-Account': 'a'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 413)
|
|
||||||
self.assertEqual(res.body, 'Upload exceeds quota.')
|
|
||||||
|
|
||||||
def test_not_exceed_counts_quota(self):
|
def test_not_exceed_counts_quota(self):
|
||||||
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
||||||
cache = FakeCache({'object_count': 1, 'meta': {'quota-count': '2'}})
|
cache = FakeCache({'object_count': 1, 'meta': {'quota-count': '2'}})
|
||||||
@ -261,26 +127,6 @@ class TestContainerQuotas(unittest.TestCase):
|
|||||||
res = req.get_response(app)
|
res = req.get_response(app)
|
||||||
self.assertEqual(res.status_int, 200)
|
self.assertEqual(res.status_int, 200)
|
||||||
|
|
||||||
def test_not_exceed_counts_quota_copy_from(self):
|
|
||||||
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
|
||||||
cache = FakeCache({'object_count': 1, 'meta': {'quota-count': '2'}})
|
|
||||||
req = Request.blank('/v1/a/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'PUT',
|
|
||||||
'swift.cache': cache},
|
|
||||||
headers={'x-copy-from': '/c2/o2'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 200)
|
|
||||||
|
|
||||||
def test_not_exceed_counts_quota_copy_verb(self):
|
|
||||||
app = container_quotas.ContainerQuotaMiddleware(FakeApp(), {})
|
|
||||||
cache = FakeCache({'object_count': 1, 'meta': {'quota-count': '2'}})
|
|
||||||
req = Request.blank('/v1/a/c2/o2',
|
|
||||||
environ={'REQUEST_METHOD': 'COPY',
|
|
||||||
'swift.cache': cache},
|
|
||||||
headers={'Destination': '/c/o'})
|
|
||||||
res = req.get_response(app)
|
|
||||||
self.assertEqual(res.status_int, 200)
|
|
||||||
|
|
||||||
def test_invalid_quotas(self):
|
def test_invalid_quotas(self):
|
||||||
req = Request.blank(
|
req = Request.blank(
|
||||||
'/v1/a/c',
|
'/v1/a/c',
|
||||||
@ -346,5 +192,168 @@ class TestContainerQuotas(unittest.TestCase):
|
|||||||
res = req.get_response(app)
|
res = req.get_response(app)
|
||||||
self.assertEqual(res.status_int, 401)
|
self.assertEqual(res.status_int, 401)
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerQuotaCopyingTestCases(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.app = FakeSwift()
|
||||||
|
self.cq_filter = container_quotas.filter_factory({})(self.app)
|
||||||
|
self.copy_filter = copy.filter_factory({})(self.cq_filter)
|
||||||
|
|
||||||
|
def test_exceed_bytes_quota_copy_verb(self):
|
||||||
|
cache = FakeCache({'bytes': 0, 'meta': {'quota-bytes': '2'}})
|
||||||
|
self.app.register('GET', '/v1/a/c2/o2', HTTPOk,
|
||||||
|
{'Content-Length': '10'}, 'passed')
|
||||||
|
|
||||||
|
req = Request.blank('/v1/a/c2/o2',
|
||||||
|
environ={'REQUEST_METHOD': 'COPY',
|
||||||
|
'swift.cache': cache},
|
||||||
|
headers={'Destination': '/c/o'})
|
||||||
|
res = req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(res.status_int, 413)
|
||||||
|
self.assertEqual(res.body, 'Upload exceeds quota.')
|
||||||
|
|
||||||
|
def test_not_exceed_bytes_quota_copy_verb(self):
|
||||||
|
self.app.register('GET', '/v1/a/c2/o2', HTTPOk,
|
||||||
|
{'Content-Length': '10'}, 'passed')
|
||||||
|
self.app.register(
|
||||||
|
'PUT', '/v1/a/c/o', HTTPOk, {}, 'passed')
|
||||||
|
cache = FakeCache({'bytes': 0, 'meta': {'quota-bytes': '100'}})
|
||||||
|
req = Request.blank('/v1/a/c2/o2',
|
||||||
|
environ={'REQUEST_METHOD': 'COPY',
|
||||||
|
'swift.cache': cache},
|
||||||
|
headers={'Destination': '/c/o'})
|
||||||
|
res = req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(res.status_int, 200)
|
||||||
|
|
||||||
|
def test_exceed_counts_quota_copy_verb(self):
|
||||||
|
self.app.register('GET', '/v1/a/c2/o2', HTTPOk, {}, 'passed')
|
||||||
|
cache = FakeCache({'object_count': 1, 'meta': {'quota-count': '1'}})
|
||||||
|
req = Request.blank('/v1/a/c2/o2',
|
||||||
|
environ={'REQUEST_METHOD': 'COPY',
|
||||||
|
'swift.cache': cache},
|
||||||
|
headers={'Destination': '/c/o'})
|
||||||
|
res = req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(res.status_int, 413)
|
||||||
|
self.assertEqual(res.body, 'Upload exceeds quota.')
|
||||||
|
|
||||||
|
def test_exceed_counts_quota_copy_cross_account_verb(self):
|
||||||
|
self.app.register('GET', '/v1/a/c2/o2', HTTPOk, {}, 'passed')
|
||||||
|
a_c_cache = {'storage_policy': '0', 'meta': {'quota-count': '2'},
|
||||||
|
'status': 200, 'object_count': 1}
|
||||||
|
a2_c_cache = {'storage_policy': '0', 'meta': {'quota-count': '1'},
|
||||||
|
'status': 200, 'object_count': 1}
|
||||||
|
req = Request.blank('/v1/a/c2/o2',
|
||||||
|
environ={'REQUEST_METHOD': 'COPY',
|
||||||
|
'swift.container/a/c': a_c_cache,
|
||||||
|
'swift.container/a2/c': a2_c_cache},
|
||||||
|
headers={'Destination': '/c/o',
|
||||||
|
'Destination-Account': 'a2'})
|
||||||
|
res = req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(res.status_int, 413)
|
||||||
|
self.assertEqual(res.body, 'Upload exceeds quota.')
|
||||||
|
|
||||||
|
def test_exceed_counts_quota_copy_cross_account_PUT_verb(self):
|
||||||
|
self.app.register('GET', '/v1/a/c2/o2', HTTPOk, {}, 'passed')
|
||||||
|
a_c_cache = {'storage_policy': '0', 'meta': {'quota-count': '2'},
|
||||||
|
'status': 200, 'object_count': 1}
|
||||||
|
a2_c_cache = {'storage_policy': '0', 'meta': {'quota-count': '1'},
|
||||||
|
'status': 200, 'object_count': 1}
|
||||||
|
req = Request.blank('/v1/a2/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT',
|
||||||
|
'swift.container/a/c': a_c_cache,
|
||||||
|
'swift.container/a2/c': a2_c_cache},
|
||||||
|
headers={'X-Copy-From': '/c2/o2',
|
||||||
|
'X-Copy-From-Account': 'a'})
|
||||||
|
res = req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(res.status_int, 413)
|
||||||
|
self.assertEqual(res.body, 'Upload exceeds quota.')
|
||||||
|
|
||||||
|
def test_exceed_bytes_quota_copy_from(self):
|
||||||
|
self.app.register('GET', '/v1/a/c2/o2', HTTPOk,
|
||||||
|
{'Content-Length': '10'}, 'passed')
|
||||||
|
cache = FakeCache({'bytes': 0, 'meta': {'quota-bytes': '2'}})
|
||||||
|
|
||||||
|
req = Request.blank('/v1/a/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT',
|
||||||
|
'swift.cache': cache},
|
||||||
|
headers={'x-copy-from': '/c2/o2'})
|
||||||
|
res = req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(res.status_int, 413)
|
||||||
|
self.assertEqual(res.body, 'Upload exceeds quota.')
|
||||||
|
|
||||||
|
def test_not_exceed_bytes_quota_copy_from(self):
|
||||||
|
self.app.register('GET', '/v1/a/c2/o2', HTTPOk,
|
||||||
|
{'Content-Length': '10'}, 'passed')
|
||||||
|
self.app.register(
|
||||||
|
'PUT', '/v1/a/c/o', HTTPOk, {}, 'passed')
|
||||||
|
cache = FakeCache({'bytes': 0, 'meta': {'quota-bytes': '100'}})
|
||||||
|
req = Request.blank('/v1/a/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT',
|
||||||
|
'swift.cache': cache},
|
||||||
|
headers={'x-copy-from': '/c2/o2'})
|
||||||
|
res = req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(res.status_int, 200)
|
||||||
|
|
||||||
|
def test_bytes_quota_copy_from_no_src(self):
|
||||||
|
self.app.register('GET', '/v1/a/c2/o3', HTTPOk, {}, 'passed')
|
||||||
|
self.app.register(
|
||||||
|
'PUT', '/v1/a/c/o', HTTPOk, {}, 'passed')
|
||||||
|
cache = FakeCache({'bytes': 0, 'meta': {'quota-bytes': '100'}})
|
||||||
|
req = Request.blank('/v1/a/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT',
|
||||||
|
'swift.cache': cache},
|
||||||
|
headers={'x-copy-from': '/c2/o3'})
|
||||||
|
res = req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(res.status_int, 200)
|
||||||
|
|
||||||
|
def test_bytes_quota_copy_from_bad_src(self):
|
||||||
|
cache = FakeCache({'bytes': 0, 'meta': {'quota-bytes': '100'}})
|
||||||
|
req = Request.blank('/v1/a/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT',
|
||||||
|
'swift.cache': cache},
|
||||||
|
headers={'x-copy-from': 'bad_path'})
|
||||||
|
with self.assertRaises(HTTPException) as catcher:
|
||||||
|
req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(412, catcher.exception.status_int)
|
||||||
|
|
||||||
|
def test_exceed_counts_quota_copy_from(self):
|
||||||
|
self.app.register('GET', '/v1/a/c2/o2', HTTPOk,
|
||||||
|
{'Content-Length': '10'}, 'passed')
|
||||||
|
cache = FakeCache({'object_count': 1, 'meta': {'quota-count': '1'}})
|
||||||
|
req = Request.blank('/v1/a/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT',
|
||||||
|
'swift.cache': cache},
|
||||||
|
headers={'x-copy-from': '/c2/o2'})
|
||||||
|
res = req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(res.status_int, 413)
|
||||||
|
self.assertEqual(res.body, 'Upload exceeds quota.')
|
||||||
|
|
||||||
|
def test_not_exceed_counts_quota_copy_from(self):
|
||||||
|
self.app.register('GET', '/v1/a/c2/o2', HTTPOk,
|
||||||
|
{'Content-Length': '10'}, 'passed')
|
||||||
|
self.app.register(
|
||||||
|
'PUT', '/v1/a/c/o', HTTPOk, {}, 'passed')
|
||||||
|
cache = FakeCache({'object_count': 1, 'meta': {'quota-count': '2'}})
|
||||||
|
req = Request.blank('/v1/a/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT',
|
||||||
|
'swift.cache': cache},
|
||||||
|
headers={'x-copy-from': '/c2/o2'})
|
||||||
|
res = req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(res.status_int, 200)
|
||||||
|
|
||||||
|
def test_not_exceed_counts_quota_copy_verb(self):
|
||||||
|
self.app.register('GET', '/v1/a/c2/o2', HTTPOk,
|
||||||
|
{'Content-Length': '10'}, 'passed')
|
||||||
|
self.app.register(
|
||||||
|
'PUT', '/v1/a/c/o', HTTPOk, {}, 'passed')
|
||||||
|
cache = FakeCache({'object_count': 1, 'meta': {'quota-count': '2'}})
|
||||||
|
req = Request.blank('/v1/a/c2/o2',
|
||||||
|
environ={'REQUEST_METHOD': 'COPY',
|
||||||
|
'swift.cache': cache},
|
||||||
|
headers={'Destination': '/c/o'})
|
||||||
|
res = req.get_response(self.copy_filter)
|
||||||
|
self.assertEqual(res.status_int, 200)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -26,7 +26,7 @@ from swift.common import swob, utils
|
|||||||
from swift.common.exceptions import ListingIterError, SegmentError
|
from swift.common.exceptions import ListingIterError, SegmentError
|
||||||
from swift.common.header_key_dict import HeaderKeyDict
|
from swift.common.header_key_dict import HeaderKeyDict
|
||||||
from swift.common.middleware import slo
|
from swift.common.middleware import slo
|
||||||
from swift.common.swob import Request, Response, HTTPException
|
from swift.common.swob import Request, HTTPException
|
||||||
from swift.common.utils import quote, closing_if_possible, close_if_possible
|
from swift.common.utils import quote, closing_if_possible, close_if_possible
|
||||||
from test.unit.common.middleware.helpers import FakeSwift
|
from test.unit.common.middleware.helpers import FakeSwift
|
||||||
|
|
||||||
@ -2653,70 +2653,6 @@ class TestSloBulkLogger(unittest.TestCase):
|
|||||||
self.assertTrue(slo_mware.logger is slo_mware.bulk_deleter.logger)
|
self.assertTrue(slo_mware.logger is slo_mware.bulk_deleter.logger)
|
||||||
|
|
||||||
|
|
||||||
class TestSloCopyHook(SloTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestSloCopyHook, self).setUp()
|
|
||||||
|
|
||||||
self.app.register(
|
|
||||||
'GET', '/v1/AUTH_test/c/o', swob.HTTPOk,
|
|
||||||
{'Content-Length': '3', 'Etag': md5hex("obj")}, "obj")
|
|
||||||
self.app.register(
|
|
||||||
'GET', '/v1/AUTH_test/c/man',
|
|
||||||
swob.HTTPOk, {'Content-Type': 'application/json',
|
|
||||||
'X-Static-Large-Object': 'true'},
|
|
||||||
json.dumps([{'name': '/c/o', 'hash': md5hex("obj"),
|
|
||||||
'bytes': '3'}]))
|
|
||||||
self.app.register(
|
|
||||||
'COPY', '/v1/AUTH_test/c/o', swob.HTTPCreated, {})
|
|
||||||
|
|
||||||
copy_hook = [None]
|
|
||||||
|
|
||||||
# slip this guy in there to pull out the hook
|
|
||||||
def extract_copy_hook(env, sr):
|
|
||||||
if env['REQUEST_METHOD'] == 'COPY':
|
|
||||||
copy_hook[0] = env['swift.copy_hook']
|
|
||||||
return self.app(env, sr)
|
|
||||||
|
|
||||||
self.slo = slo.filter_factory({})(extract_copy_hook)
|
|
||||||
|
|
||||||
req = Request.blank('/v1/AUTH_test/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'COPY'})
|
|
||||||
self.slo(req.environ, fake_start_response)
|
|
||||||
self.copy_hook = copy_hook[0]
|
|
||||||
|
|
||||||
self.assertTrue(self.copy_hook is not None) # sanity check
|
|
||||||
|
|
||||||
def test_copy_hook_passthrough(self):
|
|
||||||
source_req = Request.blank(
|
|
||||||
'/v1/AUTH_test/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'GET'})
|
|
||||||
sink_req = Request.blank(
|
|
||||||
'/v1/AUTH_test/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'PUT'})
|
|
||||||
# no X-Static-Large-Object header, so do nothing
|
|
||||||
source_resp = Response(request=source_req, status=200)
|
|
||||||
|
|
||||||
modified_resp = self.copy_hook(source_req, source_resp, sink_req)
|
|
||||||
self.assertTrue(modified_resp is source_resp)
|
|
||||||
|
|
||||||
def test_copy_hook_manifest(self):
|
|
||||||
source_req = Request.blank(
|
|
||||||
'/v1/AUTH_test/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'GET'})
|
|
||||||
sink_req = Request.blank(
|
|
||||||
'/v1/AUTH_test/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'PUT'})
|
|
||||||
source_resp = Response(request=source_req, status=200,
|
|
||||||
headers={"X-Static-Large-Object": "true"},
|
|
||||||
app_iter=[json.dumps([{'name': '/c/o',
|
|
||||||
'hash': md5hex("obj"),
|
|
||||||
'bytes': '3'}])])
|
|
||||||
|
|
||||||
modified_resp = self.copy_hook(source_req, source_resp, sink_req)
|
|
||||||
self.assertTrue(modified_resp is not source_resp)
|
|
||||||
self.assertEqual(modified_resp.etag, md5hex(md5hex("obj")))
|
|
||||||
|
|
||||||
|
|
||||||
class TestSwiftInfo(unittest.TestCase):
|
class TestSwiftInfo(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
utils._swift_info = {}
|
utils._swift_info = {}
|
||||||
|
@ -19,7 +19,7 @@ import os
|
|||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
from swift.common import swob
|
from swift.common import swob
|
||||||
from swift.common.middleware import versioned_writes
|
from swift.common.middleware import versioned_writes, copy
|
||||||
from swift.common.swob import Request
|
from swift.common.swob import Request
|
||||||
from test.unit.common.middleware.helpers import FakeSwift
|
from test.unit.common.middleware.helpers import FakeSwift
|
||||||
|
|
||||||
@ -259,6 +259,23 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase):
|
|||||||
self.assertEqual(len(self.authorized), 1)
|
self.assertEqual(len(self.authorized), 1)
|
||||||
self.assertRequestEqual(req, self.authorized[0])
|
self.assertRequestEqual(req, self.authorized[0])
|
||||||
|
|
||||||
|
def test_put_object_post_as_copy(self):
|
||||||
|
# PUTs due to a post-as-copy should NOT cause a versioning op
|
||||||
|
self.app.register(
|
||||||
|
'PUT', '/v1/a/c/o', swob.HTTPCreated, {}, 'passed')
|
||||||
|
|
||||||
|
cache = FakeCache({'sysmeta': {'versions-location': 'ver_cont'}})
|
||||||
|
req = Request.blank(
|
||||||
|
'/v1/a/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT', 'swift.cache': cache,
|
||||||
|
'CONTENT_LENGTH': '100',
|
||||||
|
'swift.post_as_copy': True})
|
||||||
|
status, headers, body = self.call_vw(req)
|
||||||
|
self.assertEqual(status, '201 Created')
|
||||||
|
self.assertEqual(len(self.authorized), 1)
|
||||||
|
self.assertRequestEqual(req, self.authorized[0])
|
||||||
|
self.assertEqual(1, self.app.call_count)
|
||||||
|
|
||||||
def test_put_first_object_success(self):
|
def test_put_first_object_success(self):
|
||||||
self.app.register(
|
self.app.register(
|
||||||
'PUT', '/v1/a/c/o', swob.HTTPOk, {}, 'passed')
|
'PUT', '/v1/a/c/o', swob.HTTPOk, {}, 'passed')
|
||||||
@ -333,7 +350,7 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase):
|
|||||||
|
|
||||||
def test_delete_object_no_versioning_with_container_config_true(self):
|
def test_delete_object_no_versioning_with_container_config_true(self):
|
||||||
# set False to versions_write obviously and expect no GET versioning
|
# set False to versions_write obviously and expect no GET versioning
|
||||||
# container and PUT called (just delete object as normal)
|
# container and GET/PUT called (just delete object as normal)
|
||||||
self.vw.conf = {'allow_versioned_writes': 'false'}
|
self.vw.conf = {'allow_versioned_writes': 'false'}
|
||||||
self.app.register(
|
self.app.register(
|
||||||
'DELETE', '/v1/a/c/o', swob.HTTPNoContent, {}, 'passed')
|
'DELETE', '/v1/a/c/o', swob.HTTPNoContent, {}, 'passed')
|
||||||
@ -351,25 +368,6 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase):
|
|||||||
self.assertTrue('GET' not in called_method)
|
self.assertTrue('GET' not in called_method)
|
||||||
self.assertEqual(1, self.app.call_count)
|
self.assertEqual(1, self.app.call_count)
|
||||||
|
|
||||||
def test_copy_object_no_versioning_with_container_config_true(self):
|
|
||||||
# set False to versions_write obviously and expect no extra
|
|
||||||
# COPY called (just copy object as normal)
|
|
||||||
self.vw.conf = {'allow_versioned_writes': 'false'}
|
|
||||||
self.app.register(
|
|
||||||
'COPY', '/v1/a/c/o', swob.HTTPCreated, {}, None)
|
|
||||||
cache = FakeCache({'versions': 'ver_cont'})
|
|
||||||
req = Request.blank(
|
|
||||||
'/v1/a/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'COPY', 'swift.cache': cache})
|
|
||||||
status, headers, body = self.call_vw(req)
|
|
||||||
self.assertEqual(status, '201 Created')
|
|
||||||
self.assertEqual(len(self.authorized), 1)
|
|
||||||
self.assertRequestEqual(req, self.authorized[0])
|
|
||||||
called_method = \
|
|
||||||
[method for (method, path, rheaders) in self.app._calls]
|
|
||||||
self.assertTrue('COPY' in called_method)
|
|
||||||
self.assertEqual(called_method.count('COPY'), 1)
|
|
||||||
|
|
||||||
def test_new_version_success(self):
|
def test_new_version_success(self):
|
||||||
self.app.register(
|
self.app.register(
|
||||||
'PUT', '/v1/a/c/o', swob.HTTPCreated, {}, 'passed')
|
'PUT', '/v1/a/c/o', swob.HTTPCreated, {}, 'passed')
|
||||||
@ -476,77 +474,6 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase):
|
|||||||
self.assertEqual('PUT', method)
|
self.assertEqual('PUT', method)
|
||||||
self.assertEqual('/v1/a/ver_cont/001o/0000000000.00000', path)
|
self.assertEqual('/v1/a/ver_cont/001o/0000000000.00000', path)
|
||||||
|
|
||||||
def test_copy_first_version(self):
|
|
||||||
self.app.register(
|
|
||||||
'COPY', '/v1/a/src_cont/src_obj', swob.HTTPOk, {}, 'passed')
|
|
||||||
self.app.register(
|
|
||||||
'GET', '/v1/a/tgt_cont/tgt_obj', swob.HTTPNotFound, {}, None)
|
|
||||||
cache = FakeCache({'sysmeta': {'versions-location': 'ver_cont'}})
|
|
||||||
req = Request.blank(
|
|
||||||
'/v1/a/src_cont/src_obj',
|
|
||||||
environ={'REQUEST_METHOD': 'COPY', 'swift.cache': cache,
|
|
||||||
'CONTENT_LENGTH': '100'},
|
|
||||||
headers={'Destination': 'tgt_cont/tgt_obj'})
|
|
||||||
status, headers, body = self.call_vw(req)
|
|
||||||
self.assertEqual(status, '200 OK')
|
|
||||||
self.assertEqual(len(self.authorized), 1)
|
|
||||||
self.assertRequestEqual(req, self.authorized[0])
|
|
||||||
self.assertEqual(2, self.app.call_count)
|
|
||||||
|
|
||||||
def test_copy_new_version(self):
|
|
||||||
self.app.register(
|
|
||||||
'COPY', '/v1/a/src_cont/src_obj', swob.HTTPOk, {}, 'passed')
|
|
||||||
self.app.register(
|
|
||||||
'GET', '/v1/a/tgt_cont/tgt_obj', swob.HTTPOk,
|
|
||||||
{'last-modified': 'Thu, 1 Jan 1970 00:00:01 GMT'}, 'passed')
|
|
||||||
self.app.register(
|
|
||||||
'PUT', '/v1/a/ver_cont/007tgt_obj/0000000001.00000', swob.HTTPOk,
|
|
||||||
{}, None)
|
|
||||||
cache = FakeCache({'sysmeta': {'versions-location': 'ver_cont'}})
|
|
||||||
req = Request.blank(
|
|
||||||
'/v1/a/src_cont/src_obj',
|
|
||||||
environ={'REQUEST_METHOD': 'COPY', 'swift.cache': cache,
|
|
||||||
'CONTENT_LENGTH': '100'},
|
|
||||||
headers={'Destination': 'tgt_cont/tgt_obj'})
|
|
||||||
status, headers, body = self.call_vw(req)
|
|
||||||
self.assertEqual(status, '200 OK')
|
|
||||||
self.assertEqual(len(self.authorized), 1)
|
|
||||||
self.assertRequestEqual(req, self.authorized[0])
|
|
||||||
self.assertEqual(3, self.app.call_count)
|
|
||||||
|
|
||||||
def test_copy_new_version_different_account(self):
|
|
||||||
self.app.register(
|
|
||||||
'COPY', '/v1/src_a/src_cont/src_obj', swob.HTTPOk, {}, 'passed')
|
|
||||||
self.app.register(
|
|
||||||
'GET', '/v1/tgt_a/tgt_cont/tgt_obj', swob.HTTPOk,
|
|
||||||
{'last-modified': 'Thu, 1 Jan 1970 00:00:01 GMT'}, 'passed')
|
|
||||||
self.app.register(
|
|
||||||
'PUT', '/v1/tgt_a/ver_cont/007tgt_obj/0000000001.00000',
|
|
||||||
swob.HTTPOk, {}, None)
|
|
||||||
cache = FakeCache({'sysmeta': {'versions-location': 'ver_cont'}})
|
|
||||||
req = Request.blank(
|
|
||||||
'/v1/src_a/src_cont/src_obj',
|
|
||||||
environ={'REQUEST_METHOD': 'COPY', 'swift.cache': cache,
|
|
||||||
'CONTENT_LENGTH': '100'},
|
|
||||||
headers={'Destination': 'tgt_cont/tgt_obj',
|
|
||||||
'Destination-Account': 'tgt_a'})
|
|
||||||
status, headers, body = self.call_vw(req)
|
|
||||||
self.assertEqual(status, '200 OK')
|
|
||||||
self.assertEqual(len(self.authorized), 1)
|
|
||||||
self.assertRequestEqual(req, self.authorized[0])
|
|
||||||
self.assertEqual(3, self.app.call_count)
|
|
||||||
|
|
||||||
def test_copy_new_version_bogus_account(self):
|
|
||||||
cache = FakeCache({'sysmeta': {'versions-location': 'ver_cont'}})
|
|
||||||
req = Request.blank(
|
|
||||||
'/v1/src_a/src_cont/src_obj',
|
|
||||||
environ={'REQUEST_METHOD': 'COPY', 'swift.cache': cache,
|
|
||||||
'CONTENT_LENGTH': '100'},
|
|
||||||
headers={'Destination': 'tgt_cont/tgt_obj',
|
|
||||||
'Destination-Account': '/im/on/a/boat'})
|
|
||||||
status, headers, body = self.call_vw(req)
|
|
||||||
self.assertEqual(status, '412 Precondition Failed')
|
|
||||||
|
|
||||||
def test_delete_first_object_success(self):
|
def test_delete_first_object_success(self):
|
||||||
self.app.register(
|
self.app.register(
|
||||||
'DELETE', '/v1/a/c/o', swob.HTTPOk, {}, 'passed')
|
'DELETE', '/v1/a/c/o', swob.HTTPOk, {}, 'passed')
|
||||||
@ -1057,3 +984,117 @@ class VersionedWritesOldContainersTestCase(VersionedWritesBaseTestCase):
|
|||||||
('PUT', '/v1/a/c/o'),
|
('PUT', '/v1/a/c/o'),
|
||||||
('DELETE', '/v1/a/ver_cont/001o/2'),
|
('DELETE', '/v1/a/ver_cont/001o/2'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class VersionedWritesCopyingTestCase(VersionedWritesBaseTestCase):
|
||||||
|
# verify interaction of copy and versioned_writes middlewares
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.app = FakeSwift()
|
||||||
|
conf = {'allow_versioned_writes': 'true'}
|
||||||
|
self.vw = versioned_writes.filter_factory(conf)(self.app)
|
||||||
|
self.filter = copy.filter_factory({})(self.vw)
|
||||||
|
|
||||||
|
def call_filter(self, req, **kwargs):
|
||||||
|
return self.call_app(req, app=self.filter, **kwargs)
|
||||||
|
|
||||||
|
def test_copy_first_version(self):
|
||||||
|
# no existing object to move to the versions container
|
||||||
|
self.app.register(
|
||||||
|
'GET', '/v1/a/tgt_cont/tgt_obj', swob.HTTPNotFound, {}, None)
|
||||||
|
self.app.register(
|
||||||
|
'GET', '/v1/a/src_cont/src_obj', swob.HTTPOk, {}, 'passed')
|
||||||
|
self.app.register(
|
||||||
|
'PUT', '/v1/a/tgt_cont/tgt_obj', swob.HTTPCreated, {}, 'passed')
|
||||||
|
cache = FakeCache({'sysmeta': {'versions-location': 'ver_cont'}})
|
||||||
|
req = Request.blank(
|
||||||
|
'/v1/a/src_cont/src_obj',
|
||||||
|
environ={'REQUEST_METHOD': 'COPY', 'swift.cache': cache,
|
||||||
|
'CONTENT_LENGTH': '100'},
|
||||||
|
headers={'Destination': 'tgt_cont/tgt_obj'})
|
||||||
|
status, headers, body = self.call_filter(req)
|
||||||
|
self.assertEqual(status, '201 Created')
|
||||||
|
self.assertEqual(len(self.authorized), 2)
|
||||||
|
self.assertEqual('GET', self.authorized[0].method)
|
||||||
|
self.assertEqual('/v1/a/src_cont/src_obj', self.authorized[0].path)
|
||||||
|
self.assertEqual('PUT', self.authorized[1].method)
|
||||||
|
self.assertEqual('/v1/a/tgt_cont/tgt_obj', self.authorized[1].path)
|
||||||
|
# note the GET on tgt_cont/tgt_obj is pre-authed
|
||||||
|
self.assertEqual(3, self.app.call_count, self.app.calls)
|
||||||
|
|
||||||
|
def test_copy_new_version(self):
|
||||||
|
# existing object should be moved to versions container
|
||||||
|
self.app.register(
|
||||||
|
'GET', '/v1/a/src_cont/src_obj', swob.HTTPOk, {}, 'passed')
|
||||||
|
self.app.register(
|
||||||
|
'GET', '/v1/a/tgt_cont/tgt_obj', swob.HTTPOk,
|
||||||
|
{'last-modified': 'Thu, 1 Jan 1970 00:00:01 GMT'}, 'passed')
|
||||||
|
self.app.register(
|
||||||
|
'PUT', '/v1/a/ver_cont/007tgt_obj/0000000001.00000', swob.HTTPOk,
|
||||||
|
{}, None)
|
||||||
|
self.app.register(
|
||||||
|
'PUT', '/v1/a/tgt_cont/tgt_obj', swob.HTTPCreated, {}, 'passed')
|
||||||
|
cache = FakeCache({'sysmeta': {'versions-location': 'ver_cont'}})
|
||||||
|
req = Request.blank(
|
||||||
|
'/v1/a/src_cont/src_obj',
|
||||||
|
environ={'REQUEST_METHOD': 'COPY', 'swift.cache': cache,
|
||||||
|
'CONTENT_LENGTH': '100'},
|
||||||
|
headers={'Destination': 'tgt_cont/tgt_obj'})
|
||||||
|
status, headers, body = self.call_filter(req)
|
||||||
|
self.assertEqual(status, '201 Created')
|
||||||
|
self.assertEqual(len(self.authorized), 2)
|
||||||
|
self.assertEqual('GET', self.authorized[0].method)
|
||||||
|
self.assertEqual('/v1/a/src_cont/src_obj', self.authorized[0].path)
|
||||||
|
self.assertEqual('PUT', self.authorized[1].method)
|
||||||
|
self.assertEqual('/v1/a/tgt_cont/tgt_obj', self.authorized[1].path)
|
||||||
|
self.assertEqual(4, self.app.call_count)
|
||||||
|
|
||||||
|
def test_copy_new_version_different_account(self):
|
||||||
|
self.app.register(
|
||||||
|
'GET', '/v1/src_a/src_cont/src_obj', swob.HTTPOk, {}, 'passed')
|
||||||
|
self.app.register(
|
||||||
|
'GET', '/v1/tgt_a/tgt_cont/tgt_obj', swob.HTTPOk,
|
||||||
|
{'last-modified': 'Thu, 1 Jan 1970 00:00:01 GMT'}, 'passed')
|
||||||
|
self.app.register(
|
||||||
|
'PUT', '/v1/tgt_a/ver_cont/007tgt_obj/0000000001.00000',
|
||||||
|
swob.HTTPOk, {}, None)
|
||||||
|
self.app.register(
|
||||||
|
'PUT', '/v1/tgt_a/tgt_cont/tgt_obj', swob.HTTPCreated, {},
|
||||||
|
'passed')
|
||||||
|
cache = FakeCache({'sysmeta': {'versions-location': 'ver_cont'}})
|
||||||
|
req = Request.blank(
|
||||||
|
'/v1/src_a/src_cont/src_obj',
|
||||||
|
environ={'REQUEST_METHOD': 'COPY', 'swift.cache': cache,
|
||||||
|
'CONTENT_LENGTH': '100'},
|
||||||
|
headers={'Destination': 'tgt_cont/tgt_obj',
|
||||||
|
'Destination-Account': 'tgt_a'})
|
||||||
|
status, headers, body = self.call_filter(req)
|
||||||
|
self.assertEqual(status, '201 Created')
|
||||||
|
self.assertEqual(len(self.authorized), 2)
|
||||||
|
self.assertEqual('GET', self.authorized[0].method)
|
||||||
|
self.assertEqual('/v1/src_a/src_cont/src_obj', self.authorized[0].path)
|
||||||
|
self.assertEqual('PUT', self.authorized[1].method)
|
||||||
|
self.assertEqual('/v1/tgt_a/tgt_cont/tgt_obj', self.authorized[1].path)
|
||||||
|
self.assertEqual(4, self.app.call_count)
|
||||||
|
|
||||||
|
def test_copy_object_no_versioning_with_container_config_true(self):
|
||||||
|
# set False to versions_write obviously and expect no extra
|
||||||
|
# COPY called (just copy object as normal)
|
||||||
|
self.vw.conf = {'allow_versioned_writes': 'false'}
|
||||||
|
self.app.register(
|
||||||
|
'GET', '/v1/a/src_cont/src_obj', swob.HTTPOk, {}, 'passed')
|
||||||
|
self.app.register(
|
||||||
|
'PUT', '/v1/a/tgt_cont/tgt_obj', swob.HTTPCreated, {}, 'passed')
|
||||||
|
cache = FakeCache({'versions': 'ver_cont'})
|
||||||
|
req = Request.blank(
|
||||||
|
'/v1/a/src_cont/src_obj',
|
||||||
|
environ={'REQUEST_METHOD': 'COPY', 'swift.cache': cache},
|
||||||
|
headers={'Destination': '/tgt_cont/tgt_obj'})
|
||||||
|
status, headers, body = self.call_filter(req)
|
||||||
|
self.assertEqual(status, '201 Created')
|
||||||
|
self.assertEqual(len(self.authorized), 2)
|
||||||
|
self.assertEqual('GET', self.authorized[0].method)
|
||||||
|
self.assertEqual('/v1/a/src_cont/src_obj', self.authorized[0].path)
|
||||||
|
self.assertEqual('PUT', self.authorized[1].method)
|
||||||
|
self.assertEqual('/v1/a/tgt_cont/tgt_obj', self.authorized[1].path)
|
||||||
|
self.assertEqual(2, self.app.call_count)
|
||||||
|
@ -173,33 +173,6 @@ class TestConstraints(unittest.TestCase):
|
|||||||
'/', headers=headers), 'object_name').status_int,
|
'/', headers=headers), 'object_name').status_int,
|
||||||
HTTP_NOT_IMPLEMENTED)
|
HTTP_NOT_IMPLEMENTED)
|
||||||
|
|
||||||
def test_check_object_creation_copy(self):
|
|
||||||
headers = {'Content-Length': '0',
|
|
||||||
'X-Copy-From': 'c/o2',
|
|
||||||
'Content-Type': 'text/plain'}
|
|
||||||
self.assertEqual(constraints.check_object_creation(Request.blank(
|
|
||||||
'/', headers=headers), 'object_name'), None)
|
|
||||||
|
|
||||||
headers = {'Content-Length': '1',
|
|
||||||
'X-Copy-From': 'c/o2',
|
|
||||||
'Content-Type': 'text/plain'}
|
|
||||||
self.assertEqual(constraints.check_object_creation(Request.blank(
|
|
||||||
'/', headers=headers), 'object_name').status_int,
|
|
||||||
HTTP_BAD_REQUEST)
|
|
||||||
|
|
||||||
headers = {'Transfer-Encoding': 'chunked',
|
|
||||||
'X-Copy-From': 'c/o2',
|
|
||||||
'Content-Type': 'text/plain'}
|
|
||||||
self.assertEqual(constraints.check_object_creation(Request.blank(
|
|
||||||
'/', headers=headers), 'object_name'), None)
|
|
||||||
|
|
||||||
# a content-length header is always required
|
|
||||||
headers = {'X-Copy-From': 'c/o2',
|
|
||||||
'Content-Type': 'text/plain'}
|
|
||||||
self.assertEqual(constraints.check_object_creation(Request.blank(
|
|
||||||
'/', headers=headers), 'object_name').status_int,
|
|
||||||
HTTP_LENGTH_REQUIRED)
|
|
||||||
|
|
||||||
def test_check_object_creation_name_length(self):
|
def test_check_object_creation_name_length(self):
|
||||||
headers = {'Transfer-Encoding': 'chunked',
|
headers = {'Transfer-Encoding': 'chunked',
|
||||||
'Content-Type': 'text/plain'}
|
'Content-Type': 'text/plain'}
|
||||||
@ -459,60 +432,6 @@ class TestConstraints(unittest.TestCase):
|
|||||||
self.assertTrue(c.MAX_HEADER_SIZE > c.MAX_META_NAME_LENGTH)
|
self.assertTrue(c.MAX_HEADER_SIZE > c.MAX_META_NAME_LENGTH)
|
||||||
self.assertTrue(c.MAX_HEADER_SIZE > c.MAX_META_VALUE_LENGTH)
|
self.assertTrue(c.MAX_HEADER_SIZE > c.MAX_META_VALUE_LENGTH)
|
||||||
|
|
||||||
def test_validate_copy_from(self):
|
|
||||||
req = Request.blank(
|
|
||||||
'/v/a/c/o',
|
|
||||||
headers={'x-copy-from': 'c/o2'})
|
|
||||||
src_cont, src_obj = constraints.check_copy_from_header(req)
|
|
||||||
self.assertEqual(src_cont, 'c')
|
|
||||||
self.assertEqual(src_obj, 'o2')
|
|
||||||
req = Request.blank(
|
|
||||||
'/v/a/c/o',
|
|
||||||
headers={'x-copy-from': 'c/subdir/o2'})
|
|
||||||
src_cont, src_obj = constraints.check_copy_from_header(req)
|
|
||||||
self.assertEqual(src_cont, 'c')
|
|
||||||
self.assertEqual(src_obj, 'subdir/o2')
|
|
||||||
req = Request.blank(
|
|
||||||
'/v/a/c/o',
|
|
||||||
headers={'x-copy-from': '/c/o2'})
|
|
||||||
src_cont, src_obj = constraints.check_copy_from_header(req)
|
|
||||||
self.assertEqual(src_cont, 'c')
|
|
||||||
self.assertEqual(src_obj, 'o2')
|
|
||||||
|
|
||||||
def test_validate_bad_copy_from(self):
|
|
||||||
req = Request.blank(
|
|
||||||
'/v/a/c/o',
|
|
||||||
headers={'x-copy-from': 'bad_object'})
|
|
||||||
self.assertRaises(HTTPException,
|
|
||||||
constraints.check_copy_from_header, req)
|
|
||||||
|
|
||||||
def test_validate_destination(self):
|
|
||||||
req = Request.blank(
|
|
||||||
'/v/a/c/o',
|
|
||||||
headers={'destination': 'c/o2'})
|
|
||||||
src_cont, src_obj = constraints.check_destination_header(req)
|
|
||||||
self.assertEqual(src_cont, 'c')
|
|
||||||
self.assertEqual(src_obj, 'o2')
|
|
||||||
req = Request.blank(
|
|
||||||
'/v/a/c/o',
|
|
||||||
headers={'destination': 'c/subdir/o2'})
|
|
||||||
src_cont, src_obj = constraints.check_destination_header(req)
|
|
||||||
self.assertEqual(src_cont, 'c')
|
|
||||||
self.assertEqual(src_obj, 'subdir/o2')
|
|
||||||
req = Request.blank(
|
|
||||||
'/v/a/c/o',
|
|
||||||
headers={'destination': '/c/o2'})
|
|
||||||
src_cont, src_obj = constraints.check_destination_header(req)
|
|
||||||
self.assertEqual(src_cont, 'c')
|
|
||||||
self.assertEqual(src_obj, 'o2')
|
|
||||||
|
|
||||||
def test_validate_bad_destination(self):
|
|
||||||
req = Request.blank(
|
|
||||||
'/v/a/c/o',
|
|
||||||
headers={'destination': 'bad_object'})
|
|
||||||
self.assertRaises(HTTPException,
|
|
||||||
constraints.check_destination_header, req)
|
|
||||||
|
|
||||||
def test_check_account_format(self):
|
def test_check_account_format(self):
|
||||||
req = Request.blank(
|
req = Request.blank(
|
||||||
'/v/a/c/o',
|
'/v/a/c/o',
|
||||||
|
@ -431,9 +431,10 @@ class TestRequest(unittest.TestCase):
|
|||||||
def test_invalid_req_environ_property_args(self):
|
def test_invalid_req_environ_property_args(self):
|
||||||
# getter only property
|
# getter only property
|
||||||
try:
|
try:
|
||||||
swift.common.swob.Request.blank('/', params={'a': 'b'})
|
swift.common.swob.Request.blank(
|
||||||
|
'/', host_url='http://example.com:8080/v1/a/c/o')
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
self.assertEqual("got unexpected keyword argument 'params'",
|
self.assertEqual("got unexpected keyword argument 'host_url'",
|
||||||
str(e))
|
str(e))
|
||||||
else:
|
else:
|
||||||
self.assertTrue(False, "invalid req_environ_property "
|
self.assertTrue(False, "invalid req_environ_property "
|
||||||
@ -525,6 +526,14 @@ class TestRequest(unittest.TestCase):
|
|||||||
self.assertEqual(req.params['a'], 'b')
|
self.assertEqual(req.params['a'], 'b')
|
||||||
self.assertEqual(req.params['c'], 'd')
|
self.assertEqual(req.params['c'], 'd')
|
||||||
|
|
||||||
|
new_params = {'e': 'f', 'g': 'h'}
|
||||||
|
req.params = new_params
|
||||||
|
self.assertDictEqual(new_params, req.params)
|
||||||
|
|
||||||
|
new_params = (('i', 'j'), ('k', 'l'))
|
||||||
|
req.params = new_params
|
||||||
|
self.assertDictEqual(dict(new_params), req.params)
|
||||||
|
|
||||||
def test_timestamp_missing(self):
|
def test_timestamp_missing(self):
|
||||||
req = swift.common.swob.Request.blank('/')
|
req = swift.common.swob.Request.blank('/')
|
||||||
self.assertRaises(exceptions.InvalidTimestamp,
|
self.assertRaises(exceptions.InvalidTimestamp,
|
||||||
|
@ -136,6 +136,11 @@ class TestWSGI(unittest.TestCase):
|
|||||||
expected = swift.common.middleware.gatekeeper.GatekeeperMiddleware
|
expected = swift.common.middleware.gatekeeper.GatekeeperMiddleware
|
||||||
self.assertTrue(isinstance(app, expected))
|
self.assertTrue(isinstance(app, expected))
|
||||||
|
|
||||||
|
app = app.app
|
||||||
|
expected = \
|
||||||
|
swift.common.middleware.copy.ServerSideCopyMiddleware
|
||||||
|
self.assertIsInstance(app, expected)
|
||||||
|
|
||||||
app = app.app
|
app = app.app
|
||||||
expected = swift.common.middleware.dlo.DynamicLargeObject
|
expected = swift.common.middleware.dlo.DynamicLargeObject
|
||||||
self.assertTrue(isinstance(app, expected))
|
self.assertTrue(isinstance(app, expected))
|
||||||
@ -1437,6 +1442,7 @@ class TestPipelineModification(unittest.TestCase):
|
|||||||
self.assertEqual(self.pipeline_modules(app),
|
self.assertEqual(self.pipeline_modules(app),
|
||||||
['swift.common.middleware.catch_errors',
|
['swift.common.middleware.catch_errors',
|
||||||
'swift.common.middleware.gatekeeper',
|
'swift.common.middleware.gatekeeper',
|
||||||
|
'swift.common.middleware.copy',
|
||||||
'swift.common.middleware.dlo',
|
'swift.common.middleware.dlo',
|
||||||
'swift.common.middleware.versioned_writes',
|
'swift.common.middleware.versioned_writes',
|
||||||
'swift.proxy.server'])
|
'swift.proxy.server'])
|
||||||
@ -1468,6 +1474,7 @@ class TestPipelineModification(unittest.TestCase):
|
|||||||
self.assertEqual(self.pipeline_modules(app),
|
self.assertEqual(self.pipeline_modules(app),
|
||||||
['swift.common.middleware.catch_errors',
|
['swift.common.middleware.catch_errors',
|
||||||
'swift.common.middleware.gatekeeper',
|
'swift.common.middleware.gatekeeper',
|
||||||
|
'swift.common.middleware.copy',
|
||||||
'swift.common.middleware.dlo',
|
'swift.common.middleware.dlo',
|
||||||
'swift.common.middleware.versioned_writes',
|
'swift.common.middleware.versioned_writes',
|
||||||
'swift.common.middleware.healthcheck',
|
'swift.common.middleware.healthcheck',
|
||||||
@ -1506,6 +1513,7 @@ class TestPipelineModification(unittest.TestCase):
|
|||||||
self.assertEqual(self.pipeline_modules(app),
|
self.assertEqual(self.pipeline_modules(app),
|
||||||
['swift.common.middleware.catch_errors',
|
['swift.common.middleware.catch_errors',
|
||||||
'swift.common.middleware.gatekeeper',
|
'swift.common.middleware.gatekeeper',
|
||||||
|
'swift.common.middleware.copy',
|
||||||
'swift.common.middleware.slo',
|
'swift.common.middleware.slo',
|
||||||
'swift.common.middleware.dlo',
|
'swift.common.middleware.dlo',
|
||||||
'swift.common.middleware.versioned_writes',
|
'swift.common.middleware.versioned_writes',
|
||||||
@ -1605,6 +1613,7 @@ class TestPipelineModification(unittest.TestCase):
|
|||||||
self.assertEqual(self.pipeline_modules(app), [
|
self.assertEqual(self.pipeline_modules(app), [
|
||||||
'swift.common.middleware.catch_errors',
|
'swift.common.middleware.catch_errors',
|
||||||
'swift.common.middleware.gatekeeper',
|
'swift.common.middleware.gatekeeper',
|
||||||
|
'swift.common.middleware.copy',
|
||||||
'swift.common.middleware.dlo',
|
'swift.common.middleware.dlo',
|
||||||
'swift.common.middleware.versioned_writes',
|
'swift.common.middleware.versioned_writes',
|
||||||
'swift.common.middleware.healthcheck',
|
'swift.common.middleware.healthcheck',
|
||||||
@ -1619,6 +1628,7 @@ class TestPipelineModification(unittest.TestCase):
|
|||||||
'swift.common.middleware.gatekeeper',
|
'swift.common.middleware.gatekeeper',
|
||||||
'swift.common.middleware.healthcheck',
|
'swift.common.middleware.healthcheck',
|
||||||
'swift.common.middleware.catch_errors',
|
'swift.common.middleware.catch_errors',
|
||||||
|
'swift.common.middleware.copy',
|
||||||
'swift.common.middleware.dlo',
|
'swift.common.middleware.dlo',
|
||||||
'swift.common.middleware.versioned_writes',
|
'swift.common.middleware.versioned_writes',
|
||||||
'swift.proxy.server'])
|
'swift.proxy.server'])
|
||||||
@ -1632,6 +1642,7 @@ class TestPipelineModification(unittest.TestCase):
|
|||||||
'swift.common.middleware.healthcheck',
|
'swift.common.middleware.healthcheck',
|
||||||
'swift.common.middleware.catch_errors',
|
'swift.common.middleware.catch_errors',
|
||||||
'swift.common.middleware.gatekeeper',
|
'swift.common.middleware.gatekeeper',
|
||||||
|
'swift.common.middleware.copy',
|
||||||
'swift.common.middleware.dlo',
|
'swift.common.middleware.dlo',
|
||||||
'swift.common.middleware.versioned_writes',
|
'swift.common.middleware.versioned_writes',
|
||||||
'swift.proxy.server'])
|
'swift.proxy.server'])
|
||||||
@ -1666,7 +1677,7 @@ class TestPipelineModification(unittest.TestCase):
|
|||||||
tempdir, policy.ring_name + '.ring.gz')
|
tempdir, policy.ring_name + '.ring.gz')
|
||||||
|
|
||||||
app = wsgi.loadapp(conf_path)
|
app = wsgi.loadapp(conf_path)
|
||||||
proxy_app = app.app.app.app.app.app
|
proxy_app = app.app.app.app.app.app.app
|
||||||
self.assertEqual(proxy_app.account_ring.serialized_path,
|
self.assertEqual(proxy_app.account_ring.serialized_path,
|
||||||
account_ring_path)
|
account_ring_path)
|
||||||
self.assertEqual(proxy_app.container_ring.serialized_path,
|
self.assertEqual(proxy_app.container_ring.serialized_path,
|
||||||
|
@ -649,7 +649,7 @@ class TestReplicatedObjController(BaseObjectControllerMixin,
|
|||||||
def test_PUT_error_during_transfer_data(self):
|
def test_PUT_error_during_transfer_data(self):
|
||||||
class FakeReader(object):
|
class FakeReader(object):
|
||||||
def read(self, size):
|
def read(self, size):
|
||||||
raise exceptions.ChunkReadError('exception message')
|
raise IOError('error message')
|
||||||
|
|
||||||
req = swob.Request.blank('/v1/a/c/o.jpg', method='PUT',
|
req = swob.Request.blank('/v1/a/c/o.jpg', method='PUT',
|
||||||
body='test body')
|
body='test body')
|
||||||
@ -747,62 +747,6 @@ class TestReplicatedObjController(BaseObjectControllerMixin,
|
|||||||
resp = req.get_response(self.app)
|
resp = req.get_response(self.app)
|
||||||
self.assertEqual(resp.status_int, 404)
|
self.assertEqual(resp.status_int, 404)
|
||||||
|
|
||||||
def test_POST_as_COPY_simple(self):
|
|
||||||
req = swift.common.swob.Request.blank('/v1/a/c/o', method='POST')
|
|
||||||
get_resp = [200] * self.obj_ring.replicas + \
|
|
||||||
[404] * self.obj_ring.max_more_nodes
|
|
||||||
put_resp = [201] * self.obj_ring.replicas
|
|
||||||
codes = get_resp + put_resp
|
|
||||||
with set_http_connect(*codes):
|
|
||||||
resp = req.get_response(self.app)
|
|
||||||
self.assertEqual(resp.status_int, 202)
|
|
||||||
self.assertEqual(req.environ['QUERY_STRING'], '')
|
|
||||||
self.assertTrue('swift.post_as_copy' in req.environ)
|
|
||||||
|
|
||||||
def test_POST_as_COPY_static_large_object(self):
|
|
||||||
req = swift.common.swob.Request.blank('/v1/a/c/o', method='POST')
|
|
||||||
get_resp = [200] * self.obj_ring.replicas + \
|
|
||||||
[404] * self.obj_ring.max_more_nodes
|
|
||||||
put_resp = [201] * self.obj_ring.replicas
|
|
||||||
codes = get_resp + put_resp
|
|
||||||
slo_headers = \
|
|
||||||
[{'X-Static-Large-Object': True}] * self.obj_ring.replicas
|
|
||||||
get_headers = slo_headers + [{}] * (len(codes) - len(slo_headers))
|
|
||||||
headers = {'headers': get_headers}
|
|
||||||
with set_http_connect(*codes, **headers):
|
|
||||||
resp = req.get_response(self.app)
|
|
||||||
self.assertEqual(resp.status_int, 202)
|
|
||||||
self.assertEqual(req.environ['QUERY_STRING'], '')
|
|
||||||
self.assertTrue('swift.post_as_copy' in req.environ)
|
|
||||||
|
|
||||||
def test_POST_delete_at(self):
|
|
||||||
t = str(int(time.time() + 100))
|
|
||||||
req = swob.Request.blank('/v1/a/c/o', method='POST',
|
|
||||||
headers={'Content-Type': 'foo/bar',
|
|
||||||
'X-Delete-At': t})
|
|
||||||
post_headers = []
|
|
||||||
|
|
||||||
def capture_headers(ip, port, device, part, method, path, headers,
|
|
||||||
**kwargs):
|
|
||||||
if method == 'POST':
|
|
||||||
post_headers.append(headers)
|
|
||||||
x_newest_responses = [200] * self.obj_ring.replicas + \
|
|
||||||
[404] * self.obj_ring.max_more_nodes
|
|
||||||
post_resp = [200] * self.obj_ring.replicas
|
|
||||||
codes = x_newest_responses + post_resp
|
|
||||||
with set_http_connect(*codes, give_connect=capture_headers):
|
|
||||||
resp = req.get_response(self.app)
|
|
||||||
self.assertEqual(resp.status_int, 200)
|
|
||||||
self.assertEqual(req.environ['QUERY_STRING'], '') # sanity
|
|
||||||
self.assertTrue('swift.post_as_copy' in req.environ)
|
|
||||||
|
|
||||||
for given_headers in post_headers:
|
|
||||||
self.assertEqual(given_headers.get('X-Delete-At'), t)
|
|
||||||
self.assertTrue('X-Delete-At-Host' in given_headers)
|
|
||||||
self.assertTrue('X-Delete-At-Device' in given_headers)
|
|
||||||
self.assertTrue('X-Delete-At-Partition' in given_headers)
|
|
||||||
self.assertTrue('X-Delete-At-Container' in given_headers)
|
|
||||||
|
|
||||||
def test_PUT_delete_at(self):
|
def test_PUT_delete_at(self):
|
||||||
t = str(int(time.time() + 100))
|
t = str(int(time.time() + 100))
|
||||||
req = swob.Request.blank('/v1/a/c/o', method='PUT', body='',
|
req = swob.Request.blank('/v1/a/c/o', method='PUT', body='',
|
||||||
@ -1000,43 +944,6 @@ class TestReplicatedObjController(BaseObjectControllerMixin,
|
|||||||
resp = req.get_response(self.app)
|
resp = req.get_response(self.app)
|
||||||
self.assertEqual(resp.status_int, 202)
|
self.assertEqual(resp.status_int, 202)
|
||||||
|
|
||||||
def test_COPY_simple(self):
|
|
||||||
req = swift.common.swob.Request.blank(
|
|
||||||
'/v1/a/c/o', method='COPY',
|
|
||||||
headers={'Content-Length': 0,
|
|
||||||
'Destination': 'c/o-copy'})
|
|
||||||
head_resp = [200] * self.obj_ring.replicas + \
|
|
||||||
[404] * self.obj_ring.max_more_nodes
|
|
||||||
put_resp = [201] * self.obj_ring.replicas
|
|
||||||
codes = head_resp + put_resp
|
|
||||||
with set_http_connect(*codes):
|
|
||||||
resp = req.get_response(self.app)
|
|
||||||
self.assertEqual(resp.status_int, 201)
|
|
||||||
|
|
||||||
def test_PUT_log_info(self):
|
|
||||||
req = swift.common.swob.Request.blank('/v1/a/c/o', method='PUT')
|
|
||||||
req.headers['x-copy-from'] = 'some/where'
|
|
||||||
req.headers['Content-Length'] = 0
|
|
||||||
# override FakeConn default resp headers to keep log_info clean
|
|
||||||
resp_headers = {'x-delete-at': None}
|
|
||||||
head_resp = [200] * self.obj_ring.replicas + \
|
|
||||||
[404] * self.obj_ring.max_more_nodes
|
|
||||||
put_resp = [201] * self.obj_ring.replicas
|
|
||||||
codes = head_resp + put_resp
|
|
||||||
with set_http_connect(*codes, headers=resp_headers):
|
|
||||||
resp = req.get_response(self.app)
|
|
||||||
self.assertEqual(resp.status_int, 201)
|
|
||||||
self.assertEqual(
|
|
||||||
req.environ.get('swift.log_info'), ['x-copy-from:some/where'])
|
|
||||||
# and then check that we don't do that for originating POSTs
|
|
||||||
req = swift.common.swob.Request.blank('/v1/a/c/o')
|
|
||||||
req.method = 'POST'
|
|
||||||
req.headers['x-copy-from'] = 'else/where'
|
|
||||||
with set_http_connect(*codes, headers=resp_headers):
|
|
||||||
resp = req.get_response(self.app)
|
|
||||||
self.assertEqual(resp.status_int, 202)
|
|
||||||
self.assertEqual(req.environ.get('swift.log_info'), None)
|
|
||||||
|
|
||||||
|
|
||||||
@patch_policies(
|
@patch_policies(
|
||||||
[StoragePolicy(0, '1-replica', True),
|
[StoragePolicy(0, '1-replica', True),
|
||||||
@ -1397,7 +1304,7 @@ class TestECObjController(BaseObjectControllerMixin, unittest.TestCase):
|
|||||||
def test_PUT_ec_error_during_transfer_data(self):
|
def test_PUT_ec_error_during_transfer_data(self):
|
||||||
class FakeReader(object):
|
class FakeReader(object):
|
||||||
def read(self, size):
|
def read(self, size):
|
||||||
raise exceptions.ChunkReadError('exception message')
|
raise IOError('error message')
|
||||||
|
|
||||||
req = swob.Request.blank('/v1/a/c/o.jpg', method='PUT',
|
req = swob.Request.blank('/v1/a/c/o.jpg', method='PUT',
|
||||||
body='test body')
|
body='test body')
|
||||||
@ -1603,72 +1510,6 @@ class TestECObjController(BaseObjectControllerMixin, unittest.TestCase):
|
|||||||
resp = req.get_response(self.app)
|
resp = req.get_response(self.app)
|
||||||
self.assertEqual(resp.status_int, 201)
|
self.assertEqual(resp.status_int, 201)
|
||||||
|
|
||||||
def test_COPY_cross_policy_type_from_replicated(self):
|
|
||||||
self.app.per_container_info = {
|
|
||||||
'c1': self.app.container_info.copy(),
|
|
||||||
'c2': self.app.container_info.copy(),
|
|
||||||
}
|
|
||||||
# make c2 use replicated storage policy 1
|
|
||||||
self.app.per_container_info['c2']['storage_policy'] = '1'
|
|
||||||
|
|
||||||
# a put request with copy from source c2
|
|
||||||
req = swift.common.swob.Request.blank('/v1/a/c1/o', method='PUT',
|
|
||||||
body='', headers={
|
|
||||||
'X-Copy-From': 'c2/o'})
|
|
||||||
|
|
||||||
# c2 get
|
|
||||||
codes = [200] * self.replicas(POLICIES[1])
|
|
||||||
codes += [404] * POLICIES[1].object_ring.max_more_nodes
|
|
||||||
# c1 put
|
|
||||||
codes += [201] * self.replicas()
|
|
||||||
expect_headers = {
|
|
||||||
'X-Obj-Metadata-Footer': 'yes',
|
|
||||||
'X-Obj-Multiphase-Commit': 'yes'
|
|
||||||
}
|
|
||||||
with set_http_connect(*codes, expect_headers=expect_headers):
|
|
||||||
resp = req.get_response(self.app)
|
|
||||||
self.assertEqual(resp.status_int, 201)
|
|
||||||
|
|
||||||
def test_COPY_cross_policy_type_to_replicated(self):
|
|
||||||
self.app.per_container_info = {
|
|
||||||
'c1': self.app.container_info.copy(),
|
|
||||||
'c2': self.app.container_info.copy(),
|
|
||||||
}
|
|
||||||
# make c1 use replicated storage policy 1
|
|
||||||
self.app.per_container_info['c1']['storage_policy'] = '1'
|
|
||||||
|
|
||||||
# a put request with copy from source c2
|
|
||||||
req = swift.common.swob.Request.blank('/v1/a/c1/o', method='PUT',
|
|
||||||
body='', headers={
|
|
||||||
'X-Copy-From': 'c2/o'})
|
|
||||||
|
|
||||||
# c2 get
|
|
||||||
codes = [404, 200] * self.policy.ec_ndata
|
|
||||||
headers = {
|
|
||||||
'X-Object-Sysmeta-Ec-Content-Length': 0,
|
|
||||||
}
|
|
||||||
# c1 put
|
|
||||||
codes += [201] * self.replicas(POLICIES[1])
|
|
||||||
with set_http_connect(*codes, headers=headers):
|
|
||||||
resp = req.get_response(self.app)
|
|
||||||
self.assertEqual(resp.status_int, 201)
|
|
||||||
|
|
||||||
def test_COPY_cross_policy_type_unknown(self):
|
|
||||||
self.app.per_container_info = {
|
|
||||||
'c1': self.app.container_info.copy(),
|
|
||||||
'c2': self.app.container_info.copy(),
|
|
||||||
}
|
|
||||||
# make c1 use some made up storage policy index
|
|
||||||
self.app.per_container_info['c1']['storage_policy'] = '13'
|
|
||||||
|
|
||||||
# a COPY request of c2 with destination in c1
|
|
||||||
req = swift.common.swob.Request.blank('/v1/a/c2/o', method='COPY',
|
|
||||||
body='', headers={
|
|
||||||
'Destination': 'c1/o'})
|
|
||||||
with set_http_connect():
|
|
||||||
resp = req.get_response(self.app)
|
|
||||||
self.assertEqual(resp.status_int, 503)
|
|
||||||
|
|
||||||
def _make_ec_archive_bodies(self, test_body, policy=None):
|
def _make_ec_archive_bodies(self, test_body, policy=None):
|
||||||
policy = policy or self.policy
|
policy = policy or self.policy
|
||||||
segment_size = policy.ec_segment_size
|
segment_size = policy.ec_segment_size
|
||||||
@ -2378,40 +2219,6 @@ class TestECObjController(BaseObjectControllerMixin, unittest.TestCase):
|
|||||||
resp = req.get_response(self.app)
|
resp = req.get_response(self.app)
|
||||||
self.assertEqual(resp.status_int, 503)
|
self.assertEqual(resp.status_int, 503)
|
||||||
|
|
||||||
def test_COPY_with_ranges(self):
|
|
||||||
req = swift.common.swob.Request.blank(
|
|
||||||
'/v1/a/c/o', method='COPY',
|
|
||||||
headers={'Destination': 'c1/o',
|
|
||||||
'Range': 'bytes=5-10'})
|
|
||||||
# turn a real body into fragments
|
|
||||||
segment_size = self.policy.ec_segment_size
|
|
||||||
real_body = ('asdf' * segment_size)[:-10]
|
|
||||||
|
|
||||||
# split it up into chunks
|
|
||||||
chunks = [real_body[x:x + segment_size]
|
|
||||||
for x in range(0, len(real_body), segment_size)]
|
|
||||||
|
|
||||||
# we need only first chunk to rebuild 5-10 range
|
|
||||||
fragments = self.policy.pyeclib_driver.encode(chunks[0])
|
|
||||||
fragment_payloads = []
|
|
||||||
fragment_payloads.append(fragments)
|
|
||||||
|
|
||||||
node_fragments = zip(*fragment_payloads)
|
|
||||||
self.assertEqual(len(node_fragments), self.replicas()) # sanity
|
|
||||||
headers = {'X-Object-Sysmeta-Ec-Content-Length': str(len(real_body))}
|
|
||||||
responses = [(200, ''.join(node_fragments[i]), headers)
|
|
||||||
for i in range(POLICIES.default.ec_ndata)]
|
|
||||||
responses += [(201, '', {})] * self.obj_ring.replicas
|
|
||||||
status_codes, body_iter, headers = zip(*responses)
|
|
||||||
expect_headers = {
|
|
||||||
'X-Obj-Metadata-Footer': 'yes',
|
|
||||||
'X-Obj-Multiphase-Commit': 'yes'
|
|
||||||
}
|
|
||||||
with set_http_connect(*status_codes, body_iter=body_iter,
|
|
||||||
headers=headers, expect_headers=expect_headers):
|
|
||||||
resp = req.get_response(self.app)
|
|
||||||
self.assertEqual(resp.status_int, 201)
|
|
||||||
|
|
||||||
def test_GET_with_invalid_ranges(self):
|
def test_GET_with_invalid_ranges(self):
|
||||||
# real body size is segment_size - 10 (just 1 segment)
|
# real body size is segment_size - 10 (just 1 segment)
|
||||||
segment_size = self.policy.ec_segment_size
|
segment_size = self.policy.ec_segment_size
|
||||||
@ -2424,18 +2231,6 @@ class TestECObjController(BaseObjectControllerMixin, unittest.TestCase):
|
|||||||
self._test_invalid_ranges('GET', real_body,
|
self._test_invalid_ranges('GET', real_body,
|
||||||
segment_size, '%s-' % (segment_size + 10))
|
segment_size, '%s-' % (segment_size + 10))
|
||||||
|
|
||||||
def test_COPY_with_invalid_ranges(self):
|
|
||||||
# real body size is segment_size - 10 (just 1 segment)
|
|
||||||
segment_size = self.policy.ec_segment_size
|
|
||||||
real_body = ('a' * segment_size)[:-10]
|
|
||||||
|
|
||||||
# range is out of real body but in segment size
|
|
||||||
self._test_invalid_ranges('COPY', real_body,
|
|
||||||
segment_size, '%s-' % (segment_size - 10))
|
|
||||||
# range is out of both real body and segment size
|
|
||||||
self._test_invalid_ranges('COPY', real_body,
|
|
||||||
segment_size, '%s-' % (segment_size + 10))
|
|
||||||
|
|
||||||
def _test_invalid_ranges(self, method, real_body, segment_size, req_range):
|
def _test_invalid_ranges(self, method, real_body, segment_size, req_range):
|
||||||
# make a request with range starts from more than real size.
|
# make a request with range starts from more than real size.
|
||||||
body_etag = md5(real_body).hexdigest()
|
body_etag = md5(real_body).hexdigest()
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -283,94 +283,3 @@ class TestObjectSysmeta(unittest.TestCase):
|
|||||||
self._assertInHeaders(resp, self.changed_sysmeta_headers)
|
self._assertInHeaders(resp, self.changed_sysmeta_headers)
|
||||||
self._assertInHeaders(resp, self.new_sysmeta_headers)
|
self._assertInHeaders(resp, self.new_sysmeta_headers)
|
||||||
self._assertNotInHeaders(resp, self.original_sysmeta_headers_2)
|
self._assertNotInHeaders(resp, self.original_sysmeta_headers_2)
|
||||||
|
|
||||||
def test_sysmeta_not_updated_by_POST(self):
|
|
||||||
self.app.object_post_as_copy = False
|
|
||||||
self._test_sysmeta_not_updated_by_POST()
|
|
||||||
|
|
||||||
def test_sysmeta_not_updated_by_POST_as_copy(self):
|
|
||||||
self.app.object_post_as_copy = True
|
|
||||||
self._test_sysmeta_not_updated_by_POST()
|
|
||||||
|
|
||||||
def test_sysmeta_updated_by_COPY(self):
|
|
||||||
# check sysmeta is updated by a COPY in same way as user meta
|
|
||||||
path = '/v1/a/c/o'
|
|
||||||
dest = '/c/o2'
|
|
||||||
env = {'REQUEST_METHOD': 'PUT'}
|
|
||||||
hdrs = dict(self.original_sysmeta_headers_1)
|
|
||||||
hdrs.update(self.original_sysmeta_headers_2)
|
|
||||||
hdrs.update(self.original_meta_headers_1)
|
|
||||||
hdrs.update(self.original_meta_headers_2)
|
|
||||||
req = Request.blank(path, environ=env, headers=hdrs, body='x')
|
|
||||||
resp = req.get_response(self.app)
|
|
||||||
self._assertStatus(resp, 201)
|
|
||||||
|
|
||||||
env = {'REQUEST_METHOD': 'COPY'}
|
|
||||||
hdrs = dict(self.changed_sysmeta_headers)
|
|
||||||
hdrs.update(self.new_sysmeta_headers)
|
|
||||||
hdrs.update(self.changed_meta_headers)
|
|
||||||
hdrs.update(self.new_meta_headers)
|
|
||||||
hdrs.update(self.bad_headers)
|
|
||||||
hdrs.update({'Destination': dest})
|
|
||||||
req = Request.blank(path, environ=env, headers=hdrs)
|
|
||||||
resp = req.get_response(self.app)
|
|
||||||
self._assertStatus(resp, 201)
|
|
||||||
self._assertInHeaders(resp, self.changed_sysmeta_headers)
|
|
||||||
self._assertInHeaders(resp, self.new_sysmeta_headers)
|
|
||||||
self._assertInHeaders(resp, self.original_sysmeta_headers_2)
|
|
||||||
self._assertInHeaders(resp, self.changed_meta_headers)
|
|
||||||
self._assertInHeaders(resp, self.new_meta_headers)
|
|
||||||
self._assertInHeaders(resp, self.original_meta_headers_2)
|
|
||||||
self._assertNotInHeaders(resp, self.bad_headers)
|
|
||||||
|
|
||||||
req = Request.blank('/v1/a/c/o2', environ={})
|
|
||||||
resp = req.get_response(self.app)
|
|
||||||
self._assertStatus(resp, 200)
|
|
||||||
self._assertInHeaders(resp, self.changed_sysmeta_headers)
|
|
||||||
self._assertInHeaders(resp, self.new_sysmeta_headers)
|
|
||||||
self._assertInHeaders(resp, self.original_sysmeta_headers_2)
|
|
||||||
self._assertInHeaders(resp, self.changed_meta_headers)
|
|
||||||
self._assertInHeaders(resp, self.new_meta_headers)
|
|
||||||
self._assertInHeaders(resp, self.original_meta_headers_2)
|
|
||||||
self._assertNotInHeaders(resp, self.bad_headers)
|
|
||||||
|
|
||||||
def test_sysmeta_updated_by_COPY_from(self):
|
|
||||||
# check sysmeta is updated by a COPY in same way as user meta
|
|
||||||
path = '/v1/a/c/o'
|
|
||||||
env = {'REQUEST_METHOD': 'PUT'}
|
|
||||||
hdrs = dict(self.original_sysmeta_headers_1)
|
|
||||||
hdrs.update(self.original_sysmeta_headers_2)
|
|
||||||
hdrs.update(self.original_meta_headers_1)
|
|
||||||
hdrs.update(self.original_meta_headers_2)
|
|
||||||
req = Request.blank(path, environ=env, headers=hdrs, body='x')
|
|
||||||
resp = req.get_response(self.app)
|
|
||||||
self._assertStatus(resp, 201)
|
|
||||||
|
|
||||||
env = {'REQUEST_METHOD': 'PUT'}
|
|
||||||
hdrs = dict(self.changed_sysmeta_headers)
|
|
||||||
hdrs.update(self.new_sysmeta_headers)
|
|
||||||
hdrs.update(self.changed_meta_headers)
|
|
||||||
hdrs.update(self.new_meta_headers)
|
|
||||||
hdrs.update(self.bad_headers)
|
|
||||||
hdrs.update({'X-Copy-From': '/c/o'})
|
|
||||||
req = Request.blank('/v1/a/c/o2', environ=env, headers=hdrs, body='')
|
|
||||||
resp = req.get_response(self.app)
|
|
||||||
self._assertStatus(resp, 201)
|
|
||||||
self._assertInHeaders(resp, self.changed_sysmeta_headers)
|
|
||||||
self._assertInHeaders(resp, self.new_sysmeta_headers)
|
|
||||||
self._assertInHeaders(resp, self.original_sysmeta_headers_2)
|
|
||||||
self._assertInHeaders(resp, self.changed_meta_headers)
|
|
||||||
self._assertInHeaders(resp, self.new_meta_headers)
|
|
||||||
self._assertInHeaders(resp, self.original_meta_headers_2)
|
|
||||||
self._assertNotInHeaders(resp, self.bad_headers)
|
|
||||||
|
|
||||||
req = Request.blank('/v1/a/c/o2', environ={})
|
|
||||||
resp = req.get_response(self.app)
|
|
||||||
self._assertStatus(resp, 200)
|
|
||||||
self._assertInHeaders(resp, self.changed_sysmeta_headers)
|
|
||||||
self._assertInHeaders(resp, self.new_sysmeta_headers)
|
|
||||||
self._assertInHeaders(resp, self.original_sysmeta_headers_2)
|
|
||||||
self._assertInHeaders(resp, self.changed_meta_headers)
|
|
||||||
self._assertInHeaders(resp, self.new_meta_headers)
|
|
||||||
self._assertInHeaders(resp, self.original_meta_headers_2)
|
|
||||||
self._assertNotInHeaders(resp, self.bad_headers)
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user