object versioning features
* add --versions to list * add --versions to delete * add --version-id to stat * add --version-id to delete * add --version-id to download Change-Id: I89802064921778fee7efe57c7d60c976cdde3a27
This commit is contained in:
parent
02e8f4f228
commit
78edffa46c
@ -921,7 +921,7 @@ def post_account(url, token, headers, http_conn=None, response_dict=None,
|
|||||||
|
|
||||||
def get_container(url, token, container, marker=None, limit=None,
|
def get_container(url, token, container, marker=None, limit=None,
|
||||||
prefix=None, delimiter=None, end_marker=None,
|
prefix=None, delimiter=None, end_marker=None,
|
||||||
path=None, http_conn=None,
|
version_marker=None, path=None, http_conn=None,
|
||||||
full_listing=False, service_token=None, headers=None,
|
full_listing=False, service_token=None, headers=None,
|
||||||
query_string=None):
|
query_string=None):
|
||||||
"""
|
"""
|
||||||
@ -935,6 +935,7 @@ def get_container(url, token, container, marker=None, limit=None,
|
|||||||
:param prefix: prefix query
|
:param prefix: prefix query
|
||||||
:param delimiter: string to delimit the queries on
|
:param delimiter: string to delimit the queries on
|
||||||
:param end_marker: marker query
|
:param end_marker: marker query
|
||||||
|
:param version_marker: version marker query
|
||||||
:param path: path query (equivalent: "delimiter=/" and "prefix=path/")
|
:param path: path query (equivalent: "delimiter=/" and "prefix=path/")
|
||||||
:param http_conn: a tuple of (parsed url, HTTPConnection object),
|
:param http_conn: a tuple of (parsed url, HTTPConnection object),
|
||||||
(If None, it will create the conn object)
|
(If None, it will create the conn object)
|
||||||
@ -951,17 +952,20 @@ def get_container(url, token, container, marker=None, limit=None,
|
|||||||
http_conn = http_connection(url)
|
http_conn = http_connection(url)
|
||||||
if full_listing:
|
if full_listing:
|
||||||
rv = get_container(url, token, container, marker, limit, prefix,
|
rv = get_container(url, token, container, marker, limit, prefix,
|
||||||
delimiter, end_marker, path, http_conn,
|
delimiter, end_marker, version_marker, path=path,
|
||||||
service_token=service_token, headers=headers)
|
http_conn=http_conn, service_token=service_token,
|
||||||
|
headers=headers)
|
||||||
listing = rv[1]
|
listing = rv[1]
|
||||||
while listing:
|
while listing:
|
||||||
if not delimiter:
|
if not delimiter:
|
||||||
marker = listing[-1]['name']
|
marker = listing[-1]['name']
|
||||||
else:
|
else:
|
||||||
marker = listing[-1].get('name', listing[-1].get('subdir'))
|
marker = listing[-1].get('name', listing[-1].get('subdir'))
|
||||||
|
version_marker = listing[-1].get('version_id')
|
||||||
listing = get_container(url, token, container, marker, limit,
|
listing = get_container(url, token, container, marker, limit,
|
||||||
prefix, delimiter, end_marker, path,
|
prefix, delimiter, end_marker,
|
||||||
http_conn, service_token=service_token,
|
version_marker, path, http_conn,
|
||||||
|
service_token=service_token,
|
||||||
headers=headers)[1]
|
headers=headers)[1]
|
||||||
if listing:
|
if listing:
|
||||||
rv[1].extend(listing)
|
rv[1].extend(listing)
|
||||||
@ -979,6 +983,8 @@ def get_container(url, token, container, marker=None, limit=None,
|
|||||||
qs += '&delimiter=%s' % quote(delimiter)
|
qs += '&delimiter=%s' % quote(delimiter)
|
||||||
if end_marker:
|
if end_marker:
|
||||||
qs += '&end_marker=%s' % quote(end_marker)
|
qs += '&end_marker=%s' % quote(end_marker)
|
||||||
|
if version_marker:
|
||||||
|
qs += '&version_marker=%s' % quote(version_marker)
|
||||||
if path:
|
if path:
|
||||||
qs += '&path=%s' % quote(path)
|
qs += '&path=%s' % quote(path)
|
||||||
if query_string:
|
if query_string:
|
||||||
@ -1816,15 +1822,17 @@ class Connection(object):
|
|||||||
return self._retry(None, head_container, container, headers=headers)
|
return self._retry(None, head_container, container, headers=headers)
|
||||||
|
|
||||||
def get_container(self, container, marker=None, limit=None, prefix=None,
|
def get_container(self, container, marker=None, limit=None, prefix=None,
|
||||||
delimiter=None, end_marker=None, path=None,
|
delimiter=None, end_marker=None, version_marker=None,
|
||||||
full_listing=False, headers=None, query_string=None):
|
path=None, full_listing=False, headers=None,
|
||||||
|
query_string=None):
|
||||||
"""Wrapper for :func:`get_container`"""
|
"""Wrapper for :func:`get_container`"""
|
||||||
# TODO(unknown): With full_listing=True this will restart the entire
|
# TODO(unknown): With full_listing=True this will restart the entire
|
||||||
# listing with each retry. Need to make a better version that just
|
# listing with each retry. Need to make a better version that just
|
||||||
# retries where it left off.
|
# retries where it left off.
|
||||||
return self._retry(None, get_container, container, marker=marker,
|
return self._retry(None, get_container, container, marker=marker,
|
||||||
limit=limit, prefix=prefix, delimiter=delimiter,
|
limit=limit, prefix=prefix, delimiter=delimiter,
|
||||||
end_marker=end_marker, path=path,
|
end_marker=end_marker,
|
||||||
|
version_marker=version_marker, path=path,
|
||||||
full_listing=full_listing, headers=headers,
|
full_listing=full_listing, headers=headers,
|
||||||
query_string=query_string)
|
query_string=query_string)
|
||||||
|
|
||||||
|
@ -143,7 +143,11 @@ def print_container_stats(items, headers, output_manager):
|
|||||||
def stat_object(conn, options, container, obj):
|
def stat_object(conn, options, container, obj):
|
||||||
req_headers = split_request_headers(options.get('header', []))
|
req_headers = split_request_headers(options.get('header', []))
|
||||||
|
|
||||||
headers = conn.head_object(container, obj, headers=req_headers)
|
query_string = None
|
||||||
|
if options.get('version_id') is not None:
|
||||||
|
query_string = 'version-id=%s' % options['version_id']
|
||||||
|
headers = conn.head_object(container, obj, headers=req_headers,
|
||||||
|
query_string=query_string)
|
||||||
items = []
|
items = []
|
||||||
if options['verbose'] > 1:
|
if options['verbose'] > 1:
|
||||||
path = '%s/%s/%s' % (conn.url, container, obj)
|
path = '%s/%s/%s' % (conn.url, container, obj)
|
||||||
|
@ -86,6 +86,9 @@ class SwiftError(Exception):
|
|||||||
value += " segment:%s" % self.segment
|
value += " segment:%s" % self.segment
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
|
||||||
def process_options(options):
|
def process_options(options):
|
||||||
# tolerate sloppy auth_version
|
# tolerate sloppy auth_version
|
||||||
@ -186,6 +189,7 @@ _default_local_options = {
|
|||||||
'leave_segments': False,
|
'leave_segments': False,
|
||||||
'changed': None,
|
'changed': None,
|
||||||
'skip_identical': False,
|
'skip_identical': False,
|
||||||
|
'version_id': None,
|
||||||
'yes_all': False,
|
'yes_all': False,
|
||||||
'read_acl': None,
|
'read_acl': None,
|
||||||
'write_acl': None,
|
'write_acl': None,
|
||||||
@ -200,6 +204,7 @@ _default_local_options = {
|
|||||||
'meta': [],
|
'meta': [],
|
||||||
'prefix': None,
|
'prefix': None,
|
||||||
'delimiter': None,
|
'delimiter': None,
|
||||||
|
'versions': False,
|
||||||
'fail_fast': False,
|
'fail_fast': False,
|
||||||
'human': False,
|
'human': False,
|
||||||
'dir_marker': False,
|
'dir_marker': False,
|
||||||
@ -336,6 +341,20 @@ class SwiftPostObject(object):
|
|||||||
self.options = options
|
self.options = options
|
||||||
|
|
||||||
|
|
||||||
|
class SwiftDeleteObject(object):
|
||||||
|
"""
|
||||||
|
Class for specifying an object delete, allowing the headers/metadata to be
|
||||||
|
specified separately for each individual object.
|
||||||
|
"""
|
||||||
|
def __init__(self, object_name, options=None):
|
||||||
|
if not (isinstance(object_name, string_types) and object_name):
|
||||||
|
raise SwiftError(
|
||||||
|
"Object names must be specified as non-empty strings"
|
||||||
|
)
|
||||||
|
self.object_name = object_name
|
||||||
|
self.options = options
|
||||||
|
|
||||||
|
|
||||||
class SwiftCopyObject(object):
|
class SwiftCopyObject(object):
|
||||||
"""
|
"""
|
||||||
Class for specifying an object copy,
|
Class for specifying an object copy,
|
||||||
@ -489,6 +508,7 @@ class SwiftService(object):
|
|||||||
|
|
||||||
{
|
{
|
||||||
'human': False,
|
'human': False,
|
||||||
|
'version_id': None,
|
||||||
'header': []
|
'header': []
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -871,6 +891,7 @@ class SwiftService(object):
|
|||||||
'long': False,
|
'long': False,
|
||||||
'prefix': None,
|
'prefix': None,
|
||||||
'delimiter': None,
|
'delimiter': None,
|
||||||
|
'versions': False,
|
||||||
'header': []
|
'header': []
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -967,13 +988,19 @@ class SwiftService(object):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _list_container_job(conn, container, options, result_queue):
|
def _list_container_job(conn, container, options, result_queue):
|
||||||
marker = options.get('marker', '')
|
marker = options.get('marker', '')
|
||||||
|
version_marker = options.get('version_marker', '')
|
||||||
error = None
|
error = None
|
||||||
req_headers = split_headers(options.get('header', []))
|
req_headers = split_headers(options.get('header', []))
|
||||||
|
if options.get('versions', False):
|
||||||
|
query_string = 'versions=true'
|
||||||
|
else:
|
||||||
|
query_string = None
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
_, items = conn.get_container(
|
_, items = conn.get_container(
|
||||||
container, marker=marker, prefix=options['prefix'],
|
container, marker=marker, version_marker=version_marker,
|
||||||
delimiter=options['delimiter'], headers=req_headers
|
prefix=options['prefix'], delimiter=options['delimiter'],
|
||||||
|
headers=req_headers, query_string=query_string
|
||||||
)
|
)
|
||||||
|
|
||||||
if not items:
|
if not items:
|
||||||
@ -991,6 +1018,7 @@ class SwiftService(object):
|
|||||||
result_queue.put(res)
|
result_queue.put(res)
|
||||||
|
|
||||||
marker = items[-1].get('name', items[-1].get('subdir'))
|
marker = items[-1].get('name', items[-1].get('subdir'))
|
||||||
|
version_marker = items[-1].get('version_id', '')
|
||||||
except ClientException as err:
|
except ClientException as err:
|
||||||
traceback, err_time = report_traceback()
|
traceback, err_time = report_traceback()
|
||||||
logger.exception(err)
|
logger.exception(err)
|
||||||
@ -1016,6 +1044,7 @@ class SwiftService(object):
|
|||||||
'prefix': options['prefix'],
|
'prefix': options['prefix'],
|
||||||
'success': False,
|
'success': False,
|
||||||
'marker': marker,
|
'marker': marker,
|
||||||
|
'version_marker': version_marker,
|
||||||
'error': error[0],
|
'error': error[0],
|
||||||
'traceback': error[1],
|
'traceback': error[1],
|
||||||
'error_timestamp': error[2]
|
'error_timestamp': error[2]
|
||||||
@ -1042,6 +1071,7 @@ class SwiftService(object):
|
|||||||
'no_download': False,
|
'no_download': False,
|
||||||
'header': [],
|
'header': [],
|
||||||
'skip_identical': False,
|
'skip_identical': False,
|
||||||
|
'version_id': None,
|
||||||
'out_directory': None,
|
'out_directory': None,
|
||||||
'checksum': True,
|
'checksum': True,
|
||||||
'out_file': None,
|
'out_file': None,
|
||||||
@ -1151,6 +1181,9 @@ class SwiftService(object):
|
|||||||
get_args = {'resp_chunk_size': DISK_BUFFER,
|
get_args = {'resp_chunk_size': DISK_BUFFER,
|
||||||
'headers': req_headers,
|
'headers': req_headers,
|
||||||
'response_dict': results_dict}
|
'response_dict': results_dict}
|
||||||
|
if options.get('version_id') is not None:
|
||||||
|
get_args['query_string'] = (
|
||||||
|
'version-id=%s' % options['version_id'])
|
||||||
if options['skip_identical']:
|
if options['skip_identical']:
|
||||||
# Assume the file is a large object; if we're wrong, the query
|
# Assume the file is a large object; if we're wrong, the query
|
||||||
# string is ignored and the If-None-Match header will trigger
|
# string is ignored and the If-None-Match header will trigger
|
||||||
@ -2337,14 +2370,28 @@ class SwiftService(object):
|
|||||||
of objects.
|
of objects.
|
||||||
|
|
||||||
:param container: The container to delete or delete from.
|
:param container: The container to delete or delete from.
|
||||||
:param objects: The list of objects to delete.
|
:param objects: A list of object names (strings) or SwiftDeleteObject
|
||||||
|
instances containing an object name, and an
|
||||||
|
options dict (can be None) to override the options for
|
||||||
|
that individual delete operation::
|
||||||
|
|
||||||
|
[
|
||||||
|
'object_name',
|
||||||
|
SwiftDeleteObject('object_name',
|
||||||
|
options={...}),
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
The options dict is described below.
|
||||||
:param options: A dictionary containing options to override the global
|
:param options: A dictionary containing options to override the global
|
||||||
options specified during the service object creation::
|
options specified during the service object creation::
|
||||||
|
|
||||||
{
|
{
|
||||||
'yes_all': False,
|
'yes_all': False,
|
||||||
'leave_segments': False,
|
'leave_segments': False,
|
||||||
|
'version_id': None,
|
||||||
'prefix': None,
|
'prefix': None,
|
||||||
|
'versions': False,
|
||||||
'header': [],
|
'header': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2364,23 +2411,28 @@ class SwiftService(object):
|
|||||||
|
|
||||||
if container is not None:
|
if container is not None:
|
||||||
if objects is not None:
|
if objects is not None:
|
||||||
|
delete_objects = self._make_delete_objects(objects)
|
||||||
if options['prefix']:
|
if options['prefix']:
|
||||||
objects = [obj for obj in objects
|
delete_objects = [
|
||||||
if obj.startswith(options['prefix'])]
|
obj for obj in delete_objects
|
||||||
|
if obj.object_name.startswith(options['prefix'])]
|
||||||
rq = Queue()
|
rq = Queue()
|
||||||
obj_dels = {}
|
obj_dels = {}
|
||||||
|
|
||||||
bulk_page_size = self._bulk_delete_page_size(objects)
|
bulk_page_size = self._bulk_delete_page_size(delete_objects)
|
||||||
if bulk_page_size > 1:
|
if bulk_page_size > 1:
|
||||||
page_at_a_time = n_at_a_time(objects, bulk_page_size)
|
page_at_a_time = n_at_a_time(delete_objects,
|
||||||
|
bulk_page_size)
|
||||||
for page_slice in page_at_a_time:
|
for page_slice in page_at_a_time:
|
||||||
for obj_slice in n_groups(
|
for obj_slice in n_groups(
|
||||||
page_slice,
|
page_slice,
|
||||||
self._options['object_dd_threads']):
|
self._options['object_dd_threads']):
|
||||||
self._bulk_delete(container, obj_slice, options,
|
object_names = [
|
||||||
|
obj.object_name for obj in obj_slice]
|
||||||
|
self._bulk_delete(container, object_names, options,
|
||||||
obj_dels)
|
obj_dels)
|
||||||
else:
|
else:
|
||||||
self._per_item_delete(container, objects, options,
|
self._per_item_delete(container, delete_objects, options,
|
||||||
obj_dels, rq)
|
obj_dels, rq)
|
||||||
|
|
||||||
# Start a thread to watch for delete results
|
# Start a thread to watch for delete results
|
||||||
@ -2445,6 +2497,11 @@ class SwiftService(object):
|
|||||||
# Not many objects; may as well delete one-by-one
|
# Not many objects; may as well delete one-by-one
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
if any(obj.options for obj in objects
|
||||||
|
if isinstance(obj, SwiftDeleteObject)):
|
||||||
|
# we can't do per option deletes for bulk
|
||||||
|
return 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cap_result = self.capabilities()
|
cap_result = self.capabilities()
|
||||||
if not cap_result['success']:
|
if not cap_result['success']:
|
||||||
@ -2463,9 +2520,11 @@ class SwiftService(object):
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
def _per_item_delete(self, container, objects, options, rdict, rq):
|
def _per_item_delete(self, container, objects, options, rdict, rq):
|
||||||
for obj in objects:
|
for delete_obj in objects:
|
||||||
|
obj = delete_obj.object_name
|
||||||
|
obj_options = dict(options, **delete_obj.options or {})
|
||||||
obj_del = self.thread_manager.object_dd_pool.submit(
|
obj_del = self.thread_manager.object_dd_pool.submit(
|
||||||
self._delete_object, container, obj, options,
|
self._delete_object, container, obj, obj_options,
|
||||||
results_queue=rq
|
results_queue=rq
|
||||||
)
|
)
|
||||||
obj_details = {'container': container, 'object': obj}
|
obj_details = {'container': container, 'object': obj}
|
||||||
@ -2500,6 +2559,24 @@ class SwiftService(object):
|
|||||||
results_queue.put(res)
|
results_queue.put(res)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _make_delete_objects(objects):
|
||||||
|
delete_objects = []
|
||||||
|
|
||||||
|
for o in objects:
|
||||||
|
if isinstance(o, string_types):
|
||||||
|
obj = SwiftDeleteObject(o)
|
||||||
|
delete_objects.append(obj)
|
||||||
|
elif isinstance(o, SwiftDeleteObject):
|
||||||
|
delete_objects.append(o)
|
||||||
|
else:
|
||||||
|
raise SwiftError(
|
||||||
|
"The delete operation takes only strings or "
|
||||||
|
"SwiftDeleteObjects as input",
|
||||||
|
obj=o)
|
||||||
|
|
||||||
|
return delete_objects
|
||||||
|
|
||||||
def _delete_object(self, conn, container, obj, options,
|
def _delete_object(self, conn, container, obj, options,
|
||||||
results_queue=None):
|
results_queue=None):
|
||||||
_headers = {}
|
_headers = {}
|
||||||
@ -2511,7 +2588,7 @@ class SwiftService(object):
|
|||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
old_manifest = None
|
old_manifest = None
|
||||||
query_string = None
|
query_params = {}
|
||||||
|
|
||||||
if not options['leave_segments']:
|
if not options['leave_segments']:
|
||||||
try:
|
try:
|
||||||
@ -2520,11 +2597,15 @@ class SwiftService(object):
|
|||||||
query_string='symlink=get')
|
query_string='symlink=get')
|
||||||
old_manifest = headers.get('x-object-manifest')
|
old_manifest = headers.get('x-object-manifest')
|
||||||
if config_true_value(headers.get('x-static-large-object')):
|
if config_true_value(headers.get('x-static-large-object')):
|
||||||
query_string = 'multipart-manifest=delete'
|
query_params['multipart-manifest'] = 'delete'
|
||||||
except ClientException as err:
|
except ClientException as err:
|
||||||
if err.http_status != 404:
|
if err.http_status != 404:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
if options.get('version_id') is not None:
|
||||||
|
query_params['version-id'] = options['version_id']
|
||||||
|
query_string = '&'.join('%s=%s' % (k, v) for (k, v)
|
||||||
|
in sorted(query_params.items()))
|
||||||
results_dict = {}
|
results_dict = {}
|
||||||
conn.delete_object(container, obj,
|
conn.delete_object(container, obj,
|
||||||
headers=_headers,
|
headers=_headers,
|
||||||
@ -2611,12 +2692,17 @@ class SwiftService(object):
|
|||||||
try:
|
try:
|
||||||
for part in self.list(container=container, options=options):
|
for part in self.list(container=container, options=options):
|
||||||
if not part["success"]:
|
if not part["success"]:
|
||||||
|
|
||||||
raise part["error"]
|
raise part["error"]
|
||||||
|
delete_objects = []
|
||||||
|
for item in part['listing']:
|
||||||
|
delete_opts = {}
|
||||||
|
if options.get('versions', False) and 'version_id' in item:
|
||||||
|
delete_opts['version_id'] = item['version_id']
|
||||||
|
delete_obj = SwiftDeleteObject(item['name'], delete_opts)
|
||||||
|
delete_objects.append(delete_obj)
|
||||||
for res in self.delete(
|
for res in self.delete(
|
||||||
container=container,
|
container=container,
|
||||||
objects=[o['name'] for o in part['listing']],
|
objects=delete_objects,
|
||||||
options=options):
|
options=options):
|
||||||
yield res
|
yield res
|
||||||
if options['prefix']:
|
if options['prefix']:
|
||||||
@ -2679,7 +2765,9 @@ class SwiftService(object):
|
|||||||
'No content received on account POST. '
|
'No content received on account POST. '
|
||||||
'Is the bulk operations middleware enabled?')})
|
'Is the bulk operations middleware enabled?')})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
res.update({'success': False, 'error': e})
|
traceback, err_time = report_traceback()
|
||||||
|
logger.exception(e)
|
||||||
|
res.update({'success': False, 'error': e, 'traceback': traceback})
|
||||||
|
|
||||||
res.update({
|
res.update({
|
||||||
'action': 'bulk_delete',
|
'action': 'bulk_delete',
|
||||||
|
@ -65,7 +65,8 @@ st_delete_options = '''[--all] [--leave-segments]
|
|||||||
[--container-threads <threads>]
|
[--container-threads <threads>]
|
||||||
[--header <header:value>]
|
[--header <header:value>]
|
||||||
[--prefix <prefix>]
|
[--prefix <prefix>]
|
||||||
[<container> [<object>] [...]]
|
[--versions]
|
||||||
|
[<container> [<object>] [--version-id <version_id>] [...]]
|
||||||
'''
|
'''
|
||||||
|
|
||||||
st_delete_help = '''
|
st_delete_help = '''
|
||||||
@ -78,6 +79,7 @@ Positional arguments:
|
|||||||
|
|
||||||
Optional arguments:
|
Optional arguments:
|
||||||
-a, --all Delete all containers and objects.
|
-a, --all Delete all containers and objects.
|
||||||
|
--versions Delete all versions
|
||||||
--leave-segments Do not delete segments of manifest objects.
|
--leave-segments Do not delete segments of manifest objects.
|
||||||
-H, --header <header:value>
|
-H, --header <header:value>
|
||||||
Adds a custom request header to use for deleting
|
Adds a custom request header to use for deleting
|
||||||
@ -89,6 +91,8 @@ Optional arguments:
|
|||||||
Number of threads to use for deleting containers.
|
Number of threads to use for deleting containers.
|
||||||
Default is 10.
|
Default is 10.
|
||||||
--prefix <prefix> Only delete objects beginning with <prefix>.
|
--prefix <prefix> Only delete objects beginning with <prefix>.
|
||||||
|
--version-id <version-id>
|
||||||
|
Delete specific version of a versioned object.
|
||||||
'''.strip("\n")
|
'''.strip("\n")
|
||||||
|
|
||||||
|
|
||||||
@ -96,9 +100,14 @@ def st_delete(parser, args, output_manager, return_parser=False):
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-a', '--all', action='store_true', dest='yes_all',
|
'-a', '--all', action='store_true', dest='yes_all',
|
||||||
default=False, help='Delete all containers and objects.')
|
default=False, help='Delete all containers and objects.')
|
||||||
|
parser.add_argument('--versions', action='store_true',
|
||||||
|
help='delete all versions')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-p', '--prefix', dest='prefix',
|
'-p', '--prefix', dest='prefix',
|
||||||
help='Only delete items beginning with <prefix>.')
|
help='Only delete items beginning with <prefix>.')
|
||||||
|
parser.add_argument(
|
||||||
|
'--version-id', action='store', default=None,
|
||||||
|
help='Delete a specific version of a versioned object')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-H', '--header', action='append', dest='header',
|
'-H', '--header', action='append', dest='header',
|
||||||
default=[],
|
default=[],
|
||||||
@ -128,6 +137,10 @@ def st_delete(parser, args, output_manager, return_parser=False):
|
|||||||
BASENAME, st_delete_options,
|
BASENAME, st_delete_options,
|
||||||
st_delete_help)
|
st_delete_help)
|
||||||
return
|
return
|
||||||
|
if options['versions'] and len(args) >= 2:
|
||||||
|
exit('--versions option not allowed for object deletes')
|
||||||
|
if options['version_id'] and len(args) < 2:
|
||||||
|
exit('--version-id option only allowed for object deletes')
|
||||||
|
|
||||||
if options['object_threads'] <= 0:
|
if options['object_threads'] <= 0:
|
||||||
output_manager.error(
|
output_manager.error(
|
||||||
@ -227,6 +240,7 @@ st_download_options = '''[--all] [--marker <marker>] [--prefix <prefix>]
|
|||||||
[--object-threads <threads>] [--ignore-checksum]
|
[--object-threads <threads>] [--ignore-checksum]
|
||||||
[--container-threads <threads>] [--no-download]
|
[--container-threads <threads>] [--no-download]
|
||||||
[--skip-identical] [--remove-prefix]
|
[--skip-identical] [--remove-prefix]
|
||||||
|
[--version-id <version_id>]
|
||||||
[--header <header:value>] [--no-shuffle]
|
[--header <header:value>] [--no-shuffle]
|
||||||
[<container> [<object>] [...]]
|
[<container> [<object>] [...]]
|
||||||
'''
|
'''
|
||||||
@ -271,6 +285,8 @@ Optional arguments:
|
|||||||
Example: --header "content-type:text/plain"
|
Example: --header "content-type:text/plain"
|
||||||
--skip-identical Skip downloading files that are identical on both
|
--skip-identical Skip downloading files that are identical on both
|
||||||
sides.
|
sides.
|
||||||
|
--version-id <version-id>
|
||||||
|
Download specific version of a versioned object.
|
||||||
--ignore-checksum Turn off checksum validation for downloads.
|
--ignore-checksum Turn off checksum validation for downloads.
|
||||||
--no-shuffle By default, when downloading a complete account or
|
--no-shuffle By default, when downloading a complete account or
|
||||||
container, download order is randomised in order to
|
container, download order is randomised in order to
|
||||||
@ -332,6 +348,9 @@ def st_download(parser, args, output_manager, return_parser=False):
|
|||||||
'--skip-identical', action='store_true', dest='skip_identical',
|
'--skip-identical', action='store_true', dest='skip_identical',
|
||||||
default=False, help='Skip downloading files that are identical on '
|
default=False, help='Skip downloading files that are identical on '
|
||||||
'both sides.')
|
'both sides.')
|
||||||
|
parser.add_argument(
|
||||||
|
'--version-id', action='store', default=None,
|
||||||
|
help='Download a specific version of a versioned object')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--ignore-checksum', action='store_false', dest='checksum',
|
'--ignore-checksum', action='store_false', dest='checksum',
|
||||||
default=True, help='Turn off checksum validation for downloads.')
|
default=True, help='Turn off checksum validation for downloads.')
|
||||||
@ -372,6 +391,8 @@ def st_download(parser, args, output_manager, return_parser=False):
|
|||||||
output_manager.error('Usage: %s download %s\n%s', BASENAME,
|
output_manager.error('Usage: %s download %s\n%s', BASENAME,
|
||||||
st_download_options, st_download_help)
|
st_download_options, st_download_help)
|
||||||
return
|
return
|
||||||
|
if options['version_id'] and len(args) < 2:
|
||||||
|
exit('--version-id option only allowed for object downloads')
|
||||||
|
|
||||||
if options['object_threads'] <= 0:
|
if options['object_threads'] <= 0:
|
||||||
output_manager.error(
|
output_manager.error(
|
||||||
@ -479,7 +500,7 @@ def st_download(parser, args, output_manager, return_parser=False):
|
|||||||
|
|
||||||
st_list_options = '''[--long] [--lh] [--totals] [--prefix <prefix>]
|
st_list_options = '''[--long] [--lh] [--totals] [--prefix <prefix>]
|
||||||
[--delimiter <delimiter>] [--header <header:value>]
|
[--delimiter <delimiter>] [--header <header:value>]
|
||||||
[<container>]
|
[--versions] [<container>]
|
||||||
'''
|
'''
|
||||||
|
|
||||||
st_list_help = '''
|
st_list_help = '''
|
||||||
@ -499,6 +520,8 @@ Optional arguments:
|
|||||||
Roll up items with the given delimiter. For containers
|
Roll up items with the given delimiter. For containers
|
||||||
only. See OpenStack Swift API documentation for what
|
only. See OpenStack Swift API documentation for what
|
||||||
this means.
|
this means.
|
||||||
|
-j, --json Display listing information in json
|
||||||
|
--versions Display listing information for all versions
|
||||||
-H, --header <header:value>
|
-H, --header <header:value>
|
||||||
Adds a custom request header to use for listing.
|
Adds a custom request header to use for listing.
|
||||||
'''.strip('\n')
|
'''.strip('\n')
|
||||||
@ -579,6 +602,8 @@ def st_list(parser, args, output_manager, return_parser=False):
|
|||||||
'what this means.')
|
'what this means.')
|
||||||
parser.add_argument('-j', '--json', action='store_true',
|
parser.add_argument('-j', '--json', action='store_true',
|
||||||
help='print listing information in json')
|
help='print listing information in json')
|
||||||
|
parser.add_argument('--versions', action='store_true',
|
||||||
|
help='display all versions')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-H', '--header', action='append', dest='header',
|
'-H', '--header', action='append', dest='header',
|
||||||
default=[],
|
default=[],
|
||||||
@ -592,6 +617,8 @@ def st_list(parser, args, output_manager, return_parser=False):
|
|||||||
args = args[1:]
|
args = args[1:]
|
||||||
if options['delimiter'] and not args:
|
if options['delimiter'] and not args:
|
||||||
exit('-d option only allowed for container listings')
|
exit('-d option only allowed for container listings')
|
||||||
|
if options['versions'] and not args:
|
||||||
|
exit('--versions option only allowed for container listings')
|
||||||
|
|
||||||
human = options.pop('human')
|
human = options.pop('human')
|
||||||
if human:
|
if human:
|
||||||
@ -642,6 +669,7 @@ def st_list(parser, args, output_manager, return_parser=False):
|
|||||||
|
|
||||||
|
|
||||||
st_stat_options = '''[--lh] [--header <header:value>]
|
st_stat_options = '''[--lh] [--header <header:value>]
|
||||||
|
[--version-id <version_id>]
|
||||||
[<container> [<object>]]
|
[<container> [<object>]]
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@ -655,6 +683,8 @@ Positional arguments:
|
|||||||
Optional arguments:
|
Optional arguments:
|
||||||
--lh Report sizes in human readable format similar to
|
--lh Report sizes in human readable format similar to
|
||||||
ls -lh.
|
ls -lh.
|
||||||
|
--version-id <version-id>
|
||||||
|
Report stat of specific version of a versioned object.
|
||||||
-H, --header <header:value>
|
-H, --header <header:value>
|
||||||
Adds a custom request header to use for stat.
|
Adds a custom request header to use for stat.
|
||||||
'''.strip('\n')
|
'''.strip('\n')
|
||||||
@ -664,6 +694,9 @@ def st_stat(parser, args, output_manager, return_parser=False):
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--lh', dest='human', action='store_true', default=False,
|
'--lh', dest='human', action='store_true', default=False,
|
||||||
help='Report sizes in human readable format similar to ls -lh.')
|
help='Report sizes in human readable format similar to ls -lh.')
|
||||||
|
parser.add_argument(
|
||||||
|
'--version-id', action='store', default=None,
|
||||||
|
help='Report stat of a specific version of a versioned object')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-H', '--header', action='append', dest='header',
|
'-H', '--header', action='append', dest='header',
|
||||||
default=[],
|
default=[],
|
||||||
@ -675,6 +708,8 @@ def st_stat(parser, args, output_manager, return_parser=False):
|
|||||||
|
|
||||||
options, args = parse_args(parser, args)
|
options, args = parse_args(parser, args)
|
||||||
args = args[1:]
|
args = args[1:]
|
||||||
|
if options['version_id'] and len(args) < 2:
|
||||||
|
exit('--version-id option only allowed for object stats')
|
||||||
|
|
||||||
with SwiftService(options=options) as swift:
|
with SwiftService(options=options) as swift:
|
||||||
try:
|
try:
|
||||||
|
@ -21,6 +21,7 @@ import six
|
|||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
import time
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
@ -33,7 +34,7 @@ import swiftclient
|
|||||||
import swiftclient.utils as utils
|
import swiftclient.utils as utils
|
||||||
from swiftclient.client import Connection, ClientException
|
from swiftclient.client import Connection, ClientException
|
||||||
from swiftclient.service import (
|
from swiftclient.service import (
|
||||||
SwiftService, SwiftError, SwiftUploadObject
|
SwiftService, SwiftError, SwiftUploadObject, SwiftDeleteObject
|
||||||
)
|
)
|
||||||
|
|
||||||
from test.unit import utils as test_utils
|
from test.unit import utils as test_utils
|
||||||
@ -315,11 +316,39 @@ class TestServiceDelete(_TestServiceBase):
|
|||||||
mock_conn.head_object.assert_called_once_with(
|
mock_conn.head_object.assert_called_once_with(
|
||||||
'test_c', 'test_o', query_string='symlink=get', headers={})
|
'test_c', 'test_o', query_string='symlink=get', headers={})
|
||||||
mock_conn.delete_object.assert_called_once_with(
|
mock_conn.delete_object.assert_called_once_with(
|
||||||
'test_c', 'test_o', query_string=None, response_dict={},
|
'test_c', 'test_o', query_string='', response_dict={},
|
||||||
headers={}
|
headers={}
|
||||||
)
|
)
|
||||||
self.assertEqual(expected_r, r)
|
self.assertEqual(expected_r, r)
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_delete_object_version(self, mock_connection_class):
|
||||||
|
mock_conn = mock_connection_class.return_value
|
||||||
|
mock_conn.url = 'http://saio/v1/AUTH_test'
|
||||||
|
mock_conn.attempts = 0
|
||||||
|
mock_conn.head_object.return_value = {}
|
||||||
|
mock_conn.delete_object.return_value = {}
|
||||||
|
expected = {
|
||||||
|
'action': 'delete_object',
|
||||||
|
'attempts': 0,
|
||||||
|
'container': 'c',
|
||||||
|
'object': 'o',
|
||||||
|
'response_dict': {},
|
||||||
|
'success': True}
|
||||||
|
with SwiftService() as swift:
|
||||||
|
delete_results = swift.delete(
|
||||||
|
container='c', objects='o', options={
|
||||||
|
'version_id': '234567.8'})
|
||||||
|
for delete_result in delete_results:
|
||||||
|
self.assertEqual(delete_result, expected)
|
||||||
|
self.assertEqual(mock_conn.mock_calls, [
|
||||||
|
mock.call.head_object('c', 'o', headers={},
|
||||||
|
query_string='symlink=get'),
|
||||||
|
mock.call.delete_object('c', 'o', headers={},
|
||||||
|
query_string='version-id=234567.8',
|
||||||
|
response_dict={}),
|
||||||
|
])
|
||||||
|
|
||||||
def test_delete_object_with_headers(self):
|
def test_delete_object_with_headers(self):
|
||||||
mock_q = Queue()
|
mock_q = Queue()
|
||||||
mock_conn = self._get_mock_connection()
|
mock_conn = self._get_mock_connection()
|
||||||
@ -338,7 +367,7 @@ class TestServiceDelete(_TestServiceBase):
|
|||||||
'test_c', 'test_o', headers={'Skip-Middleware': 'Test'},
|
'test_c', 'test_o', headers={'Skip-Middleware': 'Test'},
|
||||||
query_string='symlink=get')
|
query_string='symlink=get')
|
||||||
mock_conn.delete_object.assert_called_once_with(
|
mock_conn.delete_object.assert_called_once_with(
|
||||||
'test_c', 'test_o', query_string=None, response_dict={},
|
'test_c', 'test_o', query_string='', response_dict={},
|
||||||
headers={'Skip-Middleware': 'Test'}
|
headers={'Skip-Middleware': 'Test'}
|
||||||
)
|
)
|
||||||
self.assertEqual(expected_r, r)
|
self.assertEqual(expected_r, r)
|
||||||
@ -366,7 +395,7 @@ class TestServiceDelete(_TestServiceBase):
|
|||||||
mock_conn.head_object.assert_called_once_with(
|
mock_conn.head_object.assert_called_once_with(
|
||||||
'test_c', 'test_o', query_string='symlink=get', headers={})
|
'test_c', 'test_o', query_string='symlink=get', headers={})
|
||||||
mock_conn.delete_object.assert_called_once_with(
|
mock_conn.delete_object.assert_called_once_with(
|
||||||
'test_c', 'test_o', query_string=None, response_dict={},
|
'test_c', 'test_o', query_string='', response_dict={},
|
||||||
headers={}
|
headers={}
|
||||||
)
|
)
|
||||||
self.assertEqual(expected_r, r)
|
self.assertEqual(expected_r, r)
|
||||||
@ -431,7 +460,7 @@ class TestServiceDelete(_TestServiceBase):
|
|||||||
|
|
||||||
self.assertEqual(expected_r, r)
|
self.assertEqual(expected_r, r)
|
||||||
expected = [
|
expected = [
|
||||||
mock.call('test_c', 'test_o', query_string=None, response_dict={},
|
mock.call('test_c', 'test_o', query_string='', response_dict={},
|
||||||
headers={}),
|
headers={}),
|
||||||
mock.call('manifest_c', 'test_seg_1', response_dict={}),
|
mock.call('manifest_c', 'test_seg_1', response_dict={}),
|
||||||
mock.call('manifest_c', 'test_seg_2', response_dict={})]
|
mock.call('manifest_c', 'test_seg_2', response_dict={})]
|
||||||
@ -529,6 +558,63 @@ class TestServiceDelete(_TestServiceBase):
|
|||||||
if errors:
|
if errors:
|
||||||
self.fail('_bulk_delete_page_size() failed\n' + '\n'.join(errors))
|
self.fail('_bulk_delete_page_size() failed\n' + '\n'.join(errors))
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_bulk_delete(self, mock_connection_class):
|
||||||
|
mock_conn = mock_connection_class.return_value
|
||||||
|
mock_conn.attempts = 0
|
||||||
|
mock_conn.get_capabilities.return_value = {
|
||||||
|
'bulk_delete': {}}
|
||||||
|
stub_headers = {}
|
||||||
|
stub_resp = []
|
||||||
|
mock_conn.post_account.return_value = (
|
||||||
|
stub_headers, json.dumps(stub_resp).encode('utf8'))
|
||||||
|
obj_list = ['x%02d' % i for i in range(100)]
|
||||||
|
expected = [{
|
||||||
|
'action': u'bulk_delete',
|
||||||
|
'attempts': 0,
|
||||||
|
'container': 'c',
|
||||||
|
'objects': list(objs),
|
||||||
|
'response_dict': {},
|
||||||
|
'result': [],
|
||||||
|
'success': True,
|
||||||
|
} for objs in zip(*[iter(obj_list)] * 10)]
|
||||||
|
found_result = []
|
||||||
|
with SwiftService(options={'object_dd_threads': 10}) as swift:
|
||||||
|
delete_results = swift.delete(container='c', objects=obj_list)
|
||||||
|
for delete_result in delete_results:
|
||||||
|
found_result.append(delete_result)
|
||||||
|
self.assertEqual(sorted(found_result, key=lambda r: r['objects'][0]),
|
||||||
|
expected)
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_bulk_delete_versions(self, mock_connection_class):
|
||||||
|
mock_conn = mock_connection_class.return_value
|
||||||
|
mock_conn.attempts = 0
|
||||||
|
mock_conn.get_capabilities.return_value = {
|
||||||
|
'bulk_delete': {}}
|
||||||
|
mock_conn.head_object.return_value = {}
|
||||||
|
stub_headers = {}
|
||||||
|
stub_resp = []
|
||||||
|
mock_conn.post_account.return_value = (
|
||||||
|
stub_headers, json.dumps(stub_resp))
|
||||||
|
obj_list = [SwiftDeleteObject('x%02d' % i, options={'version_id': i})
|
||||||
|
for i in range(100)]
|
||||||
|
expected = [{
|
||||||
|
'action': u'delete_object',
|
||||||
|
'attempts': 0,
|
||||||
|
'container': 'c',
|
||||||
|
'object': obj.object_name,
|
||||||
|
'response_dict': {},
|
||||||
|
'success': True,
|
||||||
|
} for obj in obj_list]
|
||||||
|
found_result = []
|
||||||
|
with SwiftService(options={'object_dd_threads': 10}) as swift:
|
||||||
|
delete_results = swift.delete(container='c', objects=obj_list)
|
||||||
|
for delete_result in delete_results:
|
||||||
|
found_result.append(delete_result)
|
||||||
|
self.assertEqual(sorted(found_result, key=lambda r: r['object']),
|
||||||
|
expected)
|
||||||
|
|
||||||
|
|
||||||
class TestSwiftError(unittest.TestCase):
|
class TestSwiftError(unittest.TestCase):
|
||||||
|
|
||||||
@ -938,9 +1024,11 @@ class TestServiceList(_TestServiceBase):
|
|||||||
self.assertIsNone(self._get_queue(mock_q))
|
self.assertIsNone(self._get_queue(mock_q))
|
||||||
self.assertEqual(mock_conn.get_container.mock_calls, [
|
self.assertEqual(mock_conn.get_container.mock_calls, [
|
||||||
mock.call('test_c', headers={'Skip-Middleware': 'Test'},
|
mock.call('test_c', headers={'Skip-Middleware': 'Test'},
|
||||||
delimiter='', marker='', prefix=None),
|
delimiter='', marker='', prefix=None,
|
||||||
|
query_string=None, version_marker=''),
|
||||||
mock.call('test_c', headers={'Skip-Middleware': 'Test'},
|
mock.call('test_c', headers={'Skip-Middleware': 'Test'},
|
||||||
delimiter='', marker='test_o', prefix=None)])
|
delimiter='', marker='test_o', prefix=None,
|
||||||
|
query_string=None, version_marker='')])
|
||||||
|
|
||||||
def test_list_container_exception(self):
|
def test_list_container_exception(self):
|
||||||
mock_q = Queue()
|
mock_q = Queue()
|
||||||
@ -952,6 +1040,7 @@ class TestServiceList(_TestServiceBase):
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': self.exc,
|
'error': self.exc,
|
||||||
'marker': '',
|
'marker': '',
|
||||||
|
'version_marker': '',
|
||||||
'error_timestamp': mock.ANY,
|
'error_timestamp': mock.ANY,
|
||||||
'traceback': mock.ANY
|
'traceback': mock.ANY
|
||||||
})
|
})
|
||||||
@ -961,11 +1050,61 @@ class TestServiceList(_TestServiceBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
mock_conn.get_container.assert_called_once_with(
|
mock_conn.get_container.assert_called_once_with(
|
||||||
'test_c', marker='', delimiter='', prefix=None, headers={}
|
'test_c', marker='', delimiter='', prefix=None, headers={},
|
||||||
|
query_string=None, version_marker='',
|
||||||
)
|
)
|
||||||
self.assertEqual(expected_r, self._get_queue(mock_q))
|
self.assertEqual(expected_r, self._get_queue(mock_q))
|
||||||
self.assertIsNone(self._get_queue(mock_q))
|
self.assertIsNone(self._get_queue(mock_q))
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_list_container_versions(self, mock_connection_class):
|
||||||
|
mock_conn = mock_connection_class.return_value
|
||||||
|
mock_conn.url = 'http://saio/v1/AUTH_test'
|
||||||
|
resp_headers = {}
|
||||||
|
items = [{
|
||||||
|
"bytes": 9,
|
||||||
|
"content_type": "application/octet-stream",
|
||||||
|
"hash": "e55cedc11adb39c404b7365f7d6291fa",
|
||||||
|
"is_latest": True,
|
||||||
|
"last_modified": "2019-11-08T05:00:15.115360",
|
||||||
|
"name": "test",
|
||||||
|
"version_id": "1573189215.11536"
|
||||||
|
}, {
|
||||||
|
"bytes": 8,
|
||||||
|
"content_type": "application/octet-stream",
|
||||||
|
"hash": "70c1db56f301c9e337b0099bd4174b28",
|
||||||
|
"is_latest": False,
|
||||||
|
"last_modified": "2019-11-08T05:00:14.730240",
|
||||||
|
"name": "test",
|
||||||
|
"version_id": "1573184903.06720"
|
||||||
|
}]
|
||||||
|
mock_conn.get_container.side_effect = [
|
||||||
|
(resp_headers, items),
|
||||||
|
(resp_headers, []),
|
||||||
|
]
|
||||||
|
expected = {
|
||||||
|
'action': 'list_container_part',
|
||||||
|
'container': 'c',
|
||||||
|
'listing': items,
|
||||||
|
'marker': '',
|
||||||
|
'prefix': None,
|
||||||
|
'success': True,
|
||||||
|
}
|
||||||
|
with SwiftService() as swift:
|
||||||
|
list_result_gen = swift.list(container='c', options={
|
||||||
|
'versions': True})
|
||||||
|
self.maxDiff = None
|
||||||
|
for result in list_result_gen:
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
self.assertEqual(mock_conn.get_container.mock_calls, [
|
||||||
|
mock.call('c', delimiter=None, headers={}, marker='',
|
||||||
|
prefix=None, query_string='versions=true',
|
||||||
|
version_marker=''),
|
||||||
|
mock.call('c', delimiter=None, headers={}, marker='test',
|
||||||
|
prefix=None, query_string='versions=true',
|
||||||
|
version_marker='1573184903.06720'),
|
||||||
|
])
|
||||||
|
|
||||||
@mock.patch('swiftclient.service.get_conn')
|
@mock.patch('swiftclient.service.get_conn')
|
||||||
def test_list_queue_size(self, mock_get_conn):
|
def test_list_queue_size(self, mock_get_conn):
|
||||||
mock_conn = self._get_mock_connection()
|
mock_conn = self._get_mock_connection()
|
||||||
@ -1042,6 +1181,67 @@ class TestServiceList(_TestServiceBase):
|
|||||||
self.assertEqual(observed_listing, expected_listing)
|
self.assertEqual(observed_listing, expected_listing)
|
||||||
|
|
||||||
|
|
||||||
|
class TestServiceStat(_TestServiceBase):
|
||||||
|
|
||||||
|
maxDiff = None
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_stat_object(self, mock_connection_class):
|
||||||
|
mock_conn = mock_connection_class.return_value
|
||||||
|
mock_conn.url = 'http://saio/v1/AUTH_test'
|
||||||
|
mock_conn.head_object.return_value = {}
|
||||||
|
expected = {
|
||||||
|
'action': 'stat_object',
|
||||||
|
'container': 'c',
|
||||||
|
'object': 'o',
|
||||||
|
'headers': {},
|
||||||
|
'items': [('Account', 'AUTH_test'),
|
||||||
|
('Container', 'c'),
|
||||||
|
('Object', 'o'),
|
||||||
|
('Content Type', None),
|
||||||
|
('Content Length', '0'),
|
||||||
|
('Last Modified', None),
|
||||||
|
('ETag', None),
|
||||||
|
('Manifest', None)],
|
||||||
|
'success': True}
|
||||||
|
with SwiftService() as swift:
|
||||||
|
stat_results = swift.stat(container='c', objects='o')
|
||||||
|
for stat_result in stat_results:
|
||||||
|
self.assertEqual(stat_result, expected)
|
||||||
|
self.assertEqual(mock_conn.head_object.mock_calls, [
|
||||||
|
mock.call('c', 'o', headers={}, query_string=None),
|
||||||
|
])
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_stat_versioned_object(self, mock_connection_class):
|
||||||
|
mock_conn = mock_connection_class.return_value
|
||||||
|
mock_conn.url = 'http://saio/v1/AUTH_test'
|
||||||
|
mock_conn.head_object.return_value = {}
|
||||||
|
expected = {
|
||||||
|
'action': 'stat_object',
|
||||||
|
'container': 'c',
|
||||||
|
'object': 'o',
|
||||||
|
'headers': {},
|
||||||
|
'items': [('Account', 'AUTH_test'),
|
||||||
|
('Container', 'c'),
|
||||||
|
('Object', 'o'),
|
||||||
|
('Content Type', None),
|
||||||
|
('Content Length', '0'),
|
||||||
|
('Last Modified', None),
|
||||||
|
('ETag', None),
|
||||||
|
('Manifest', None)],
|
||||||
|
'success': True}
|
||||||
|
with SwiftService() as swift:
|
||||||
|
stat_results = swift.stat(container='c', objects='o', options={
|
||||||
|
'version_id': '234567.8'})
|
||||||
|
for stat_result in stat_results:
|
||||||
|
self.assertEqual(stat_result, expected)
|
||||||
|
self.assertEqual(mock_conn.head_object.mock_calls, [
|
||||||
|
mock.call('c', 'o', headers={},
|
||||||
|
query_string='version-id=234567.8'),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
class TestService(unittest.TestCase):
|
class TestService(unittest.TestCase):
|
||||||
|
|
||||||
def test_upload_with_bad_segment_size(self):
|
def test_upload_with_bad_segment_size(self):
|
||||||
@ -1791,13 +1991,14 @@ class TestServiceUpload(_TestServiceBase):
|
|||||||
mock_conn.head_object.assert_called_with('test_c', 'test_o')
|
mock_conn.head_object.assert_called_with('test_c', 'test_o')
|
||||||
expected = [
|
expected = [
|
||||||
mock.call('test_c_segments', prefix='test_o/prefix',
|
mock.call('test_c_segments', prefix='test_o/prefix',
|
||||||
marker='', delimiter=None, headers={}),
|
marker='', delimiter=None, headers={},
|
||||||
|
query_string=None, version_marker=''),
|
||||||
mock.call('test_c_segments', prefix='test_o/prefix',
|
mock.call('test_c_segments', prefix='test_o/prefix',
|
||||||
marker="test_o/prefix/01", delimiter=None,
|
marker="test_o/prefix/01", delimiter=None,
|
||||||
headers={}),
|
headers={}, query_string=None, version_marker=''),
|
||||||
mock.call('test_c_segments', prefix='test_o/prefix',
|
mock.call('test_c_segments', prefix='test_o/prefix',
|
||||||
marker="test_o/prefix/02", delimiter=None,
|
marker="test_o/prefix/02", delimiter=None,
|
||||||
headers={}),
|
headers={}, query_string=None, version_marker=''),
|
||||||
]
|
]
|
||||||
mock_conn.get_container.assert_has_calls(expected)
|
mock_conn.get_container.assert_has_calls(expected)
|
||||||
|
|
||||||
@ -2332,6 +2533,29 @@ class TestServiceDownload(_TestServiceBase):
|
|||||||
self.assertEqual(resp['object'], 'test')
|
self.assertEqual(resp['object'], 'test')
|
||||||
self.assertEqual(resp['path'], 'test')
|
self.assertEqual(resp['path'], 'test')
|
||||||
|
|
||||||
|
def test_download_version_id(self):
|
||||||
|
self.opts['version_id'] = '23456.7'
|
||||||
|
with mock.patch('swiftclient.service.Connection') as mock_conn:
|
||||||
|
header = {'content-length': self.obj_len,
|
||||||
|
'etag': self.obj_etag}
|
||||||
|
mock_conn.get_object.return_value = header, self._readbody()
|
||||||
|
|
||||||
|
resp = SwiftService()._download_object_job(mock_conn,
|
||||||
|
'c',
|
||||||
|
'test',
|
||||||
|
self.opts)
|
||||||
|
|
||||||
|
self.assertIsNone(resp.get('error'))
|
||||||
|
self.assertIs(True, resp['success'])
|
||||||
|
self.assertEqual(resp['action'], 'download_object')
|
||||||
|
self.assertEqual(resp['object'], 'test')
|
||||||
|
self.assertEqual(resp['path'], 'test')
|
||||||
|
self.assertEqual(mock_conn.get_object.mock_calls, [
|
||||||
|
mock.call(
|
||||||
|
'c', 'test', headers={}, query_string='version-id=23456.7',
|
||||||
|
resp_chunk_size=65536, response_dict={}),
|
||||||
|
])
|
||||||
|
|
||||||
@mock.patch('swiftclient.service.interruptable_as_completed')
|
@mock.patch('swiftclient.service.interruptable_as_completed')
|
||||||
@mock.patch('swiftclient.service.SwiftService._download_container')
|
@mock.patch('swiftclient.service.SwiftService._download_container')
|
||||||
@mock.patch('swiftclient.service.SwiftService._download_object_job')
|
@mock.patch('swiftclient.service.SwiftService._download_object_job')
|
||||||
@ -2545,17 +2769,17 @@ class TestServiceDownload(_TestServiceBase):
|
|||||||
delimiter=None,
|
delimiter=None,
|
||||||
prefix='test_o/prefix',
|
prefix='test_o/prefix',
|
||||||
marker='',
|
marker='',
|
||||||
headers={}),
|
headers={}, query_string=None, version_marker=''),
|
||||||
mock.call('test_c_segments',
|
mock.call('test_c_segments',
|
||||||
delimiter=None,
|
delimiter=None,
|
||||||
prefix='test_o/prefix',
|
prefix='test_o/prefix',
|
||||||
marker='test_o/prefix/2',
|
marker='test_o/prefix/2',
|
||||||
headers={}),
|
headers={}, query_string=None, version_marker=''),
|
||||||
mock.call('test_c_segments',
|
mock.call('test_c_segments',
|
||||||
delimiter=None,
|
delimiter=None,
|
||||||
prefix='test_o/prefix',
|
prefix='test_o/prefix',
|
||||||
marker='test_o/prefix/3',
|
marker='test_o/prefix/3',
|
||||||
headers={})])
|
headers={}, query_string=None, version_marker='')])
|
||||||
|
|
||||||
def test_download_object_job_skip_identical_nested_slo(self):
|
def test_download_object_job_skip_identical_nested_slo(self):
|
||||||
with tempfile.NamedTemporaryFile() as f:
|
with tempfile.NamedTemporaryFile() as f:
|
||||||
@ -2682,6 +2906,7 @@ class TestServiceDownload(_TestServiceBase):
|
|||||||
obj='test_o',
|
obj='test_o',
|
||||||
options=options)
|
options=options)
|
||||||
|
|
||||||
|
self.maxDiff = None
|
||||||
self.assertEqual(r, expected_r)
|
self.assertEqual(r, expected_r)
|
||||||
|
|
||||||
self.assertEqual(mock_conn.get_container.mock_calls, [
|
self.assertEqual(mock_conn.get_container.mock_calls, [
|
||||||
@ -2689,17 +2914,17 @@ class TestServiceDownload(_TestServiceBase):
|
|||||||
delimiter=None,
|
delimiter=None,
|
||||||
prefix='test_o/prefix',
|
prefix='test_o/prefix',
|
||||||
marker='',
|
marker='',
|
||||||
headers={}),
|
headers={}, query_string=None, version_marker=''),
|
||||||
mock.call('test_c_segments',
|
mock.call('test_c_segments',
|
||||||
delimiter=None,
|
delimiter=None,
|
||||||
prefix='test_o/prefix',
|
prefix='test_o/prefix',
|
||||||
marker='test_o/prefix/2',
|
marker='test_o/prefix/2',
|
||||||
headers={}),
|
headers={}, query_string=None, version_marker=''),
|
||||||
mock.call('test_c_segments',
|
mock.call('test_c_segments',
|
||||||
delimiter=None,
|
delimiter=None,
|
||||||
prefix='test_o/prefix',
|
prefix='test_o/prefix',
|
||||||
marker='test_o/prefix/3',
|
marker='test_o/prefix/3',
|
||||||
headers={})])
|
headers={}, query_string=None, version_marker='')])
|
||||||
self.assertEqual(mock_conn.get_object.mock_calls, [
|
self.assertEqual(mock_conn.get_object.mock_calls, [
|
||||||
mock.call('test_c',
|
mock.call('test_c',
|
||||||
'test_o',
|
'test_o',
|
||||||
|
@ -241,6 +241,30 @@ class TestShell(unittest.TestCase):
|
|||||||
self.assertEqual(connection.return_value.head_container.mock_calls, [
|
self.assertEqual(connection.return_value.head_container.mock_calls, [
|
||||||
mock.call('container', headers={'Skip-Middleware': 'Test'})])
|
mock.call('container', headers={'Skip-Middleware': 'Test'})])
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_stat_version_id(self, connection):
|
||||||
|
argv = ["", "stat", "--version-id", "1"]
|
||||||
|
with self.assertRaises(SystemExit) as caught:
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
self.assertEqual(str(caught.exception),
|
||||||
|
"--version-id option only allowed for "
|
||||||
|
"object stats")
|
||||||
|
|
||||||
|
argv = ["", "stat", "--version-id", "1", "container"]
|
||||||
|
with self.assertRaises(SystemExit) as caught:
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
self.assertEqual(str(caught.exception),
|
||||||
|
"--version-id option only allowed for "
|
||||||
|
"object stats")
|
||||||
|
|
||||||
|
argv = ["", "stat", "--version-id", "1", "container", "object"]
|
||||||
|
connection.return_value.head_object.return_value = {}
|
||||||
|
with CaptureOutput():
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
self.assertEqual([mock.call('container', 'object', headers={},
|
||||||
|
query_string='version-id=1')],
|
||||||
|
connection.return_value.head_object.mock_calls)
|
||||||
|
|
||||||
@mock.patch('swiftclient.service.Connection')
|
@mock.patch('swiftclient.service.Connection')
|
||||||
def test_stat_object(self, connection):
|
def test_stat_object(self, connection):
|
||||||
return_headers = {
|
return_headers = {
|
||||||
@ -295,7 +319,45 @@ class TestShell(unittest.TestCase):
|
|||||||
' Manifest: manifest\n')
|
' Manifest: manifest\n')
|
||||||
self.assertEqual(connection.return_value.head_object.mock_calls, [
|
self.assertEqual(connection.return_value.head_object.mock_calls, [
|
||||||
mock.call('container', 'object',
|
mock.call('container', 'object',
|
||||||
headers={'Skip-Middleware': 'Test'})])
|
headers={'Skip-Middleware': 'Test'},
|
||||||
|
query_string=None)])
|
||||||
|
|
||||||
|
def test_list_account_with_delimiter(self):
|
||||||
|
argv = ["", "list", "--delimiter", "foo"]
|
||||||
|
with self.assertRaises(SystemExit) as caught:
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
self.assertEqual(str(caught.exception),
|
||||||
|
"-d option only allowed for "
|
||||||
|
"container listings")
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_list_container_with_versions(self, connection):
|
||||||
|
connection.return_value.get_container.side_effect = [
|
||||||
|
[None, [
|
||||||
|
{'name': 'foo', 'version_id': '2'},
|
||||||
|
{'name': 'foo', 'version_id': '1'},
|
||||||
|
]],
|
||||||
|
[None, []],
|
||||||
|
]
|
||||||
|
argv = ["", "list", "container", "--versions"]
|
||||||
|
with CaptureOutput(suppress_systemexit=True) as output:
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
calls = [mock.call('container', delimiter=None, headers={}, marker='',
|
||||||
|
prefix=None, query_string='versions=true',
|
||||||
|
version_marker=''),
|
||||||
|
mock.call('container', delimiter=None, headers={},
|
||||||
|
marker='foo', prefix=None,
|
||||||
|
query_string='versions=true', version_marker='1')]
|
||||||
|
connection.return_value.get_container.assert_has_calls(calls)
|
||||||
|
self.assertEqual(output.out, 'foo\nfoo\n')
|
||||||
|
|
||||||
|
def test_list_account_with_versions(self):
|
||||||
|
argv = ["", "list", "--versions"]
|
||||||
|
with self.assertRaises(SystemExit) as caught:
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
self.assertEqual(str(caught.exception),
|
||||||
|
"--versions option only allowed for "
|
||||||
|
"container listings")
|
||||||
|
|
||||||
@mock.patch('swiftclient.service.Connection')
|
@mock.patch('swiftclient.service.Connection')
|
||||||
def test_list_json(self, connection):
|
def test_list_json(self, connection):
|
||||||
@ -431,9 +493,11 @@ class TestShell(unittest.TestCase):
|
|||||||
swiftclient.shell.main(argv)
|
swiftclient.shell.main(argv)
|
||||||
calls = [
|
calls = [
|
||||||
mock.call('container', marker='',
|
mock.call('container', marker='',
|
||||||
delimiter=None, prefix=None, headers={}),
|
delimiter=None, prefix=None, headers={},
|
||||||
|
query_string=None, version_marker=''),
|
||||||
mock.call('container', marker='object_a',
|
mock.call('container', marker='object_a',
|
||||||
delimiter=None, prefix=None, headers={})]
|
delimiter=None, prefix=None, headers={},
|
||||||
|
query_string=None, version_marker='')]
|
||||||
connection.return_value.get_container.assert_has_calls(calls)
|
connection.return_value.get_container.assert_has_calls(calls)
|
||||||
|
|
||||||
self.assertEqual(output.out, 'object_a\n')
|
self.assertEqual(output.out, 'object_a\n')
|
||||||
@ -450,9 +514,11 @@ class TestShell(unittest.TestCase):
|
|||||||
swiftclient.shell.main(argv)
|
swiftclient.shell.main(argv)
|
||||||
calls = [
|
calls = [
|
||||||
mock.call('container', marker='',
|
mock.call('container', marker='',
|
||||||
delimiter=None, prefix=None, headers={}),
|
delimiter=None, prefix=None, headers={},
|
||||||
|
query_string=None, version_marker=''),
|
||||||
mock.call('container', marker='object_a',
|
mock.call('container', marker='object_a',
|
||||||
delimiter=None, prefix=None, headers={})]
|
delimiter=None, prefix=None, headers={},
|
||||||
|
query_string=None, version_marker='')]
|
||||||
connection.return_value.get_container.assert_has_calls(calls)
|
connection.return_value.get_container.assert_has_calls(calls)
|
||||||
|
|
||||||
self.assertEqual(output.out,
|
self.assertEqual(output.out,
|
||||||
@ -472,14 +538,44 @@ class TestShell(unittest.TestCase):
|
|||||||
calls = [
|
calls = [
|
||||||
mock.call('container', marker='',
|
mock.call('container', marker='',
|
||||||
delimiter=None, prefix=None,
|
delimiter=None, prefix=None,
|
||||||
headers={'Skip-Middleware': 'Test'}),
|
headers={'Skip-Middleware': 'Test'},
|
||||||
|
query_string=None, version_marker=''),
|
||||||
mock.call('container', marker='object_a',
|
mock.call('container', marker='object_a',
|
||||||
delimiter=None, prefix=None,
|
delimiter=None, prefix=None,
|
||||||
headers={'Skip-Middleware': 'Test'})]
|
headers={'Skip-Middleware': 'Test'},
|
||||||
|
query_string=None, version_marker='')]
|
||||||
connection.return_value.get_container.assert_has_calls(calls)
|
connection.return_value.get_container.assert_has_calls(calls)
|
||||||
|
|
||||||
self.assertEqual(output.out, 'object_a\n')
|
self.assertEqual(output.out, 'object_a\n')
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_download_version_id(self, connection):
|
||||||
|
argv = ["", "download", "--yes-all", "--version-id", "5"]
|
||||||
|
with self.assertRaises(SystemExit) as caught:
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
self.assertEqual(str(caught.exception),
|
||||||
|
"--version-id option only allowed for "
|
||||||
|
"object downloads")
|
||||||
|
|
||||||
|
argv = ["", "download", "--version-id", "2", "container"]
|
||||||
|
with self.assertRaises(SystemExit) as caught:
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
self.assertEqual(str(caught.exception),
|
||||||
|
"--version-id option only allowed for "
|
||||||
|
"object downloads")
|
||||||
|
|
||||||
|
argv = ["", "download", "--version-id", "1", "container", "object"]
|
||||||
|
connection.return_value.head_object.return_value = {}
|
||||||
|
connection.return_value.get_object.return_value = {}, ''
|
||||||
|
connection.return_value.attempts = 0
|
||||||
|
with CaptureOutput():
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
self.assertEqual([mock.call('container', 'object', headers={},
|
||||||
|
query_string='version-id=1',
|
||||||
|
resp_chunk_size=65536,
|
||||||
|
response_dict={})],
|
||||||
|
connection.return_value.get_object.mock_calls)
|
||||||
|
|
||||||
@mock.patch('swiftclient.service.makedirs')
|
@mock.patch('swiftclient.service.makedirs')
|
||||||
@mock.patch('swiftclient.service.Connection')
|
@mock.patch('swiftclient.service.Connection')
|
||||||
def test_download(self, connection, makedirs):
|
def test_download(self, connection, makedirs):
|
||||||
@ -1085,6 +1181,33 @@ class TestShell(unittest.TestCase):
|
|||||||
check_good(["--object-threads", "1"])
|
check_good(["--object-threads", "1"])
|
||||||
check_good(["--container-threads", "1"])
|
check_good(["--container-threads", "1"])
|
||||||
|
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_delete_version_id(self, connection):
|
||||||
|
argv = ["", "delete", "--yes-all", "--version-id", "3"]
|
||||||
|
with self.assertRaises(SystemExit) as caught:
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
self.assertEqual(str(caught.exception),
|
||||||
|
"--version-id option only allowed for "
|
||||||
|
"object deletes")
|
||||||
|
|
||||||
|
argv = ["", "delete", "--version-id", "1", "container"]
|
||||||
|
with self.assertRaises(SystemExit) as caught:
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
self.assertEqual(str(caught.exception),
|
||||||
|
"--version-id option only allowed for "
|
||||||
|
"object deletes")
|
||||||
|
|
||||||
|
argv = ["", "delete", "--version-id", "1", "container", "object"]
|
||||||
|
connection.return_value.head_object.return_value = {}
|
||||||
|
connection.return_value.delete_object.return_value = None
|
||||||
|
connection.return_value.attempts = 0
|
||||||
|
with CaptureOutput():
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
self.assertEqual([mock.call('container', 'object', headers={},
|
||||||
|
query_string='version-id=1',
|
||||||
|
response_dict={})],
|
||||||
|
connection.return_value.delete_object.mock_calls)
|
||||||
|
|
||||||
@mock.patch.object(swiftclient.service.SwiftService,
|
@mock.patch.object(swiftclient.service.SwiftService,
|
||||||
'_bulk_delete_page_size', lambda *a: 1)
|
'_bulk_delete_page_size', lambda *a: 1)
|
||||||
@mock.patch('swiftclient.service.Connection')
|
@mock.patch('swiftclient.service.Connection')
|
||||||
@ -1094,10 +1217,12 @@ class TestShell(unittest.TestCase):
|
|||||||
[None, [{'name': 'empty_container'}]],
|
[None, [{'name': 'empty_container'}]],
|
||||||
[None, []],
|
[None, []],
|
||||||
]
|
]
|
||||||
|
# N.B: missing --versions flag, version-id gets ignored
|
||||||
|
# only latest object is deleted
|
||||||
connection.return_value.get_container.side_effect = [
|
connection.return_value.get_container.side_effect = [
|
||||||
[None, [{'name': 'object'}, {'name': 'obj\xe9ct2'}]],
|
[None, [{'name': 'object'}, {'name': 'obj\xe9ct2'}]],
|
||||||
[None, []],
|
[None, []],
|
||||||
[None, [{'name': 'object'}]],
|
[None, [{'name': 'object', 'version_id': 1}]],
|
||||||
[None, []],
|
[None, []],
|
||||||
[None, []],
|
[None, []],
|
||||||
]
|
]
|
||||||
@ -1107,11 +1232,48 @@ class TestShell(unittest.TestCase):
|
|||||||
connection.return_value.delete_object.return_value = None
|
connection.return_value.delete_object.return_value = None
|
||||||
swiftclient.shell.main(argv)
|
swiftclient.shell.main(argv)
|
||||||
connection.return_value.delete_object.assert_has_calls([
|
connection.return_value.delete_object.assert_has_calls([
|
||||||
mock.call('container', 'object', query_string=None,
|
mock.call('container', 'object', query_string='',
|
||||||
response_dict={}, headers={}),
|
response_dict={}, headers={}),
|
||||||
mock.call('container', 'obj\xe9ct2', query_string=None,
|
mock.call('container', 'obj\xe9ct2', query_string='',
|
||||||
response_dict={}, headers={}),
|
response_dict={}, headers={}),
|
||||||
mock.call('container2', 'object', query_string=None,
|
mock.call('container2', 'object', query_string='',
|
||||||
|
response_dict={}, headers={})], any_order=True)
|
||||||
|
self.assertEqual(3, connection.return_value.delete_object.call_count,
|
||||||
|
'Expected 3 calls but found\n%r'
|
||||||
|
% connection.return_value.delete_object.mock_calls)
|
||||||
|
self.assertEqual(
|
||||||
|
connection.return_value.delete_container.mock_calls, [
|
||||||
|
mock.call('container', response_dict={}, headers={}),
|
||||||
|
mock.call('container2', response_dict={}, headers={}),
|
||||||
|
mock.call('empty_container', response_dict={}, headers={})])
|
||||||
|
|
||||||
|
@mock.patch.object(swiftclient.service.SwiftService,
|
||||||
|
'_bulk_delete_page_size', lambda *a: 1)
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_delete_account_versions(self, connection):
|
||||||
|
connection.return_value.get_account.side_effect = [
|
||||||
|
[None, [{'name': 'container'}, {'name': 'container2'}]],
|
||||||
|
[None, [{'name': 'empty_container'}]],
|
||||||
|
[None, []],
|
||||||
|
]
|
||||||
|
connection.return_value.get_container.side_effect = [
|
||||||
|
[None, [{'name': 'object'}, {'name': 'obj\xe9ct2'}]],
|
||||||
|
[None, []],
|
||||||
|
[None, [{'name': 'obj', 'version_id': 1}]],
|
||||||
|
[None, []],
|
||||||
|
[None, []],
|
||||||
|
]
|
||||||
|
connection.return_value.attempts = 0
|
||||||
|
argv = ["", "delete", "--all", "--versions"]
|
||||||
|
connection.return_value.head_object.return_value = {}
|
||||||
|
connection.return_value.delete_object.return_value = None
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
connection.return_value.delete_object.assert_has_calls([
|
||||||
|
mock.call('container', 'object', query_string='',
|
||||||
|
response_dict={}, headers={}),
|
||||||
|
mock.call('container', 'obj\xe9ct2', query_string='',
|
||||||
|
response_dict={}, headers={}),
|
||||||
|
mock.call('container2', 'obj', query_string='version-id=1',
|
||||||
response_dict={}, headers={})], any_order=True)
|
response_dict={}, headers={})], any_order=True)
|
||||||
self.assertEqual(3, connection.return_value.delete_object.call_count,
|
self.assertEqual(3, connection.return_value.delete_object.call_count,
|
||||||
'Expected 3 calls but found\n%r'
|
'Expected 3 calls but found\n%r'
|
||||||
@ -1323,9 +1485,39 @@ class TestShell(unittest.TestCase):
|
|||||||
connection.return_value.delete_container.assert_called_with(
|
connection.return_value.delete_container.assert_called_with(
|
||||||
'container', response_dict={}, headers={})
|
'container', response_dict={}, headers={})
|
||||||
connection.return_value.delete_object.assert_called_with(
|
connection.return_value.delete_object.assert_called_with(
|
||||||
'container', 'object', query_string=None, response_dict={},
|
'container', 'object', query_string='', response_dict={},
|
||||||
headers={})
|
headers={})
|
||||||
|
|
||||||
|
@mock.patch.object(swiftclient.service.SwiftService,
|
||||||
|
'_bulk_delete_page_size', lambda *a: 1)
|
||||||
|
@mock.patch('swiftclient.service.Connection')
|
||||||
|
def test_delete_container_versions(self, connection):
|
||||||
|
argv = ["", "delete", "--versions", "container", "obj"]
|
||||||
|
with self.assertRaises(SystemExit) as caught:
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
self.assertEqual(str(caught.exception),
|
||||||
|
"--versions option not allowed for object deletes")
|
||||||
|
|
||||||
|
connection.return_value.get_container.side_effect = [
|
||||||
|
[None, [{'name': 'object', 'version_id': 2},
|
||||||
|
{'name': 'object', 'version_id': 1}]],
|
||||||
|
[None, []],
|
||||||
|
]
|
||||||
|
connection.return_value.attempts = 0
|
||||||
|
argv = ["", "delete", "--versions", "container"]
|
||||||
|
connection.return_value.head_object.return_value = {}
|
||||||
|
swiftclient.shell.main(argv)
|
||||||
|
connection.return_value.delete_container.assert_called_with(
|
||||||
|
'container', response_dict={}, headers={})
|
||||||
|
expected_calls = [
|
||||||
|
mock.call('container', 'object', query_string='version-id=2',
|
||||||
|
response_dict={}, headers={}),
|
||||||
|
mock.call('container', 'object', query_string='version-id=1',
|
||||||
|
response_dict={}, headers={})]
|
||||||
|
|
||||||
|
self.assertEqual(connection.return_value.delete_object.mock_calls,
|
||||||
|
expected_calls)
|
||||||
|
|
||||||
@mock.patch.object(swiftclient.service.SwiftService,
|
@mock.patch.object(swiftclient.service.SwiftService,
|
||||||
'_bulk_delete_page_size', lambda *a: 1)
|
'_bulk_delete_page_size', lambda *a: 1)
|
||||||
@mock.patch('swiftclient.service.Connection')
|
@mock.patch('swiftclient.service.Connection')
|
||||||
@ -1342,7 +1534,7 @@ class TestShell(unittest.TestCase):
|
|||||||
'container', response_dict={},
|
'container', response_dict={},
|
||||||
headers={'Skip-Middleware': 'Test'})
|
headers={'Skip-Middleware': 'Test'})
|
||||||
connection.return_value.delete_object.assert_called_with(
|
connection.return_value.delete_object.assert_called_with(
|
||||||
'container', 'object', query_string=None, response_dict={},
|
'container', 'object', query_string='', response_dict={},
|
||||||
headers={'Skip-Middleware': 'Test'})
|
headers={'Skip-Middleware': 'Test'})
|
||||||
|
|
||||||
@mock.patch.object(swiftclient.service.SwiftService,
|
@mock.patch.object(swiftclient.service.SwiftService,
|
||||||
@ -1408,7 +1600,7 @@ class TestShell(unittest.TestCase):
|
|||||||
connection.return_value.attempts = 0
|
connection.return_value.attempts = 0
|
||||||
swiftclient.shell.main(argv)
|
swiftclient.shell.main(argv)
|
||||||
connection.return_value.delete_object.assert_called_with(
|
connection.return_value.delete_object.assert_called_with(
|
||||||
'container', 'object', query_string=None, response_dict={},
|
'container', 'object', query_string='', response_dict={},
|
||||||
headers={})
|
headers={})
|
||||||
|
|
||||||
@mock.patch.object(swiftclient.service.SwiftService,
|
@mock.patch.object(swiftclient.service.SwiftService,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user