Add copy object method
Implement copy object method in swiftclient Connection, Service and CLI. Although COPY functionality can be accomplished with 'X-Copy-From' header in PUT request, using copy is more convenient especially when using copy for updating object metadata non-destructively. Closes-Bug: 1474939 Change-Id: I1338ac411f418f4adb3d06753d044a484a7f32a4
This commit is contained in:
parent
e05464fbfa
commit
4a2465fb12
@ -79,6 +79,19 @@ For more details and options see swift post \-\-help.
|
|||||||
\fBExample\fR: post \-m Color:Blue \-m Size:Large
|
\fBExample\fR: post \-m Color:Blue \-m Size:Large
|
||||||
.RE
|
.RE
|
||||||
|
|
||||||
|
\fBcopy\fR [\fIcommand-options\fR] \fIcontainer\fR \fIobject\fR
|
||||||
|
.RS 4
|
||||||
|
Copies an object to a new destination or adds user metadata to the object (current
|
||||||
|
user metadata will be preserved, in contrast with the post command) depending
|
||||||
|
on the args given. The \-\-destination option sets the destination in the form
|
||||||
|
/container/object. If not set, the object will be copied onto itself which is useful
|
||||||
|
for adding metadata. The \-M or \-\-fresh\-metadata option copies the object without
|
||||||
|
the existing user metadata. The \-m or \-\-meta option is always allowed and is used
|
||||||
|
to define the user metadata items to set in the form Name:Value (this option
|
||||||
|
can be repeated).
|
||||||
|
For more details and options see swift copy \-\-help.
|
||||||
|
.RE
|
||||||
|
|
||||||
\fBdownload\fR [\fIcommand-options\fR] [\fIcontainer\fR] [\fIobject\fR] [\fIobject\fR] [...]
|
\fBdownload\fR [\fIcommand-options\fR] [\fIcontainer\fR] [\fIobject\fR] [\fIobject\fR] [...]
|
||||||
.RS 4
|
.RS 4
|
||||||
Downloads everything in the account (with \-\-all), or everything in a
|
Downloads everything in the account (with \-\-all), or everything in a
|
||||||
|
@ -199,6 +199,20 @@ Delete
|
|||||||
of manifest objects will be deleted as well, unless you specify the
|
of manifest objects will be deleted as well, unless you specify the
|
||||||
``--leave-segments`` option.
|
``--leave-segments`` option.
|
||||||
|
|
||||||
|
Copy
|
||||||
|
----
|
||||||
|
|
||||||
|
``copy [command-options] container object``
|
||||||
|
|
||||||
|
Copies an object to a new destination or adds user metadata to an object. Depending
|
||||||
|
on the options supplied, you can preserve existing metadata in contrast to the post
|
||||||
|
command. The ``--destination`` option sets the copy target destination in the form
|
||||||
|
``/container/object``. If not set, the object will be copied onto itself which is useful
|
||||||
|
for adding metadata. You can use the ``-M`` or ``--fresh-metadata`` option to copy
|
||||||
|
an object without existing user meta data, and the ``-m`` or ``--meta`` option
|
||||||
|
to define user meta data items to set in the form ``Name:Value``. You can repeat
|
||||||
|
this option. For example: ``copy -m Color:Blue -m Size:Large``.
|
||||||
|
|
||||||
Capabilities
|
Capabilities
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
@ -217,6 +217,17 @@ Options
|
|||||||
are downloaded in lexically-sorted order. Setting this option to ``True``
|
are downloaded in lexically-sorted order. Setting this option to ``True``
|
||||||
gives the same shuffling behaviour as the CLI.
|
gives the same shuffling behaviour as the CLI.
|
||||||
|
|
||||||
|
``destination``: ``None``
|
||||||
|
When copying objects, this specifies the destination where the object
|
||||||
|
will be copied to. The default of None means copy will be the same as
|
||||||
|
source.
|
||||||
|
|
||||||
|
``fresh_metadata``: ``None``
|
||||||
|
When copying objects, this specifies that the object metadata on the
|
||||||
|
source will *not* be applied to the destination object - the
|
||||||
|
destination object will have a new fresh set of metadata that includes
|
||||||
|
*only* the metadata specified in the meta option if any at all.
|
||||||
|
|
||||||
Other available options can be found in ``swiftclient/service.py`` in the
|
Other available options can be found in ``swiftclient/service.py`` in the
|
||||||
source code for ``python-swiftclient``. Each ``SwiftService`` method also allows
|
source code for ``python-swiftclient``. Each ``SwiftService`` method also allows
|
||||||
for an optional dictionary to override those specified at init time, and the
|
for an optional dictionary to override those specified at init time, and the
|
||||||
@ -735,13 +746,76 @@ Example
|
|||||||
|
|
||||||
The code below demonstrates the use of ``delete`` to remove a given list of
|
The code below demonstrates the use of ``delete`` to remove a given list of
|
||||||
objects from a specified container. As the objects are deleted the transaction
|
objects from a specified container. As the objects are deleted the transaction
|
||||||
id of the relevant request is printed along with the object name and number
|
ID of the relevant request is printed along with the object name and number
|
||||||
of attempts required. By printing the transaction id, the printed operations
|
of attempts required. By printing the transaction ID, the printed operations
|
||||||
can be easily linked to events in the swift server logs:
|
can be easily linked to events in the swift server logs:
|
||||||
|
|
||||||
.. literalinclude:: ../../examples/delete.py
|
.. literalinclude:: ../../examples/delete.py
|
||||||
:language: python
|
:language: python
|
||||||
|
|
||||||
|
Copy
|
||||||
|
~~~~
|
||||||
|
|
||||||
|
Copy can be called to copy an object or update the metadata on the given items.
|
||||||
|
|
||||||
|
Each element of the object list may be a plain string of the object name, or a
|
||||||
|
``SwiftCopyObject`` that allows finer control over the options applied to each
|
||||||
|
of the individual copy operations (destination, fresh_metadata, options).
|
||||||
|
|
||||||
|
Destination should be in format /container/object; if not set, the object will be
|
||||||
|
copied onto itself. Fresh_metadata sets mode of operation on metadata. If not set,
|
||||||
|
current object user metadata will be copied/preserved; if set, all current user
|
||||||
|
metadata will be removed.
|
||||||
|
|
||||||
|
Returns an iterator over the results generated for each object copy (and may
|
||||||
|
also include the results of creating destination containers).
|
||||||
|
|
||||||
|
When a string is given for the object name, destination and fresh metadata will
|
||||||
|
default to None and None, which result in adding metadata to existing objects.
|
||||||
|
|
||||||
|
Successful copy results are dictionaries as described below:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
{
|
||||||
|
'action': 'copy_object',
|
||||||
|
'success': True,
|
||||||
|
'container': <container>,
|
||||||
|
'object': <object>,
|
||||||
|
'destination': <destination>,
|
||||||
|
'headers': {},
|
||||||
|
'fresh_metadata': <boolean>,
|
||||||
|
'response_dict': <HTTP response details>
|
||||||
|
}
|
||||||
|
|
||||||
|
Any failure in a copy operation will return a failure dictionary as described
|
||||||
|
below:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
{
|
||||||
|
'action': 'copy_object',
|
||||||
|
'success': False,
|
||||||
|
'container': <container>,
|
||||||
|
'object': <object>,
|
||||||
|
'destination': <destination>,
|
||||||
|
'headers': {},
|
||||||
|
'fresh_metadata': <boolean>,
|
||||||
|
'response_dict': <HTTP response details>,
|
||||||
|
'error': <error>,
|
||||||
|
'traceback': <traceback>,
|
||||||
|
'error_timestamp': <timestamp>
|
||||||
|
}
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
The code below demonstrates the use of ``copy`` to add new user metadata for
|
||||||
|
objects a and b, and to copy object c to d (with added metadata).
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/copy.py
|
||||||
|
:language: python
|
||||||
|
|
||||||
Capabilities
|
Capabilities
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
@ -764,7 +838,7 @@ returned with the contents described below:
|
|||||||
}
|
}
|
||||||
|
|
||||||
The contents of the capabilities dictionary contain the core swift capabilities
|
The contents of the capabilities dictionary contain the core swift capabilities
|
||||||
under the key ``swift``, all other keys show the configuration options for
|
under the key ``swift``; all other keys show the configuration options for
|
||||||
additional middlewares deployed in the proxy pipeline. An example capabilities
|
additional middlewares deployed in the proxy pipeline. An example capabilities
|
||||||
dictionary is given below:
|
dictionary is given below:
|
||||||
|
|
||||||
@ -819,7 +893,7 @@ Example
|
|||||||
^^^^^^^
|
^^^^^^^
|
||||||
|
|
||||||
The code below demonstrates the use of ``capabilities`` to determine if the
|
The code below demonstrates the use of ``capabilities`` to determine if the
|
||||||
Swift cluster supports static large objects, and if so, the maximum number of
|
Swift cluster supports static large objects, and if so, the maximum number of
|
||||||
segments that can be described in a single manifest file, along with the
|
segments that can be described in a single manifest file, along with the
|
||||||
size restrictions on those objects:
|
size restrictions on those objects:
|
||||||
|
|
||||||
|
30
examples/copy.py
Normal file
30
examples/copy.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from swiftclient.service import SwiftService, SwiftCopyObject, SwiftError
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.ERROR)
|
||||||
|
logging.getLogger("requests").setLevel(logging.CRITICAL)
|
||||||
|
logging.getLogger("swiftclient").setLevel(logging.CRITICAL)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
with SwiftService() as swift:
|
||||||
|
try:
|
||||||
|
obj = SwiftCopyObject("c", {"Destination": "/cont/d"})
|
||||||
|
for i in swift.copy(
|
||||||
|
"cont", ["a", "b", obj],
|
||||||
|
{"meta": ["foo:bar"], "Destination": "/cc"}):
|
||||||
|
if i["success"]:
|
||||||
|
if i["action"] == "copy_object":
|
||||||
|
print(
|
||||||
|
"object %s copied from /%s/%s" %
|
||||||
|
(i["destination"], i["container"], i["object"])
|
||||||
|
)
|
||||||
|
if i["action"] == "create_container":
|
||||||
|
print(
|
||||||
|
"container %s created" % i["container"]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if "error" in i and isinstance(i["error"], Exception):
|
||||||
|
raise i["error"]
|
||||||
|
except SwiftError as e:
|
||||||
|
logger.error(e.value)
|
@ -1319,6 +1319,70 @@ def post_object(url, token, container, name, headers, http_conn=None,
|
|||||||
raise ClientException.from_response(resp, 'Object POST failed', body)
|
raise ClientException.from_response(resp, 'Object POST failed', body)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_object(url, token, container, name, destination=None,
|
||||||
|
headers=None, fresh_metadata=None, http_conn=None,
|
||||||
|
response_dict=None, service_token=None):
|
||||||
|
"""
|
||||||
|
Copy object
|
||||||
|
|
||||||
|
:param url: storage URL
|
||||||
|
:param token: auth token; if None, no token will be sent
|
||||||
|
:param container: container name that the source object is in
|
||||||
|
:param name: source object name
|
||||||
|
:param destination: The container and object name of the destination object
|
||||||
|
in the form of /container/object; if None, the copy
|
||||||
|
will use the source as the destination.
|
||||||
|
:param headers: additional headers to include in the request
|
||||||
|
:param fresh_metadata: Enables object creation that omits existing user
|
||||||
|
metadata, default None
|
||||||
|
:param http_conn: HTTP connection object (If None, it will create the
|
||||||
|
conn object)
|
||||||
|
:param response_dict: an optional dictionary into which to place
|
||||||
|
the response - status, reason and headers
|
||||||
|
:param service_token: service auth token
|
||||||
|
:raises ClientException: HTTP COPY request failed
|
||||||
|
"""
|
||||||
|
if http_conn:
|
||||||
|
parsed, conn = http_conn
|
||||||
|
else:
|
||||||
|
parsed, conn = http_connection(url)
|
||||||
|
|
||||||
|
path = parsed.path
|
||||||
|
container = quote(container)
|
||||||
|
name = quote(name)
|
||||||
|
path = '%s/%s/%s' % (path.rstrip('/'), container, name)
|
||||||
|
|
||||||
|
headers = dict(headers) if headers else {}
|
||||||
|
|
||||||
|
if destination is not None:
|
||||||
|
headers['Destination'] = quote(destination)
|
||||||
|
elif container and name:
|
||||||
|
headers['Destination'] = '/%s/%s' % (container, name)
|
||||||
|
|
||||||
|
if token is not None:
|
||||||
|
headers['X-Auth-Token'] = token
|
||||||
|
if service_token is not None:
|
||||||
|
headers['X-Service-Token'] = service_token
|
||||||
|
|
||||||
|
if fresh_metadata is not None:
|
||||||
|
# remove potential fresh metadata headers
|
||||||
|
for fresh_hdr in [hdr for hdr in headers.keys()
|
||||||
|
if hdr.lower() == 'x-fresh-metadata']:
|
||||||
|
headers.pop(fresh_hdr)
|
||||||
|
headers['X-Fresh-Metadata'] = 'true' if fresh_metadata else 'false'
|
||||||
|
|
||||||
|
conn.request('COPY', path, '', headers)
|
||||||
|
resp = conn.getresponse()
|
||||||
|
body = resp.read()
|
||||||
|
http_log(('%s%s' % (url.replace(parsed.path, ''), path), 'COPY',),
|
||||||
|
{'headers': headers}, resp, body)
|
||||||
|
|
||||||
|
store_response(resp, response_dict)
|
||||||
|
|
||||||
|
if resp.status < 200 or resp.status >= 300:
|
||||||
|
raise ClientException.from_response(resp, 'Object COPY failed', body)
|
||||||
|
|
||||||
|
|
||||||
def delete_object(url, token=None, container=None, name=None, http_conn=None,
|
def delete_object(url, token=None, container=None, name=None, http_conn=None,
|
||||||
headers=None, proxy=None, query_string=None,
|
headers=None, proxy=None, query_string=None,
|
||||||
response_dict=None, service_token=None):
|
response_dict=None, service_token=None):
|
||||||
@ -1711,6 +1775,13 @@ class Connection(object):
|
|||||||
return self._retry(None, post_object, container, obj, headers,
|
return self._retry(None, post_object, container, obj, headers,
|
||||||
response_dict=response_dict)
|
response_dict=response_dict)
|
||||||
|
|
||||||
|
def copy_object(self, container, obj, destination=None, headers=None,
|
||||||
|
fresh_metadata=None, response_dict=None):
|
||||||
|
"""Wrapper for :func:`copy_object`"""
|
||||||
|
return self._retry(None, copy_object, container, obj, destination,
|
||||||
|
headers, fresh_metadata,
|
||||||
|
response_dict=response_dict)
|
||||||
|
|
||||||
def delete_object(self, container, obj, query_string=None,
|
def delete_object(self, container, obj, query_string=None,
|
||||||
response_dict=None):
|
response_dict=None):
|
||||||
"""Wrapper for :func:`delete_object`"""
|
"""Wrapper for :func:`delete_object`"""
|
||||||
|
@ -200,7 +200,9 @@ _default_local_options = {
|
|||||||
'human': False,
|
'human': False,
|
||||||
'dir_marker': False,
|
'dir_marker': False,
|
||||||
'checksum': True,
|
'checksum': True,
|
||||||
'shuffle': False
|
'shuffle': False,
|
||||||
|
'destination': None,
|
||||||
|
'fresh_metadata': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
POLICY = 'X-Storage-Policy'
|
POLICY = 'X-Storage-Policy'
|
||||||
@ -330,6 +332,42 @@ class SwiftPostObject(object):
|
|||||||
self.options = options
|
self.options = options
|
||||||
|
|
||||||
|
|
||||||
|
class SwiftCopyObject(object):
|
||||||
|
"""
|
||||||
|
Class for specifying an object copy,
|
||||||
|
allowing the destination/headers/metadata/fresh_metadata to be specified
|
||||||
|
separately for each individual object.
|
||||||
|
destination and fresh_metadata should be set in options
|
||||||
|
"""
|
||||||
|
def __init__(self, object_name, options=None):
|
||||||
|
if not isinstance(object_name, string_types) or not object_name:
|
||||||
|
raise SwiftError(
|
||||||
|
"Object names must be specified as non-empty strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.object_name = object_name
|
||||||
|
self.options = options
|
||||||
|
|
||||||
|
if self.options is None:
|
||||||
|
self.destination = None
|
||||||
|
self.fresh_metadata = False
|
||||||
|
else:
|
||||||
|
self.destination = self.options.get('destination')
|
||||||
|
self.fresh_metadata = self.options.get('fresh_metadata', False)
|
||||||
|
|
||||||
|
if self.destination is not None:
|
||||||
|
destination_components = self.destination.split('/')
|
||||||
|
if destination_components[0] or len(destination_components) < 2:
|
||||||
|
raise SwiftError("destination must be in format /cont[/obj]")
|
||||||
|
if not destination_components[-1]:
|
||||||
|
raise SwiftError("destination must not end in a slash")
|
||||||
|
if len(destination_components) == 2:
|
||||||
|
# only container set in destination
|
||||||
|
self.destination = "{0}/{1}".format(
|
||||||
|
self.destination, object_name
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class _SwiftReader(object):
|
class _SwiftReader(object):
|
||||||
"""
|
"""
|
||||||
Class for downloading objects from swift and raising appropriate
|
Class for downloading objects from swift and raising appropriate
|
||||||
@ -2389,6 +2427,191 @@ class SwiftService(object):
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
# Copy related methods
|
||||||
|
#
|
||||||
|
def copy(self, container, objects, options=None):
|
||||||
|
"""
|
||||||
|
Copy operations on a list of objects in a container. Destination
|
||||||
|
containers will be created.
|
||||||
|
|
||||||
|
:param container: The container from which to copy the objects.
|
||||||
|
:param objects: A list of object names (strings) or SwiftCopyObject
|
||||||
|
instances containing an object name and an
|
||||||
|
options dict (can be None) to override the options for
|
||||||
|
that individual copy operation::
|
||||||
|
|
||||||
|
[
|
||||||
|
'object_name',
|
||||||
|
SwiftCopyObject(
|
||||||
|
'object_name',
|
||||||
|
options={
|
||||||
|
'destination': '/container/object',
|
||||||
|
'fresh_metadata': False,
|
||||||
|
...
|
||||||
|
}),
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
The options dict is described below.
|
||||||
|
:param options: A dictionary containing options to override the global
|
||||||
|
options specified during the service object creation.
|
||||||
|
These options are applied to all copy operations
|
||||||
|
performed by this call, unless overridden on a per
|
||||||
|
object basis.
|
||||||
|
The options "destination" and "fresh_metadata" do
|
||||||
|
not need to be set, in this case objects will be
|
||||||
|
copied onto themselves and metadata will not be
|
||||||
|
refreshed.
|
||||||
|
The option "destination" can also be specified in the
|
||||||
|
format '/container', in which case objects without an
|
||||||
|
explicit destination will be copied to the destination
|
||||||
|
/container/original_object_name. Combinations of
|
||||||
|
multiple objects and a destination in the format
|
||||||
|
'/container/object' is invalid. Possible options are
|
||||||
|
given below::
|
||||||
|
|
||||||
|
{
|
||||||
|
'meta': [],
|
||||||
|
'header': [],
|
||||||
|
'destination': '/container/object',
|
||||||
|
'fresh_metadata': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
:returns: A generator returning the results of copying the given list
|
||||||
|
of objects.
|
||||||
|
|
||||||
|
:raises: SwiftError
|
||||||
|
"""
|
||||||
|
if options is not None:
|
||||||
|
options = dict(self._options, **options)
|
||||||
|
else:
|
||||||
|
options = self._options
|
||||||
|
|
||||||
|
# Try to create the container, just in case it doesn't exist. If this
|
||||||
|
# fails, it might just be because the user doesn't have container PUT
|
||||||
|
# permissions, so we'll ignore any error. If there's really a problem,
|
||||||
|
# it'll surface on the first object COPY.
|
||||||
|
containers = set(
|
||||||
|
next(p for p in obj.destination.split("/") if p)
|
||||||
|
for obj in objects
|
||||||
|
if isinstance(obj, SwiftCopyObject) and obj.destination
|
||||||
|
)
|
||||||
|
if options.get('destination'):
|
||||||
|
destination_split = options['destination'].split('/')
|
||||||
|
if destination_split[0]:
|
||||||
|
raise SwiftError("destination must be in format /cont[/obj]")
|
||||||
|
_str_objs = [
|
||||||
|
o for o in objects if not isinstance(o, SwiftCopyObject)
|
||||||
|
]
|
||||||
|
if len(destination_split) > 2 and len(_str_objs) > 1:
|
||||||
|
# TODO (clayg): could be useful to copy multiple objects into
|
||||||
|
# a destination like "/container/common/prefix/for/objects/"
|
||||||
|
# where the trailing "/" indicates the destination option is a
|
||||||
|
# prefix!
|
||||||
|
raise SwiftError("Combination of multiple objects and "
|
||||||
|
"destination including object is invalid")
|
||||||
|
if destination_split[-1] == '':
|
||||||
|
# N.B. this protects the above case
|
||||||
|
raise SwiftError("destination can not end in a slash")
|
||||||
|
containers.add(destination_split[1])
|
||||||
|
|
||||||
|
policy_header = {}
|
||||||
|
_header = split_headers(options["header"])
|
||||||
|
if POLICY in _header:
|
||||||
|
policy_header[POLICY] = _header[POLICY]
|
||||||
|
create_containers = [
|
||||||
|
self.thread_manager.container_pool.submit(
|
||||||
|
self._create_container_job, cont, headers=policy_header)
|
||||||
|
for cont in containers
|
||||||
|
]
|
||||||
|
|
||||||
|
# wait for container creation jobs to complete before any COPY
|
||||||
|
for r in interruptable_as_completed(create_containers):
|
||||||
|
res = r.result()
|
||||||
|
yield res
|
||||||
|
|
||||||
|
copy_futures = []
|
||||||
|
copy_objects = self._make_copy_objects(objects, options)
|
||||||
|
for copy_object in copy_objects:
|
||||||
|
obj = copy_object.object_name
|
||||||
|
obj_options = copy_object.options
|
||||||
|
destination = copy_object.destination
|
||||||
|
fresh_metadata = copy_object.fresh_metadata
|
||||||
|
headers = split_headers(
|
||||||
|
options['meta'], 'X-Object-Meta-')
|
||||||
|
# add header options to the headers object for the request.
|
||||||
|
headers.update(
|
||||||
|
split_headers(options['header'], ''))
|
||||||
|
if obj_options is not None:
|
||||||
|
if 'meta' in obj_options:
|
||||||
|
headers.update(
|
||||||
|
split_headers(
|
||||||
|
obj_options['meta'], 'X-Object-Meta-'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if 'header' in obj_options:
|
||||||
|
headers.update(
|
||||||
|
split_headers(obj_options['header'], '')
|
||||||
|
)
|
||||||
|
|
||||||
|
copy = self.thread_manager.object_uu_pool.submit(
|
||||||
|
self._copy_object_job, container, obj, destination,
|
||||||
|
headers, fresh_metadata
|
||||||
|
)
|
||||||
|
copy_futures.append(copy)
|
||||||
|
|
||||||
|
for r in interruptable_as_completed(copy_futures):
|
||||||
|
res = r.result()
|
||||||
|
yield res
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _make_copy_objects(objects, options):
|
||||||
|
copy_objects = []
|
||||||
|
|
||||||
|
for o in objects:
|
||||||
|
if isinstance(o, string_types):
|
||||||
|
obj = SwiftCopyObject(o, options)
|
||||||
|
copy_objects.append(obj)
|
||||||
|
elif isinstance(o, SwiftCopyObject):
|
||||||
|
copy_objects.append(o)
|
||||||
|
else:
|
||||||
|
raise SwiftError(
|
||||||
|
"The copy operation takes only strings or "
|
||||||
|
"SwiftCopyObjects as input",
|
||||||
|
obj=o)
|
||||||
|
|
||||||
|
return copy_objects
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _copy_object_job(conn, container, obj, destination, headers,
|
||||||
|
fresh_metadata):
|
||||||
|
response_dict = {}
|
||||||
|
res = {
|
||||||
|
'success': True,
|
||||||
|
'action': 'copy_object',
|
||||||
|
'container': container,
|
||||||
|
'object': obj,
|
||||||
|
'destination': destination,
|
||||||
|
'headers': headers,
|
||||||
|
'fresh_metadata': fresh_metadata,
|
||||||
|
'response_dict': response_dict
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
conn.copy_object(
|
||||||
|
container, obj, destination=destination, headers=headers,
|
||||||
|
fresh_metadata=fresh_metadata, response_dict=response_dict)
|
||||||
|
except Exception as err:
|
||||||
|
traceback, err_time = report_traceback()
|
||||||
|
logger.exception(err)
|
||||||
|
res.update({
|
||||||
|
'success': False,
|
||||||
|
'error': err,
|
||||||
|
'traceback': traceback,
|
||||||
|
'error_timestamp': err_time
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
# Capabilities related methods
|
# Capabilities related methods
|
||||||
#
|
#
|
||||||
def capabilities(self, url=None, refresh_cache=False):
|
def capabilities(self, url=None, refresh_cache=False):
|
||||||
|
@ -46,7 +46,7 @@ except ImportError:
|
|||||||
from pipes import quote as sh_quote
|
from pipes import quote as sh_quote
|
||||||
|
|
||||||
BASENAME = 'swift'
|
BASENAME = 'swift'
|
||||||
commands = ('delete', 'download', 'list', 'post', 'stat', 'upload',
|
commands = ('delete', 'download', 'list', 'post', 'copy', 'stat', 'upload',
|
||||||
'capabilities', 'info', 'tempurl', 'auth')
|
'capabilities', 'info', 'tempurl', 'auth')
|
||||||
|
|
||||||
|
|
||||||
@ -746,6 +746,105 @@ def st_post(parser, args, output_manager):
|
|||||||
output_manager.error(e.value)
|
output_manager.error(e.value)
|
||||||
|
|
||||||
|
|
||||||
|
st_copy_options = '''[--destination </container/object>] [--fresh-metadata]
|
||||||
|
[--meta <name:value>] [--header <header>] container object
|
||||||
|
'''
|
||||||
|
|
||||||
|
st_copy_help = '''
|
||||||
|
Copies object to new destination, optionally updates objects metadata.
|
||||||
|
If destination is not set, will update metadata of object
|
||||||
|
|
||||||
|
Positional arguments:
|
||||||
|
container Name of container to copy from.
|
||||||
|
object Name of object to copy. Specify multiple times
|
||||||
|
for multiple objects
|
||||||
|
|
||||||
|
Optional arguments:
|
||||||
|
-d, --destination </container[/object]>
|
||||||
|
The container and name of the destination object. Name
|
||||||
|
of destination object can be ommited, then will be
|
||||||
|
same as name of source object. Supplying multiple
|
||||||
|
objects and destination with object name is invalid.
|
||||||
|
-M, --fresh-metadata Copy the object without any existing metadata,
|
||||||
|
If not set, metadata will be preserved or appended
|
||||||
|
-m, --meta <name:value>
|
||||||
|
Sets a meta data item. This option may be repeated.
|
||||||
|
Example: -m Color:Blue -m Size:Large
|
||||||
|
-H, --header <header:value>
|
||||||
|
Adds a customized request header.
|
||||||
|
This option may be repeated. Example
|
||||||
|
-H "content-type:text/plain" -H "Content-Length: 4000"
|
||||||
|
'''.strip('\n')
|
||||||
|
|
||||||
|
|
||||||
|
def st_copy(parser, args, output_manager):
|
||||||
|
parser.add_argument(
|
||||||
|
'-d', '--destination', help='The container and name of the '
|
||||||
|
'destination object')
|
||||||
|
parser.add_argument(
|
||||||
|
'-M', '--fresh-metadata', action='store_true',
|
||||||
|
help='Copy the object without any existing metadata', default=False)
|
||||||
|
parser.add_argument(
|
||||||
|
'-m', '--meta', action='append', dest='meta', default=[],
|
||||||
|
help='Sets a meta data item. This option may be repeated. '
|
||||||
|
'Example: -m Color:Blue -m Size:Large')
|
||||||
|
parser.add_argument(
|
||||||
|
'-H', '--header', action='append', dest='header',
|
||||||
|
default=[], help='Adds a customized request header. '
|
||||||
|
'This option may be repeated. '
|
||||||
|
'Example: -H "content-type:text/plain" '
|
||||||
|
'-H "Content-Length: 4000"')
|
||||||
|
(options, args) = parse_args(parser, args)
|
||||||
|
args = args[1:]
|
||||||
|
|
||||||
|
with SwiftService(options=options) as swift:
|
||||||
|
try:
|
||||||
|
if len(args) >= 2:
|
||||||
|
container = args[0]
|
||||||
|
if '/' in container:
|
||||||
|
output_manager.error(
|
||||||
|
'WARNING: / in container name; you might have '
|
||||||
|
"meant '%s' instead of '%s'." %
|
||||||
|
(args[0].replace('/', ' ', 1), args[0]))
|
||||||
|
return
|
||||||
|
objects = [arg for arg in args[1:]]
|
||||||
|
|
||||||
|
for r in swift.copy(
|
||||||
|
container=container, objects=objects,
|
||||||
|
options=options):
|
||||||
|
if r['success']:
|
||||||
|
if options['verbose']:
|
||||||
|
if r['action'] == 'copy_object':
|
||||||
|
output_manager.print_msg(
|
||||||
|
'%s/%s copied to %s' % (
|
||||||
|
r['container'],
|
||||||
|
r['object'],
|
||||||
|
r['destination'] or '<self>'))
|
||||||
|
if r['action'] == 'create_container':
|
||||||
|
output_manager.print_msg(
|
||||||
|
'created container %s' % r['container']
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
error = r['error']
|
||||||
|
if 'action' in r and r['action'] == 'create_container':
|
||||||
|
# it is not an error to be unable to create the
|
||||||
|
# container so print a warning and carry on
|
||||||
|
output_manager.warning(
|
||||||
|
'Warning: failed to create container '
|
||||||
|
"'%s': %s", container, error
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
output_manager.error("%s" % error)
|
||||||
|
else:
|
||||||
|
output_manager.error(
|
||||||
|
'Usage: %s copy %s\n%s', BASENAME,
|
||||||
|
st_copy_options, st_copy_help)
|
||||||
|
return
|
||||||
|
|
||||||
|
except SwiftError as e:
|
||||||
|
output_manager.error(e.value)
|
||||||
|
|
||||||
|
|
||||||
st_upload_options = '''[--changed] [--skip-identical] [--segment-size <size>]
|
st_upload_options = '''[--changed] [--skip-identical] [--segment-size <size>]
|
||||||
[--segment-container <container>] [--leave-segments]
|
[--segment-container <container>] [--leave-segments]
|
||||||
[--object-threads <thread>] [--segment-threads <threads>]
|
[--object-threads <thread>] [--segment-threads <threads>]
|
||||||
@ -1268,6 +1367,7 @@ Positional arguments:
|
|||||||
for a container.
|
for a container.
|
||||||
post Updates meta information for the account, container,
|
post Updates meta information for the account, container,
|
||||||
or object; creates containers if not present.
|
or object; creates containers if not present.
|
||||||
|
copy Copies object, optionally adds meta
|
||||||
stat Displays information for the account, container,
|
stat Displays information for the account, container,
|
||||||
or object.
|
or object.
|
||||||
upload Uploads files or directories to the given container.
|
upload Uploads files or directories to the given container.
|
||||||
|
@ -405,6 +405,46 @@ class TestFunctional(unittest.TestCase):
|
|||||||
headers = self.conn.head_object(self.containername, self.objectname)
|
headers = self.conn.head_object(self.containername, self.objectname)
|
||||||
self.assertEqual('Something', headers.get('x-object-meta-color'))
|
self.assertEqual('Something', headers.get('x-object-meta-color'))
|
||||||
|
|
||||||
|
def test_copy_object(self):
|
||||||
|
self.conn.put_object(
|
||||||
|
self.containername, self.objectname, self.test_data)
|
||||||
|
self.conn.copy_object(self.containername,
|
||||||
|
self.objectname,
|
||||||
|
headers={'x-object-meta-color': 'Something'})
|
||||||
|
|
||||||
|
headers = self.conn.head_object(self.containername, self.objectname)
|
||||||
|
self.assertEqual('Something', headers.get('x-object-meta-color'))
|
||||||
|
|
||||||
|
self.conn.copy_object(self.containername,
|
||||||
|
self.objectname,
|
||||||
|
headers={'x-object-meta-taste': 'Second'})
|
||||||
|
|
||||||
|
headers = self.conn.head_object(self.containername, self.objectname)
|
||||||
|
self.assertEqual('Something', headers.get('x-object-meta-color'))
|
||||||
|
self.assertEqual('Second', headers.get('x-object-meta-taste'))
|
||||||
|
|
||||||
|
destination = "/%s/%s" % (self.containername, self.objectname_2)
|
||||||
|
self.conn.copy_object(self.containername,
|
||||||
|
self.objectname,
|
||||||
|
destination,
|
||||||
|
headers={'x-object-meta-taste': 'Second'})
|
||||||
|
headers, data = self.conn.get_object(self.containername,
|
||||||
|
self.objectname_2)
|
||||||
|
self.assertEqual(self.test_data, data)
|
||||||
|
self.assertEqual('Something', headers.get('x-object-meta-color'))
|
||||||
|
self.assertEqual('Second', headers.get('x-object-meta-taste'))
|
||||||
|
|
||||||
|
destination = "/%s/%s" % (self.containername, self.objectname_2)
|
||||||
|
self.conn.copy_object(self.containername,
|
||||||
|
self.objectname,
|
||||||
|
destination,
|
||||||
|
headers={'x-object-meta-color': 'Else'},
|
||||||
|
fresh_metadata=True)
|
||||||
|
|
||||||
|
headers = self.conn.head_object(self.containername, self.objectname_2)
|
||||||
|
self.assertEqual('Else', headers.get('x-object-meta-color'))
|
||||||
|
self.assertIsNone(headers.get('x-object-meta-taste'))
|
||||||
|
|
||||||
def test_get_capabilities(self):
|
def test_get_capabilities(self):
|
||||||
resp = self.conn.get_capabilities()
|
resp = self.conn.get_capabilities()
|
||||||
self.assertTrue(resp.get('swift'))
|
self.assertTrue(resp.get('swift'))
|
||||||
|
@ -69,6 +69,42 @@ class TestSwiftPostObject(unittest.TestCase):
|
|||||||
self.assertRaises(SwiftError, self.spo, 1)
|
self.assertRaises(SwiftError, self.spo, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSwiftCopyObject(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestSwiftCopyObject, self).setUp()
|
||||||
|
self.sco = swiftclient.service.SwiftCopyObject
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
sco = self.sco('obj_name')
|
||||||
|
|
||||||
|
self.assertEqual(sco.object_name, 'obj_name')
|
||||||
|
self.assertIsNone(sco.destination)
|
||||||
|
self.assertFalse(sco.fresh_metadata)
|
||||||
|
|
||||||
|
sco = self.sco('obj_name',
|
||||||
|
{'destination': '/dest', 'fresh_metadata': True})
|
||||||
|
|
||||||
|
self.assertEqual(sco.object_name, 'obj_name')
|
||||||
|
self.assertEqual(sco.destination, '/dest/obj_name')
|
||||||
|
self.assertTrue(sco.fresh_metadata)
|
||||||
|
|
||||||
|
sco = self.sco('obj_name',
|
||||||
|
{'destination': '/dest/new_obj/a',
|
||||||
|
'fresh_metadata': False})
|
||||||
|
|
||||||
|
self.assertEqual(sco.object_name, 'obj_name')
|
||||||
|
self.assertEqual(sco.destination, '/dest/new_obj/a')
|
||||||
|
self.assertFalse(sco.fresh_metadata)
|
||||||
|
|
||||||
|
def test_create_with_invalid_name(self):
|
||||||
|
# empty strings are not allowed as names
|
||||||
|
self.assertRaises(SwiftError, self.sco, '')
|
||||||
|
|
||||||
|
# names cannot be anything but strings
|
||||||
|
self.assertRaises(SwiftError, self.sco, 1)
|
||||||
|
|
||||||
|
|
||||||
class TestSwiftReader(unittest.TestCase):
|
class TestSwiftReader(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -2344,3 +2380,73 @@ class TestServicePost(_TestServiceBase):
|
|||||||
|
|
||||||
res_iter.assert_called_with(
|
res_iter.assert_called_with(
|
||||||
[tm_instance.object_uu_pool.submit()] * len(calls))
|
[tm_instance.object_uu_pool.submit()] * len(calls))
|
||||||
|
|
||||||
|
|
||||||
|
class TestServiceCopy(_TestServiceBase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestServiceCopy, self).setUp()
|
||||||
|
self.opts = swiftclient.service._default_local_options.copy()
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.MultiThreadingManager')
|
||||||
|
@mock.patch('swiftclient.service.interruptable_as_completed')
|
||||||
|
def test_object_copy(self, inter_compl, thread_manager):
|
||||||
|
"""
|
||||||
|
Check copy method translates strings and objects to _copy_object_job
|
||||||
|
calls correctly
|
||||||
|
"""
|
||||||
|
tm_instance = Mock()
|
||||||
|
thread_manager.return_value = tm_instance
|
||||||
|
|
||||||
|
self.opts.update({'meta': ["meta1:test1"], "header": ["hdr1:test1"]})
|
||||||
|
sco = swiftclient.service.SwiftCopyObject(
|
||||||
|
"test_sco",
|
||||||
|
options={'meta': ["meta1:test2"], "header": ["hdr1:test2"],
|
||||||
|
'destination': "/cont_new/test_sco"})
|
||||||
|
|
||||||
|
res = SwiftService().copy('test_c', ['test_o', sco], self.opts)
|
||||||
|
res = list(res)
|
||||||
|
|
||||||
|
calls = [
|
||||||
|
mock.call(
|
||||||
|
SwiftService._create_container_job, 'cont_new', headers={}),
|
||||||
|
]
|
||||||
|
tm_instance.container_pool.submit.assert_has_calls(calls,
|
||||||
|
any_order=True)
|
||||||
|
self.assertEqual(
|
||||||
|
tm_instance.container_pool.submit.call_count, len(calls))
|
||||||
|
|
||||||
|
calls = [
|
||||||
|
mock.call(
|
||||||
|
SwiftService._copy_object_job, 'test_c', 'test_o',
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"X-Object-Meta-Meta1": "test1",
|
||||||
|
"Hdr1": "test1"},
|
||||||
|
False),
|
||||||
|
mock.call(
|
||||||
|
SwiftService._copy_object_job, 'test_c', 'test_sco',
|
||||||
|
'/cont_new/test_sco',
|
||||||
|
{
|
||||||
|
"X-Object-Meta-Meta1": "test2",
|
||||||
|
"Hdr1": "test2"},
|
||||||
|
False),
|
||||||
|
]
|
||||||
|
tm_instance.object_uu_pool.submit.assert_has_calls(calls)
|
||||||
|
self.assertEqual(
|
||||||
|
tm_instance.object_uu_pool.submit.call_count, len(calls))
|
||||||
|
|
||||||
|
inter_compl.assert_called_with(
|
||||||
|
[tm_instance.object_uu_pool.submit()] * len(calls))
|
||||||
|
|
||||||
|
def test_object_copy_fail_dest(self):
|
||||||
|
"""
|
||||||
|
Destination in incorrect format and destination with object
|
||||||
|
used when multiple objects are copied raises SwiftError
|
||||||
|
"""
|
||||||
|
with self.assertRaises(SwiftError):
|
||||||
|
list(SwiftService().copy('test_c', ['test_o'],
|
||||||
|
{'destination': 'cont'}))
|
||||||
|
with self.assertRaises(SwiftError):
|
||||||
|
list(SwiftService().copy('test_c', ['test_o', 'test_o2'],
|
||||||
|
{'destination': '/cont/obj'}))
|
||||||
|
@ -1244,6 +1244,139 @@ class TestShell(unittest.TestCase):
|
|||||||
self.assertTrue(output.err != '')
|
self.assertTrue(output.err != '')
|
||||||
self.assertTrue(output.err.startswith('Usage'))
|
self.assertTrue(output.err.startswith('Usage'))
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_copy_object_no_destination(self, connection):
|
||||||
|
argv = ["", "copy", "container", "object",
|
||||||
|
"--meta", "Color:Blue",
|
||||||
|
"--header", "content-type:text/plain"
|
||||||
|
]
|
||||||
|
with CaptureOutput() as output:
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
connection.return_value.copy_object.assert_called_with(
|
||||||
|
'container', 'object', destination=None, fresh_metadata=False,
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
'X-Object-Meta-Color': 'Blue'}, response_dict={})
|
||||||
|
self.assertEqual(output.out, 'container/object copied to <self>\n')
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_copy_object(self, connection):
|
||||||
|
argv = ["", "copy", "container", "object",
|
||||||
|
"--meta", "Color:Blue",
|
||||||
|
"--header", "content-type:text/plain",
|
||||||
|
"--destination", "/c/o"
|
||||||
|
]
|
||||||
|
with CaptureOutput() as output:
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
connection.return_value.copy_object.assert_called_with(
|
||||||
|
'container', 'object', destination="/c/o",
|
||||||
|
fresh_metadata=False,
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
'X-Object-Meta-Color': 'Blue'}, response_dict={})
|
||||||
|
self.assertEqual(
|
||||||
|
output.out,
|
||||||
|
'created container c\ncontainer/object copied to /c/o\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_copy_object_fresh_metadata(self, connection):
|
||||||
|
argv = ["", "copy", "container", "object",
|
||||||
|
"--meta", "Color:Blue", "--fresh-metadata",
|
||||||
|
"--header", "content-type:text/plain",
|
||||||
|
"--destination", "/c/o"
|
||||||
|
]
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
connection.return_value.copy_object.assert_called_with(
|
||||||
|
'container', 'object', destination="/c/o", fresh_metadata=True,
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
'X-Object-Meta-Color': 'Blue'}, response_dict={})
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_copy_two_objects(self, connection):
|
||||||
|
argv = ["", "copy", "container", "object", "object2",
|
||||||
|
"--meta", "Color:Blue"]
|
||||||
|
connection.return_value.copy_object.return_value = None
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
calls = [
|
||||||
|
mock.call(
|
||||||
|
'container', 'object', destination=None,
|
||||||
|
fresh_metadata=False, headers={'X-Object-Meta-Color': 'Blue'},
|
||||||
|
response_dict={}),
|
||||||
|
mock.call(
|
||||||
|
'container', 'object2', destination=None,
|
||||||
|
fresh_metadata=False, headers={'X-Object-Meta-Color': 'Blue'},
|
||||||
|
response_dict={})
|
||||||
|
]
|
||||||
|
for call in calls:
|
||||||
|
self.assertIn(call, connection.return_value.copy_object.mock_calls)
|
||||||
|
self.assertEqual(len(connection.return_value.copy_object.mock_calls),
|
||||||
|
len(calls))
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_copy_two_objects_destination(self, connection):
|
||||||
|
argv = ["", "copy", "container", "object", "object2",
|
||||||
|
"--meta", "Color:Blue", "--destination", "/c"]
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
calls = [
|
||||||
|
mock.call(
|
||||||
|
'container', 'object', destination="/c/object",
|
||||||
|
fresh_metadata=False, headers={'X-Object-Meta-Color': 'Blue'},
|
||||||
|
response_dict={}),
|
||||||
|
mock.call(
|
||||||
|
'container', 'object2', destination="/c/object2",
|
||||||
|
fresh_metadata=False, headers={'X-Object-Meta-Color': 'Blue'},
|
||||||
|
response_dict={})
|
||||||
|
]
|
||||||
|
connection.return_value.copy_object.assert_has_calls(calls)
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_copy_two_objects_bad_destination(self, connection):
|
||||||
|
argv = ["", "copy", "container", "object", "object2",
|
||||||
|
"--meta", "Color:Blue", "--destination", "/c/o"]
|
||||||
|
|
||||||
|
with CaptureOutput() as output:
|
||||||
|
with self.assertRaises(SystemExit):
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
output.err,
|
||||||
|
'Combination of multiple objects and destination '
|
||||||
|
'including object is invalid\n')
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_copy_object_bad_auth(self, connection):
|
||||||
|
argv = ["", "copy", "container", "object"]
|
||||||
|
connection.return_value.copy_object.side_effect = \
|
||||||
|
swiftclient.ClientException("bad auth")
|
||||||
|
|
||||||
|
with CaptureOutput() as output:
|
||||||
|
with self.assertRaises(SystemExit):
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
|
||||||
|
self.assertEqual(output.err, 'bad auth\n')
|
||||||
|
|
||||||
|
def test_copy_object_not_enough_args(self):
|
||||||
|
argv = ["", "copy", "container"]
|
||||||
|
|
||||||
|
with CaptureOutput() as output:
|
||||||
|
with self.assertRaises(SystemExit):
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
|
||||||
|
self.assertTrue(output.err != '')
|
||||||
|
self.assertTrue(output.err.startswith('Usage'))
|
||||||
|
|
||||||
|
def test_copy_bad_container(self):
|
||||||
|
argv = ["", "copy", "cont/ainer", "object"]
|
||||||
|
|
||||||
|
with CaptureOutput() as output:
|
||||||
|
with self.assertRaises(SystemExit):
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
|
||||||
|
self.assertTrue(output.err != '')
|
||||||
|
self.assertTrue(output.err.startswith('WARN'))
|
||||||
|
|
||||||
@mock.patch('swiftclient.shell.generate_temp_url', return_value='')
|
@mock.patch('swiftclient.shell.generate_temp_url', return_value='')
|
||||||
def test_temp_url(self, temp_url):
|
def test_temp_url(self, temp_url):
|
||||||
argv = ["", "tempurl", "GET", "60", "/v1/AUTH_account/c/o",
|
argv = ["", "tempurl", "GET", "60", "/v1/AUTH_account/c/o",
|
||||||
|
@ -1398,6 +1398,109 @@ class TestPostObject(MockHttpTest):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class TestCopyObject(MockHttpTest):
|
||||||
|
|
||||||
|
def test_server_error(self):
|
||||||
|
c.http_connection = self.fake_http_connection(500)
|
||||||
|
self.assertRaises(
|
||||||
|
c.ClientException, c.copy_object,
|
||||||
|
'http://www.test.com/v1/AUTH', 'asdf', 'asdf', 'asdf')
|
||||||
|
|
||||||
|
def test_ok(self):
|
||||||
|
c.http_connection = self.fake_http_connection(200)
|
||||||
|
c.copy_object(
|
||||||
|
'http://www.test.com/v1/AUTH', 'token', 'container', 'obj',
|
||||||
|
destination='/container2/obj')
|
||||||
|
self.assertRequests([
|
||||||
|
('COPY', 'http://www.test.com/v1/AUTH/container/obj', '', {
|
||||||
|
'X-Auth-Token': 'token',
|
||||||
|
'Destination': '/container2/obj',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_service_token(self):
|
||||||
|
c.http_connection = self.fake_http_connection(200)
|
||||||
|
c.copy_object('http://www.test.com/v1/AUTH', None, 'container',
|
||||||
|
'obj', destination='/container2/obj',
|
||||||
|
service_token="TOKEN")
|
||||||
|
self.assertRequests([
|
||||||
|
('COPY', 'http://www.test.com/v1/AUTH/container/obj', '', {
|
||||||
|
'X-Service-Token': 'TOKEN',
|
||||||
|
'Destination': '/container2/obj',
|
||||||
|
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_headers(self):
|
||||||
|
c.http_connection = self.fake_http_connection(200)
|
||||||
|
c.copy_object(
|
||||||
|
'http://www.test.com/v1/AUTH', 'token', 'container', 'obj',
|
||||||
|
destination='/container2/obj',
|
||||||
|
headers={'some-hdr': 'a', 'other-hdr': 'b'})
|
||||||
|
self.assertRequests([
|
||||||
|
('COPY', 'http://www.test.com/v1/AUTH/container/obj', '', {
|
||||||
|
'X-Auth-Token': 'token',
|
||||||
|
'Destination': '/container2/obj',
|
||||||
|
'some-hdr': 'a',
|
||||||
|
'other-hdr': 'b',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_fresh_metadata_default(self):
|
||||||
|
c.http_connection = self.fake_http_connection(200)
|
||||||
|
c.copy_object(
|
||||||
|
'http://www.test.com/v1/AUTH', 'token', 'container', 'obj',
|
||||||
|
'/container2/obj', {'x-fresh-metadata': 'hdr-value'})
|
||||||
|
self.assertRequests([
|
||||||
|
('COPY', 'http://www.test.com/v1/AUTH/container/obj', '', {
|
||||||
|
'X-Auth-Token': 'token',
|
||||||
|
'Destination': '/container2/obj',
|
||||||
|
'X-Fresh-Metadata': 'hdr-value',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_fresh_metadata_true(self):
|
||||||
|
c.http_connection = self.fake_http_connection(200)
|
||||||
|
c.copy_object(
|
||||||
|
'http://www.test.com/v1/AUTH', 'token', 'container', 'obj',
|
||||||
|
destination='/container2/obj',
|
||||||
|
headers={'x-fresh-metadata': 'hdr-value'},
|
||||||
|
fresh_metadata=True)
|
||||||
|
self.assertRequests([
|
||||||
|
('COPY', 'http://www.test.com/v1/AUTH/container/obj', '', {
|
||||||
|
'X-Auth-Token': 'token',
|
||||||
|
'Destination': '/container2/obj',
|
||||||
|
'X-Fresh-Metadata': 'true',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_fresh_metadata_false(self):
|
||||||
|
c.http_connection = self.fake_http_connection(200)
|
||||||
|
c.copy_object(
|
||||||
|
'http://www.test.com/v1/AUTH', 'token', 'container', 'obj',
|
||||||
|
destination='/container2/obj',
|
||||||
|
headers={'x-fresh-metadata': 'hdr-value'},
|
||||||
|
fresh_metadata=False)
|
||||||
|
self.assertRequests([
|
||||||
|
('COPY', 'http://www.test.com/v1/AUTH/container/obj', '', {
|
||||||
|
'x-auth-token': 'token',
|
||||||
|
'Destination': '/container2/obj',
|
||||||
|
'X-Fresh-Metadata': 'false',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_no_destination(self):
|
||||||
|
c.http_connection = self.fake_http_connection(200)
|
||||||
|
c.copy_object(
|
||||||
|
'http://www.test.com/v1/AUTH', 'token', 'container', 'obj')
|
||||||
|
self.assertRequests([
|
||||||
|
('COPY', 'http://www.test.com/v1/AUTH/container/obj', '', {
|
||||||
|
'x-auth-token': 'token',
|
||||||
|
'Destination': '/container/obj',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
class TestDeleteObject(MockHttpTest):
|
class TestDeleteObject(MockHttpTest):
|
||||||
|
|
||||||
def test_ok(self):
|
def test_ok(self):
|
||||||
@ -2246,6 +2349,7 @@ class TestResponseDict(MockHttpTest):
|
|||||||
('delete_container', 'c'),
|
('delete_container', 'c'),
|
||||||
('post_object', 'c', 'o', {}),
|
('post_object', 'c', 'o', {}),
|
||||||
('put_object', 'c', 'o', 'body'),
|
('put_object', 'c', 'o', 'body'),
|
||||||
|
('copy_object', 'c', 'o'),
|
||||||
('delete_object', 'c', 'o')]
|
('delete_object', 'c', 'o')]
|
||||||
|
|
||||||
def fake_get_auth(*args, **kwargs):
|
def fake_get_auth(*args, **kwargs):
|
||||||
|
Loading…
Reference in New Issue
Block a user