1 # Copyright 2011-2013 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
11 # 2. Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following
13 # disclaimer in the documentation and/or other materials
14 # provided with the distribution.
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.command
34 from time import localtime, strftime
35 from io import StringIO
36 from pydoc import pager
37 from os import path, walk, makedirs
39 from kamaki.clients.pithos import PithosClient, ClientError
41 from kamaki.cli import command
42 from kamaki.cli.command_tree import CommandTree
43 from kamaki.cli.commands import (
44 _command_init, errors, addLogSettings, DontRaiseKeyError, _optional_json,
45 _name_filter, _optional_output_cmd)
46 from kamaki.cli.errors import (
47 CLIBaseUrlError, CLIError, CLIInvalidArgument, raiseCLIError)
48 from kamaki.cli.argument import (
49 FlagArgument, IntArgument, ValueArgument, DateArgument, KeyValueArgument,
50 ProgressBarArgument, RepeatableArgument, DataSizeArgument)
51 from kamaki.cli.utils import (
52 format_size, bold, get_path_size, guess_mime_type)
54 file_cmds = CommandTree('file', 'Pithos+/Storage object level API commands')
55 container_cmds = CommandTree(
56 'container', 'Pithos+/Storage container level API commands')
57 sharers_commands = CommandTree('sharers', 'Pithos+/Storage sharers')
58 _commands = [file_cmds, container_cmds, sharers_commands]
61 class _pithos_init(_command_init):
62 """Initilize a pithos+ client
63 There is always a default account (current user uuid)
64 There is always a default container (pithos)
68 def _custom_container(self):
69 return self.config.get_cloud(self.cloud, 'pithos_container')
72 def _custom_uuid(self):
73 return self.config.get_cloud(self.cloud, 'pithos_uuid')
75 def _set_account(self):
76 self.account = self._custom_uuid()
79 astakos = getattr(self, 'auth_base', None)
81 self.account = astakos.user_term('id', self.token)
83 raise CLIBaseUrlError(service='astakos')
88 cloud = getattr(self, 'cloud', None)
90 self.base_url = self._custom_url('pithos')
92 self.cloud = 'default'
93 self.token = self._custom_token('pithos')
94 self.container = self._custom_container() or 'pithos'
96 astakos = getattr(self, 'auth_base', None)
98 self.token = self.token or astakos.token
100 pithos_endpoints = astakos.get_service_endpoints(
101 self._custom_type('pithos') or 'object-store',
102 self._custom_version('pithos') or '')
103 self.base_url = pithos_endpoints['publicURL']
105 raise CLIBaseUrlError(service='astakos')
108 self.client = PithosClient(
109 self.base_url, self.token, self.account, self.container)
115 class _pithos_account(_pithos_init):
118 def __init__(self, arguments={}, auth_base=None, cloud=None):
119 super(_pithos_account, self).__init__(arguments, auth_base, cloud)
120 self['account'] = ValueArgument(
121 'Use (a different) user uuid', ('-A', '--account'))
123 def print_objects(self, object_list):
124 for index, obj in enumerate(object_list):
125 pretty_obj = obj.copy()
127 empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
130 if self._is_dir(obj):
133 size = format_size(obj['bytes'])
134 pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
135 oname = obj['name'] if self['more'] else bold(obj['name'])
136 prfx = ('%s%s. ' % (empty_space, index)) if self['enum'] else ''
138 self.writeln('%s%s' % (prfx, oname))
139 self.print_dict(pretty_obj, exclude=('name'))
142 oname = '%s%9s %s' % (prfx, size, oname)
143 oname += '/' if self._is_dir(obj) else u''
147 def _is_dir(remote_dict):
148 return 'application/directory' == remote_dict.get(
149 'content_type', remote_dict.get('content-type', ''))
152 super(_pithos_account, self)._run()
153 self.client.account = self['account'] or getattr(
154 self, 'account', getattr(self.client, 'account', None))
157 class _pithos_container(_pithos_account):
158 """Setup container"""
160 def __init__(self, arguments={}, auth_base=None, cloud=None):
161 super(_pithos_container, self).__init__(arguments, auth_base, cloud)
162 self['container'] = ValueArgument(
163 'Use this container (default: pithos)', ('-C', '--container'))
166 def _resolve_pithos_url(url):
167 """Match urls of one of the following formats:
168 pithos://ACCOUNT/CONTAINER/OBJECT_PATH
169 /CONTAINER/OBJECT_PATH
170 return account, container, path
172 account, container, obj_path, prefix = '', '', url, 'pithos://'
173 if url.startswith(prefix):
174 account, sep, url = url[len(prefix):].partition('/')
176 if url.startswith('/'):
177 container, sep, obj_path = url[1:].partition('/')
178 return account, container, obj_path
180 def _run(self, url=None):
181 acc, con, self.path = self._resolve_pithos_url(url or '')
182 self.account = acc or getattr(self, 'account', '')
183 super(_pithos_container, self)._run()
184 self.container = con or self['container'] or getattr(
185 self, 'container', None) or getattr(self.client, 'container', '')
186 self.client.container = self.container
190 class file_info(_pithos_container, _optional_json):
191 """Get information/details about a file"""
194 object_version=ValueArgument(
195 'download a file of a specific version', '--object-version'),
196 hashmap=FlagArgument(
197 'Get file hashmap instead of details', '--hashmap'),
198 matching_etag=ValueArgument(
199 'show output if ETags match', '--if-match'),
200 non_matching_etag=ValueArgument(
201 'show output if ETags DO NOT match', '--if-none-match'),
202 modified_since_date=DateArgument(
203 'show output modified since then', '--if-modified-since'),
204 unmodified_since_date=DateArgument(
205 'show output unmodified since then', '--if-unmodified-since'),
206 sharing=FlagArgument(
207 'show object permissions and sharing information', '--sharing'),
208 metadata=FlagArgument('show only object metadata', '--metadata'),
209 versions=FlagArgument(
210 'show the list of versions for the file', '--object-versions')
213 def version_print(self, versions):
214 return {'/%s/%s' % (self.container, self.path): [
215 dict(version_id=vitem[0], created=strftime(
217 localtime(float(vitem[1])))) for vitem in versions]}
220 @errors.pithos.connection
221 @errors.pithos.container
222 @errors.pithos.object_path
225 r = self.client.get_object_hashmap(
227 version=self['object_version'],
228 if_match=self['matching_etag'],
229 if_none_match=self['non_matching_etag'],
230 if_modified_since=self['modified_since_date'],
231 if_unmodified_since=self['unmodified_since_date'])
232 elif self['sharing']:
233 r = self.client.get_object_sharing(self.path)
234 r['public url'] = self.client.get_object_info(
235 self.path, version=self['object_version']).get(
236 'x-object-public', None)
237 elif self['metadata']:
238 r, preflen = dict(), len('x-object-meta-')
239 for k, v in self.client.get_object_meta(self.path).items():
241 elif self['versions']:
242 r = self.version_print(
243 self.client.get_object_versionlist(self.path))
245 r = self.client.get_object_info(
246 self.path, version=self['object_version'])
247 self._print(r, self.print_dict)
249 def main(self, path_or_url):
250 super(self.__class__, self)._run(path_or_url)
255 class file_list(_pithos_container, _optional_json, _name_filter):
256 """List all objects in a container or a directory object"""
259 detail=FlagArgument('detailed output', ('-l', '--list')),
260 limit=IntArgument('limit number of listed items', ('-n', '--number')),
261 marker=ValueArgument('output greater that marker', '--marker'),
262 delimiter=ValueArgument('show output up to delimiter', '--delimiter'),
264 'show output with specified meta keys', '--meta',
266 if_modified_since=ValueArgument(
267 'show output modified since then', '--if-modified-since'),
268 if_unmodified_since=ValueArgument(
269 'show output not modified since then', '--if-unmodified-since'),
270 until=DateArgument('show metadata until then', '--until'),
271 format=ValueArgument(
272 'format to parse until data (default: d/m/Y H:M:S )', '--format'),
273 shared=FlagArgument('show only shared', '--shared'),
274 more=FlagArgument('read long results', '--more'),
275 enum=FlagArgument('Enumerate results', '--enumerate'),
276 recursive=FlagArgument(
277 'Recursively list containers and their contents',
278 ('-R', '--recursive'))
282 @errors.pithos.connection
283 @errors.pithos.container
284 @errors.pithos.object_path
286 r = self.client.container_get(
287 limit=False if self['more'] else self['limit'],
288 marker=self['marker'],
289 prefix=self['name_pref'],
290 delimiter=self['delimiter'],
291 path=self.path or '',
292 if_modified_since=self['if_modified_since'],
293 if_unmodified_since=self['if_unmodified_since'],
296 show_only_shared=self['shared'])
297 files = self._filter_by_name(r.json)
299 outbu, self._out = self._out, StringIO()
301 if self['json_output'] or self['output_format']:
304 self.print_objects(files)
307 pager(self._out.getvalue())
310 def main(self, path_or_url=''):
311 super(self.__class__, self)._run(path_or_url)
316 class file_modify(_pithos_container):
317 """Modify the attributes of a file or directory object"""
320 publish=FlagArgument(
321 'Make an object public (returns the public URL)', '--publish'),
322 unpublish=FlagArgument(
323 'Make an object unpublic', '--unpublish'),
324 uuid_for_read_permission=RepeatableArgument(
325 'Give read access to user/group (can be repeated, accumulative). '
326 'Format for users: UUID . Format for groups: UUID:GROUP . '
327 'Use * for all users/groups', '--read-permission'),
328 uuid_for_write_permission=RepeatableArgument(
329 'Give write access to user/group (can be repeated, accumulative). '
330 'Format for users: UUID . Format for groups: UUID:GROUP . '
331 'Use * for all users/groups', '--write-permission'),
332 no_permissions=FlagArgument('Remove permissions', '--no-permissions'),
333 metadata_to_set=KeyValueArgument(
334 'Add metadata (KEY=VALUE) to an object (can be repeated)',
336 metadata_key_to_delete=RepeatableArgument(
337 'Delete object metadata (can be repeated)', '--metadata-del'),
340 'publish', 'unpublish', 'uuid_for_read_permission', 'metadata_to_set',
341 'uuid_for_write_permission', 'no_permissions',
342 'metadata_key_to_delete']
345 @errors.pithos.connection
346 @errors.pithos.container
347 @errors.pithos.object_path
350 self.writeln(self.client.publish_object(self.path))
351 if self['unpublish']:
352 self.client.unpublish_object(self.path)
353 if self['uuid_for_read_permission'] or self[
354 'uuid_for_write_permission']:
355 perms = self.client.get_object_sharing(self.path)
356 read, write = perms.get('read', ''), perms.get('write', '')
357 read = read.split(',') if read else []
358 write = write.split(',') if write else []
359 read += self['uuid_for_read_permission']
360 write += self['uuid_for_write_permission']
361 self.client.set_object_sharing(
362 self.path, read_permission=read, write_permission=write)
363 self.print_dict(self.client.get_object_sharing(self.path))
364 if self['no_permissions']:
365 self.client.del_object_sharing(self.path)
366 metadata = self['metadata_to_set'] or dict()
367 for k in self['metadata_key_to_delete']:
370 self.client.set_object_meta(self.path, metadata)
371 self.print_dict(self.client.get_object_meta(self.path))
373 def main(self, path_or_url):
374 super(self.__class__, self)._run(path_or_url)
375 if self['publish'] and self['unpublish']:
376 raise CLIInvalidArgument(
377 'Arguments %s and %s cannot be used together' % (
378 '/'.join(self.arguments['publish'].parsed_name),
379 '/'.join(self.arguments['publish'].parsed_name)))
380 if self['no_permissions'] and (
381 self['uuid_for_read_permission'] or self[
382 'uuid_for_write_permission']):
383 raise CLIInvalidArgument(
384 '%s cannot be used with other permission arguments' % '/'.join(
385 self.arguments['no_permissions'].parsed_name))
390 class file_create(_pithos_container, _optional_output_cmd):
391 """Create an empty file"""
394 content_type=ValueArgument(
395 'Set content type (default: application/octet-stream)',
397 default='application/octet-stream')
401 @errors.pithos.connection
402 @errors.pithos.container
404 self._optional_output(
405 self.client.create_object(self.path, self['content_type']))
407 def main(self, path_or_url):
408 super(self.__class__, self)._run(path_or_url)
413 class file_mkdir(_pithos_container, _optional_output_cmd):
414 """Create a directory: /file create --content-type='applcation/directory'
418 @errors.pithos.connection
419 @errors.pithos.container
421 self._optional_output(self.client.create_directory(self.path))
423 def main(self, path_or_url):
424 super(self.__class__, self)._run(path_or_url)
429 class file_delete(_pithos_container):
430 """Delete a file or directory object"""
433 until_date=DateArgument('remove history until then', '--until'),
434 yes=FlagArgument('Do not prompt for permission', '--yes'),
435 recursive=FlagArgument(
436 'If a directory, empty first', ('-r', '--recursive')),
437 delimiter=ValueArgument(
438 'delete objects prefixed with <object><delimiter>', '--delimiter')
442 @errors.pithos.connection
443 @errors.pithos.container
444 @errors.pithos.object_path
447 if self['yes'] or self.ask_user(
448 'Delete /%s/%s ?' % (self.container, self.path)):
449 self.client.del_object(
451 until=self['until_date'],
452 delimiter='/' if self['recursive'] else self['delimiter'])
454 self.error('Aborted')
456 if self['yes'] or self.ask_user(
457 'Empty container /%s ?' % self.container):
458 self.client.container_delete(self.container, delimiter='/')
460 self.error('Aborted')
462 def main(self, path_or_url):
463 super(self.__class__, self)._run(path_or_url)
467 class _source_destination(_pithos_container, _optional_output_cmd):
470 destination_user_uuid=ValueArgument(
471 'default: current user uuid', '--to-account'),
472 destination_container=ValueArgument(
473 'default: pithos', '--to-container'),
474 source_prefix=FlagArgument(
475 'Transfer all files that are prefixed with SOURCE PATH If the '
476 'destination path is specified, replace SOURCE_PATH with '
478 ('-r', '--recursive')),
480 'Overwrite destination objects, if needed', ('-f', '--force')),
481 source_version=ValueArgument(
482 'The version of the source object', '--source-version')
485 def __init__(self, arguments={}, auth_base=None, cloud=None):
486 self.arguments.update(arguments)
487 self.arguments.update(self.sd_arguments)
488 super(_source_destination, self).__init__(
489 self.arguments, auth_base, cloud)
491 def _report_transfer(self, src, dst, transfer_name):
493 if transfer_name in ('move', ):
494 self.error(' delete source directory %s' % src)
496 dst_prf = '' if self.account == self.dst_client.account else (
497 'pithos://%s' % self.dst_client.account)
499 src_prf = '' if self.account == self.dst_client.account else (
500 'pithos://%s' % self.account)
501 self.error(' %s %s/%s/%s\n --> %s/%s/%s' % (
503 src_prf, self.container, src,
504 dst_prf, self.dst_client.container, dst))
506 self.error(' mkdir %s/%s/%s' % (
507 dst_prf, self.dst_client.container, dst))
510 @errors.pithos.account
511 def _src_dst(self, version=None):
513 self.account, self.container, self.path
514 self.dst_acc, self.dst_con, self.dst_path
515 They should all be configured properly
516 :returns: [(src_path, dst_path), ...], if src_path is None, create
517 destination directory
519 src_objects, dst_objects, pairs = dict(), dict(), []
521 for obj in self.dst_client.list_objects(
522 prefix=self.dst_path or self.path or '/'):
523 dst_objects[obj['name']] = obj
524 except ClientError as ce:
525 if ce.status in (404, ):
527 'Destination container pithos://%s/%s not found' % (
528 self.dst_client.account, self.dst_client.container))
530 if self['source_prefix']:
531 # Copy and replace prefixes
532 for src_obj in self.client.list_objects(prefix=self.path):
533 src_objects[src_obj['name']] = src_obj
534 for src_path, src_obj in src_objects.items():
535 dst_path = '%s%s' % (
536 self.dst_path or self.path, src_path[len(self.path):])
537 dst_obj = dst_objects.get(dst_path, None)
538 if self['force'] or not dst_obj:
541 None if self._is_dir(src_obj) else src_path, dst_path))
542 if self._is_dir(src_obj):
543 pairs.append((self.path or dst_path, None))
544 elif not (self._is_dir(dst_obj) and self._is_dir(src_obj)):
546 'Destination object exists', importance=2, details=[
547 'Failed while transfering:',
548 ' pithos://%s/%s/%s' % (
552 '--> pithos://%s/%s/%s' % (
553 self.dst_client.account,
554 self.dst_client.container,
556 'Use %s to transfer overwrite' % ('/'.join(
557 self.arguments['force'].parsed_name))])
559 # One object transfer
561 src_version_arg = self.arguments.get('source_version', None)
562 src_obj = self.client.get_object_info(
564 version=src_version_arg.value if src_version_arg else None)
565 except ClientError as ce:
566 if ce.status in (204, ):
568 'Missing specific path container %s' % self.container,
569 importance=2, details=[
570 'To transfer container contents %s' % (
571 '/'.join(self.arguments[
572 'source_prefix'].parsed_name))])
574 dst_path = self.dst_path or self.path
575 dst_obj = dst_objects.get(dst_path or self.path, None)
576 if self['force'] or not dst_obj:
578 (None if self._is_dir(src_obj) else self.path, dst_path))
579 if self._is_dir(src_obj):
580 pairs.append((self.path or dst_path, None))
581 elif self._is_dir(src_obj):
583 'Cannot transfer an application/directory object',
584 importance=2, details=[
585 'The object pithos://%s/%s/%s is a directory' % (
589 'To recursively copy a directory, use',
591 self.arguments['source_prefix'].parsed_name)),
592 'To create a file, use',
593 ' /file create (general purpose)',
594 ' /file mkdir (a directory object)'])
597 'Destination object exists',
598 importance=2, details=[
599 'Failed while transfering:',
600 ' pithos://%s/%s/%s' % (
604 '--> pithos://%s/%s/%s' % (
605 self.dst_client.account,
606 self.dst_client.container,
608 'Use %s to transfer overwrite' % ('/'.join(
609 self.arguments['force'].parsed_name))])
612 def _run(self, source_path_or_url, destination_path_or_url=''):
613 super(_source_destination, self)._run(source_path_or_url)
614 dst_acc, dst_con, dst_path = self._resolve_pithos_url(
615 destination_path_or_url)
616 self.dst_client = PithosClient(
617 base_url=self.client.base_url, token=self.client.token,
619 'destination_container'] or dst_con or self.client.container,
621 'destination_user_uuid'] or dst_acc or self.client.account)
622 self.dst_path = dst_path or self.path
626 class file_copy(_source_destination):
627 """Copy objects, even between different accounts or containers"""
630 public=ValueArgument('publish new object', '--public'),
631 content_type=ValueArgument(
632 'change object\'s content type', '--content-type'),
633 source_version=ValueArgument(
634 'The version of the source object', '--object-version')
638 @errors.pithos.connection
639 @errors.pithos.container
640 @errors.pithos.account
642 for src, dst in self._src_dst(self['source_version']):
643 self._report_transfer(src, dst, 'copy')
645 self.dst_client.copy_object(
646 src_container=self.client.container,
648 dst_container=self.dst_client.container,
650 source_account=self.account,
651 source_version=self['source_version'],
652 public=self['public'],
653 content_type=self['content_type'])
655 self.dst_client.create_directory(dst)
657 def main(self, source_path_or_url, destination_path_or_url=None):
658 super(file_copy, self)._run(
659 source_path_or_url, destination_path_or_url or '')
664 class file_move(_source_destination):
665 """Move objects, even between different accounts or containers"""
668 public=ValueArgument('publish new object', '--public'),
669 content_type=ValueArgument(
670 'change object\'s content type', '--content-type')
674 @errors.pithos.connection
675 @errors.pithos.container
676 @errors.pithos.account
678 for src, dst in self._src_dst():
679 self._report_transfer(src, dst, 'move')
681 self.dst_client.move_object(
682 src_container=self.client.container,
684 dst_container=self.dst_client.container,
686 source_account=self.account,
687 public=self['public'],
688 content_type=self['content_type'])
690 self.dst_client.create_directory(dst)
692 self.client.del_object(src)
694 def main(self, source_path_or_url, destination_path_or_url=None):
695 super(file_move, self)._run(
696 source_path_or_url, destination_path_or_url or '')
701 class file_append(_pithos_container, _optional_output_cmd):
702 """Append local file to (existing) remote object
703 The remote object should exist.
704 If the remote object is a directory, it is transformed into a file.
705 In the later case, objects under the directory remain intact.
709 progress_bar=ProgressBarArgument(
710 'do not show progress bar', ('-N', '--no-progress-bar'),
712 max_threads=IntArgument('default: 1', '--threads'),
716 @errors.pithos.connection
717 @errors.pithos.container
718 @errors.pithos.object_path
719 def _run(self, local_path):
720 if self['max_threads'] > 0:
721 self.client.MAX_THREADS = int(self['max_threads'])
722 (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
724 with open(local_path, 'rb') as f:
725 self._optional_output(
726 self.client.append_object(self.path, f, upload_cb))
728 self._safe_progress_bar_finish(progress_bar)
730 def main(self, local_path, remote_path_or_url):
731 super(self.__class__, self)._run(remote_path_or_url)
732 self._run(local_path)
736 class file_truncate(_pithos_container, _optional_output_cmd):
737 """Truncate remote file up to size"""
740 size_in_bytes=IntArgument('Length of file after truncation', '--size')
742 required = ('size_in_bytes', )
745 @errors.pithos.connection
746 @errors.pithos.container
747 @errors.pithos.object_path
748 @errors.pithos.object_size
749 def _run(self, size):
750 self._optional_output(self.client.truncate_object(self.path, size))
752 def main(self, path_or_url):
753 super(self.__class__, self)._run(path_or_url)
754 self._run(size=self['size_in_bytes'])
758 class file_overwrite(_pithos_container, _optional_output_cmd):
759 """Overwrite part of a remote file"""
762 progress_bar=ProgressBarArgument(
763 'do not show progress bar', ('-N', '--no-progress-bar'),
765 start_position=IntArgument('File position in bytes', '--from'),
766 end_position=IntArgument('File position in bytes', '--to')
768 required = ('start_position', 'end_position')
771 @errors.pithos.connection
772 @errors.pithos.container
773 @errors.pithos.object_path
774 @errors.pithos.object_size
775 def _run(self, local_path, start, end):
776 start, end = int(start), int(end)
777 (progress_bar, upload_cb) = self._safe_progress_bar(
778 'Overwrite %s bytes' % (end - start))
780 with open(path.abspath(local_path), 'rb') as f:
781 self._optional_output(self.client.overwrite_object(
786 upload_cb=upload_cb))
788 self._safe_progress_bar_finish(progress_bar)
790 def main(self, local_path, path_or_url):
791 super(self.__class__, self)._run(path_or_url)
792 self.path = self.path or path.basename(local_path)
794 local_path=local_path,
795 start=self['start_position'],
796 end=self['end_position'])
800 class file_upload(_pithos_container, _optional_output_cmd):
804 max_threads=IntArgument('default: 5', '--threads'),
805 content_encoding=ValueArgument(
806 'set MIME content type', '--content-encoding'),
807 content_disposition=ValueArgument(
808 'specify objects presentation style', '--content-disposition'),
809 content_type=ValueArgument('specify content type', '--content-type'),
810 uuid_for_read_permission=RepeatableArgument(
811 'Give read access to a user or group (can be repeated) '
812 'Use * for all users',
813 '--read-permission'),
814 uuid_for_write_permission=RepeatableArgument(
815 'Give write access to a user or group (can be repeated) '
816 'Use * for all users',
817 '--write-permission'),
818 public=FlagArgument('make object publicly accessible', '--public'),
819 overwrite=FlagArgument('Force (over)write', ('-f', '--force')),
820 recursive=FlagArgument(
821 'Recursively upload directory *contents* + subdirectories',
822 ('-r', '--recursive')),
823 unchunked=FlagArgument(
824 'Upload file as one block (not recommended)', '--unchunked'),
825 md5_checksum=ValueArgument(
826 'Confirm upload with a custom checksum (MD5)', '--etag'),
827 use_hashes=FlagArgument(
828 'Source file contains hashmap not data', '--source-is-hashmap'),
833 readlist = self['uuid_for_read_permission']
835 sharing['read'] = self['uuid_for_read_permission']
836 writelist = self['uuid_for_write_permission']
838 sharing['write'] = self['uuid_for_write_permission']
839 return sharing or None
841 def _check_container_limit(self, path):
842 cl_dict = self.client.get_container_limit()
843 container_limit = int(cl_dict['x-container-policy-quota'])
844 r = self.client.container_get()
845 used_bytes = sum(int(o['bytes']) for o in r.json)
846 path_size = get_path_size(path)
847 if container_limit and path_size > (container_limit - used_bytes):
849 'Container %s (limit(%s) - used(%s)) < (size(%s) of %s)' % (
850 self.client.container,
851 format_size(container_limit),
852 format_size(used_bytes),
853 format_size(path_size),
856 'Check accound limit: /file quota',
857 'Check container limit:',
858 '\t/file containerlimit get %s' % self.client.container,
859 'Increase container limit:',
860 '\t/file containerlimit set <new limit> %s' % (
861 self.client.container)])
863 def _src_dst(self, local_path, remote_path, objlist=None):
864 lpath = path.abspath(local_path)
865 short_path = path.basename(path.abspath(local_path))
866 rpath = remote_path or short_path
867 if path.isdir(lpath):
868 if not self['recursive']:
869 raise CLIError('%s is a directory' % lpath, details=[
870 'Use %s to upload directories & contents' % '/'.join(
871 self.arguments['recursive'].parsed_name)])
872 robj = self.client.container_get(path=rpath)
873 if not self['overwrite']:
876 'Objects/files prefixed as %s already exist' % rpath,
877 details=['Existing objects:'] + ['\t/%s/\t%s' % (
879 o['content_type'][12:]) for o in robj.json] + [
880 'Use -f to add, overwrite or resume'])
883 topobj = self.client.get_object_info(rpath)
884 if not self._is_dir(topobj):
886 'Object /%s/%s exists but not a directory' % (
887 self.container, rpath),
888 details=['Use -f to overwrite'])
889 except ClientError as ce:
890 if ce.status not in (404, ):
892 self._check_container_limit(lpath)
894 for top, subdirs, files in walk(lpath):
898 rel_path = rpath + top.split(lpath)[1]
901 self.error('mkdir /%s/%s' % (
902 self.client.container, rel_path))
903 self.client.create_directory(rel_path)
905 fpath = path.join(top, f)
906 if path.isfile(fpath):
907 rel_path = rel_path.replace(path.sep, '/')
908 pathfix = f.replace(path.sep, '/')
909 yield open(fpath, 'rb'), '%s/%s' % (rel_path, pathfix)
911 self.error('%s is not a regular file' % fpath)
913 if not path.isfile(lpath):
914 raise CLIError(('%s is not a regular file' % lpath) if (
915 path.exists(lpath)) else '%s does not exist' % lpath)
917 robj = self.client.get_object_info(rpath)
918 if remote_path and self._is_dir(robj):
919 rpath += '/%s' % (short_path.replace(path.sep, '/'))
920 self.client.get_object_info(rpath)
921 if not self['overwrite']:
923 'Object /%s/%s already exists' % (
924 self.container, rpath),
925 details=['use -f to overwrite / resume'])
926 except ClientError as ce:
927 if ce.status not in (404, ):
929 self._check_container_limit(lpath)
930 yield open(lpath, 'rb'), rpath
932 def _run(self, local_path, remote_path):
933 if self['max_threads'] > 0:
934 self.client.MAX_THREADS = int(self['max_threads'])
936 content_encoding=self['content_encoding'],
937 content_type=self['content_type'],
938 content_disposition=self['content_disposition'],
939 sharing=self._sharing(),
940 public=self['public'])
941 uploaded, container_info_cache = list, dict()
942 rpref = 'pithos://%s' if self['account'] else ''
943 for f, rpath in self._src_dst(local_path, remote_path):
944 self.error('%s --> %s/%s/%s' % (
945 f.name, rpref, self.client.container, rpath))
946 if not (self['content_type'] and self['content_encoding']):
947 ctype, cenc = guess_mime_type(f.name)
948 params['content_type'] = self['content_type'] or ctype
949 params['content_encoding'] = self['content_encoding'] or cenc
950 if self['unchunked']:
951 r = self.client.upload_object_unchunked(
953 etag=self['md5_checksum'], withHashFile=self['use_hashes'],
955 if self['with_output'] or self['json_output']:
956 r['name'] = '/%s/%s' % (self.client.container, rpath)
960 (progress_bar, upload_cb) = self._safe_progress_bar(
961 'Uploading %s' % f.name.split(path.sep)[-1])
963 hash_bar = progress_bar.clone()
964 hash_cb = hash_bar.get_generator(
965 'Calculating block hashes')
968 r = self.client.upload_object(
972 container_info_cache=container_info_cache,
974 if self['with_output'] or self['json_output']:
975 r['name'] = '/%s/%s' % (self.client.container, rpath)
978 self._safe_progress_bar_finish(progress_bar)
981 self._safe_progress_bar_finish(progress_bar)
982 self._optional_output(uploaded)
983 self.error('Upload completed')
985 def main(self, local_path, remote_path_or_url):
986 super(self.__class__, self)._run(remote_path_or_url)
987 remote_path = self.path or path.basename(path.abspath(local_path))
988 self._run(local_path=local_path, remote_path=remote_path)
991 class RangeArgument(ValueArgument):
993 :value type: string of the form <start>-<end> where <start> and <end> are
995 :value returns: the input string, after type checking <start> and <end>
1000 return getattr(self, '_value', self.default)
1003 def value(self, newvalues):
1005 self._value = getattr(self, '_value', self.default)
1006 for newvalue in newvalues.split(','):
1007 self._value = ('%s,' % self._value) if self._value else ''
1008 start, sep, end = newvalue.partition('-')
1011 start, end = (int(start), int(end))
1013 raise CLIInvalidArgument(
1014 'Invalid range %s' % newvalue, details=[
1015 'Valid range formats',
1016 ' START-END', ' UP_TO', ' -FROM',
1017 'where all values are integers'])
1018 self._value += '%s-%s' % (start, end)
1020 self._value += '-%s' % int(end)
1022 self._value += '%s' % int(start)
1026 class file_cat(_pithos_container):
1027 """Fetch remote file contents"""
1030 range=RangeArgument('show range of data', '--range'),
1031 if_match=ValueArgument('show output if ETags match', '--if-match'),
1032 if_none_match=ValueArgument(
1033 'show output if ETags match', '--if-none-match'),
1034 if_modified_since=DateArgument(
1035 'show output modified since then', '--if-modified-since'),
1036 if_unmodified_since=DateArgument(
1037 'show output unmodified since then', '--if-unmodified-since'),
1038 object_version=ValueArgument(
1039 'Get contents of the chosen version', '--object-version')
1043 @errors.pithos.connection
1044 @errors.pithos.container
1045 @errors.pithos.object_path
1047 self.client.download_object(
1048 self.path, self._out,
1049 range_str=self['range'],
1050 version=self['object_version'],
1051 if_match=self['if_match'],
1052 if_none_match=self['if_none_match'],
1053 if_modified_since=self['if_modified_since'],
1054 if_unmodified_since=self['if_unmodified_since'])
1056 def main(self, path_or_url):
1057 super(self.__class__, self)._run(path_or_url)
1062 class file_download(_pithos_container):
1063 """Download a remove file or directory object to local file system"""
1066 resume=FlagArgument(
1067 'Resume/Overwrite (attempt resume, else overwrite)',
1068 ('-f', '--resume')),
1069 range=RangeArgument('Download only that range of data', '--range'),
1070 matching_etag=ValueArgument('download iff ETag match', '--if-match'),
1071 non_matching_etag=ValueArgument(
1072 'download iff ETags DO NOT match', '--if-none-match'),
1073 modified_since_date=DateArgument(
1074 'download iff remote file is modified since then',
1075 '--if-modified-since'),
1076 unmodified_since_date=DateArgument(
1077 'show output iff remote file is unmodified since then',
1078 '--if-unmodified-since'),
1079 object_version=ValueArgument(
1080 'download a file of a specific version', '--object-version'),
1081 max_threads=IntArgument('default: 5', '--threads'),
1082 progress_bar=ProgressBarArgument(
1083 'do not show progress bar', ('-N', '--no-progress-bar'),
1085 recursive=FlagArgument(
1086 'Download a remote directory object and its contents',
1087 ('-r', '--recursive'))
1090 def _src_dst(self, local_path):
1091 """Create a list of (src, dst) where src is a remote location and dst
1092 is an open file descriptor. Directories are denoted as (None, dirpath)
1093 and they are pretended to other objects in a very strict order (shorter
1098 obj = self.client.get_object_info(
1099 self.path, version=self['object_version'])
1100 obj.setdefault('name', self.path.strip('/'))
1103 except ClientError as ce:
1104 if ce.status in (404, ):
1105 raiseCLIError(ce, details=[
1106 'To download an object, it must exist either as a file or'
1108 'For example, to download everything under prefix/ the '
1109 'directory "prefix" must exist.',
1110 'To see if an remote object is actually there:',
1111 ' /file info [/CONTAINER/]OBJECT',
1112 'To create a directory object:',
1113 ' /file mkdir [/CONTAINER/]OBJECT'])
1114 if ce.status in (204, ):
1116 'No file or directory objects to download',
1118 'To download a container (e.g., %s):' % self.container,
1119 ' [kamaki] container download %s [LOCAL_PATH]' % (
1122 rpath = self.path.strip('/')
1123 if local_path and self.path and local_path.endswith('/'):
1124 local_path = local_path[-1:]
1126 if (not obj) or self._is_dir(obj):
1127 if self['recursive']:
1128 if not (self.path or local_path.endswith('/')):
1129 # Download the whole container
1130 local_path = '' if local_path in ('.', ) else local_path
1131 local_path = '%s/' % (local_path or self.container)
1133 name='', content_type='application/directory')
1134 dirs, files = [obj, ], []
1135 objects = self.client.container_get(
1137 if_modified_since=self['modified_since_date'],
1138 if_unmodified_since=self['unmodified_since_date'])
1139 for o in objects.json:
1140 (dirs if self._is_dir(o) else files).append(o)
1142 # Put the directories on top of the list
1143 for dpath in sorted(['%s%s' % (
1144 local_path, d['name'][len(rpath):]) for d in dirs]):
1145 if path.exists(dpath):
1146 if path.isdir(dpath):
1149 'Cannot replace local file %s with a directory '
1150 'of the same name' % dpath,
1152 'Either remove the file or specify a'
1153 'different target location'])
1154 ret.append((None, dpath, None))
1156 # Append the file objects
1157 for opath in [o['name'] for o in files]:
1158 lpath = '%s%s' % (local_path, opath[len(rpath):])
1160 fxists = path.exists(lpath)
1161 if fxists and path.isdir(lpath):
1163 'Cannot change local dir %s info file' % (
1166 'Either remove the file or specify a'
1167 'different target location'])
1168 ret.append((opath, lpath, fxists))
1169 elif path.exists(lpath):
1171 'Cannot overwrite %s' % lpath,
1172 details=['To overwrite/resume, use %s' % '/'.join(
1173 self.arguments['resume'].parsed_name)])
1175 ret.append((opath, lpath, None))
1178 'Remote object /%s/%s is a directory' % (
1179 self.container, local_path),
1180 details=['Use %s to download directories' % '/'.join(
1181 self.arguments['recursive'].parsed_name)])
1183 parsed_name = '/'.join(self.arguments['recursive'].parsed_name)
1185 'Cannot download container %s' % self.container,
1187 'Use %s to download containers' % parsed_name,
1188 ' [kamaki] file download %s /%s [LOCAL_PATH]' % (
1189 parsed_name, self.container)])
1191 # Remote object is just a file
1192 if path.exists(local_path) and not self['resume']:
1194 'Cannot overwrite local file %s' % (lpath),
1195 details=['To overwrite/resume, use %s' % '/'.join(
1196 self.arguments['resume'].parsed_name)])
1197 ret.append((rpath, local_path, self['resume']))
1198 for r, l, resume in ret:
1200 with open(l, 'rwb+' if resume else 'wb+') as f:
1206 @errors.pithos.connection
1207 @errors.pithos.container
1208 @errors.pithos.object_path
1209 @errors.pithos.local_path
1210 @errors.pithos.local_path_download
1211 def _run(self, local_path):
1212 self.client.MAX_THREADS = self['max_threads'] or 5
1215 for rpath, output_file in self._src_dst(local_path):
1217 self.error('Create local directory %s' % output_file)
1218 makedirs(output_file)
1220 self.error('/%s/%s --> %s' % (
1221 self.container, rpath, output_file.name))
1222 progress_bar, download_cb = self._safe_progress_bar(
1224 self.client.download_object(
1226 download_cb=download_cb,
1227 range_str=self['range'],
1228 version=self['object_version'],
1229 if_match=self['matching_etag'],
1230 resume=self['resume'],
1231 if_none_match=self['non_matching_etag'],
1232 if_modified_since=self['modified_since_date'],
1233 if_unmodified_since=self['unmodified_since_date'])
1234 except KeyboardInterrupt:
1235 from threading import activeCount, enumerate as activethreads
1237 while activeCount() > 1:
1238 self._out.write('\nCancel %s threads: ' % (activeCount() - 1))
1240 for thread in activethreads():
1242 thread.join(timeout)
1243 self._out.write('.' if thread.isAlive() else '*')
1244 except RuntimeError:
1249 self.error('\nDownload canceled by user')
1250 if local_path is not None:
1251 self.error('to resume, re-run with --resume')
1253 self._safe_progress_bar_finish(progress_bar)
1256 self._safe_progress_bar_finish(progress_bar)
1258 def main(self, remote_path_or_url, local_path=None):
1259 super(self.__class__, self)._run(remote_path_or_url)
1260 local_path = local_path or self.path or '.'
1261 self._run(local_path=local_path)
1264 @command(container_cmds)
1265 class container_info(_pithos_account, _optional_json):
1266 """Get information about a container"""
1269 until_date=DateArgument('show metadata until then', '--until'),
1270 metadata=FlagArgument('Show only container metadata', '--metadata'),
1271 sizelimit=FlagArgument(
1272 'Show the maximum size limit for container', '--size-limit'),
1273 in_bytes=FlagArgument('Show size limit in bytes', ('-b', '--bytes'))
1277 @errors.pithos.connection
1278 @errors.pithos.container
1279 @errors.pithos.object_path
1281 if self['metadata']:
1282 r, preflen = dict(), len('x-container-meta-')
1283 for k, v in self.client.get_container_meta(
1284 until=self['until_date']).items():
1286 elif self['sizelimit']:
1287 r = self.client.get_container_limit(
1288 self.container)['x-container-policy-quota']
1289 r = {'size limit': 'unlimited' if r in ('0', ) else (
1290 int(r) if self['in_bytes'] else format_size(r))}
1292 r = self.client.get_container_info(self.container)
1293 self._print(r, self.print_dict)
1295 def main(self, container):
1296 super(self.__class__, self)._run()
1297 self.container, self.client.container = container, container
1301 class VersioningArgument(ValueArgument):
1303 schemes = ('auto', 'none')
1307 return getattr(self, '_value', None)
1310 def value(self, new_scheme):
1312 new_scheme = new_scheme.lower()
1313 if new_scheme not in self.schemes:
1314 raise CLIInvalidArgument('Invalid versioning value', details=[
1315 'Valid versioning values are %s' % ', '.join(
1317 self._value = new_scheme
1320 @command(container_cmds)
1321 class container_modify(_pithos_account, _optional_json):
1322 """Modify the properties of a container"""
1325 metadata_to_add=KeyValueArgument(
1326 'Add metadata in the form KEY=VALUE (can be repeated)',
1328 metadata_to_delete=RepeatableArgument(
1329 'Delete metadata by KEY (can be repeated)', '--metadata-del'),
1330 sizelimit=DataSizeArgument(
1331 'Set max size limit (0 for unlimited, '
1332 'use units B, KiB, KB, etc.)', '--size-limit'),
1333 versioning=VersioningArgument(
1334 'Set a versioning scheme (%s)' % ', '.join(
1335 VersioningArgument.schemes), '--versioning')
1337 required = ['metadata_to_add', 'metadata_to_delete', 'sizelimit']
1340 @errors.pithos.connection
1341 @errors.pithos.container
1342 def _run(self, container):
1343 metadata = self['metadata_to_add']
1344 for k in self['metadata_to_delete']:
1347 self.client.set_container_meta(metadata)
1348 self._print(self.client.get_container_meta(), self.print_dict)
1349 if self['sizelimit'] is not None:
1350 self.client.set_container_limit(self['sizelimit'])
1351 r = self.client.get_container_limit()['x-container-policy-quota']
1352 r = 'unlimited' if r in ('0', ) else format_size(r)
1353 self.writeln('new size limit: %s' % r)
1354 if self['versioning']:
1355 self.client.set_container_versioning(self['versioning'])
1356 self.writeln('new versioning scheme: %s' % (
1357 self.client.get_container_versioning(self.container)[
1358 'x-container-policy-versioning']))
1360 def main(self, container):
1361 super(self.__class__, self)._run()
1362 self.client.container, self.container = container, container
1363 self._run(container=container)
1366 @command(container_cmds)
1367 class container_list(_pithos_account, _optional_json, _name_filter):
1368 """List all containers, or their contents"""
1371 detail=FlagArgument('Containers with details', ('-l', '--list')),
1372 limit=IntArgument('limit number of listed items', ('-n', '--number')),
1373 marker=ValueArgument('output greater that marker', '--marker'),
1374 modified_since_date=ValueArgument(
1375 'show output modified since then', '--if-modified-since'),
1376 unmodified_since_date=ValueArgument(
1377 'show output not modified since then', '--if-unmodified-since'),
1378 until_date=DateArgument('show metadata until then', '--until'),
1379 shared=FlagArgument('show only shared', '--shared'),
1380 more=FlagArgument('read long results', '--more'),
1381 enum=FlagArgument('Enumerate results', '--enumerate'),
1382 recursive=FlagArgument(
1383 'Recursively list containers and their contents',
1384 ('-r', '--recursive'))
1387 def print_containers(self, container_list):
1388 for index, container in enumerate(container_list):
1389 if 'bytes' in container:
1390 size = format_size(container['bytes'])
1391 prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
1392 _cname = container['name'] if (
1393 self['more']) else bold(container['name'])
1394 cname = u'%s%s' % (prfx, _cname)
1397 pretty_c = container.copy()
1398 if 'bytes' in container:
1399 pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
1400 self.print_dict(pretty_c, exclude=('name'))
1403 if 'count' in container and 'bytes' in container:
1404 self.writeln('%s (%s, %s objects)' % (
1405 cname, size, container['count']))
1408 objects = container.get('objects', [])
1410 self.print_objects(objects)
1413 def _create_object_forest(self, container_list):
1415 for container in container_list:
1416 self.client.container = container['name']
1417 objects = self.client.container_get(
1418 limit=False if self['more'] else self['limit'],
1419 if_modified_since=self['modified_since_date'],
1420 if_unmodified_since=self['unmodified_since_date'],
1421 until=self['until_date'],
1422 show_only_shared=self['shared'])
1423 container['objects'] = objects.json
1425 self.client.container = None
1428 @errors.pithos.connection
1429 @errors.pithos.object_path
1430 @errors.pithos.container
1431 def _run(self, container):
1433 r = self.client.container_get(
1434 limit=False if self['more'] else self['limit'],
1435 marker=self['marker'],
1436 if_modified_since=self['modified_since_date'],
1437 if_unmodified_since=self['unmodified_since_date'],
1438 until=self['until_date'],
1439 show_only_shared=self['shared'])
1441 r = self.client.account_get(
1442 limit=False if self['more'] else self['limit'],
1443 marker=self['marker'],
1444 if_modified_since=self['modified_since_date'],
1445 if_unmodified_since=self['unmodified_since_date'],
1446 until=self['until_date'],
1447 show_only_shared=self['shared'])
1448 files = self._filter_by_name(r.json)
1449 if self['recursive'] and not container:
1450 self._create_object_forest(files)
1452 outbu, self._out = self._out, StringIO()
1454 if self['json_output'] or self['output_format']:
1457 (self.print_objects if container else self.print_containers)(
1461 pager(self._out.getvalue())
1464 def main(self, container=None):
1465 super(self.__class__, self)._run()
1466 self.client.container, self.container = container, container
1467 self._run(container)
1470 @command(container_cmds)
1471 class container_create(_pithos_account):
1472 """Create a new container"""
1475 versioning=ValueArgument(
1476 'set container versioning (auto/none)', '--versioning'),
1477 limit=IntArgument('set default container limit', '--limit'),
1478 meta=KeyValueArgument(
1479 'set container metadata (can be repeated)', '--meta')
1483 @errors.pithos.connection
1484 @errors.pithos.container
1485 def _run(self, container):
1487 self.client.create_container(
1488 container=container,
1489 sizelimit=self['limit'],
1490 versioning=self['versioning'],
1491 metadata=self['meta'],
1493 except ClientError as ce:
1494 if ce.status in (202, ):
1496 'Container %s alread exists' % container, details=[
1497 'Either delete %s or choose another name' % (container)])
1500 def main(self, new_container):
1501 super(self.__class__, self)._run()
1502 self._run(container=new_container)
1505 @command(container_cmds)
1506 class container_delete(_pithos_account):
1507 """Delete a container"""
1510 yes=FlagArgument('Do not prompt for permission', '--yes'),
1511 recursive=FlagArgument(
1512 'delete container even if not empty', ('-r', '--recursive'))
1516 @errors.pithos.connection
1517 @errors.pithos.container
1518 def _run(self, container):
1519 num_of_contents = int(self.client.get_container_info(container)[
1520 'x-container-object-count'])
1521 delimiter, msg = None, 'Delete container %s ?' % container
1522 if self['recursive']:
1523 delimiter, msg = '/', 'Empty and d%s' % msg[1:]
1524 elif num_of_contents:
1525 raise CLIError('Container %s is not empty' % container, details=[
1526 'Use %s to delete non-empty containers' % '/'.join(
1527 self.arguments['recursive'].parsed_name)])
1528 if self['yes'] or self.ask_user(msg):
1530 self.client.del_container(delimiter=delimiter)
1531 self.client.purge_container()
1533 def main(self, container):
1534 super(self.__class__, self)._run()
1535 self.container, self.client.container = container, container
1536 self._run(container)
1539 @command(container_cmds)
1540 class container_empty(_pithos_account):
1541 """Empty a container"""
1543 arguments = dict(yes=FlagArgument('Do not prompt for permission', '--yes'))
1546 @errors.pithos.connection
1547 @errors.pithos.container
1548 def _run(self, container):
1549 if self['yes'] or self.ask_user('Empty container %s ?' % container):
1550 self.client.del_container(delimiter='/')
1552 def main(self, container):
1553 super(self.__class__, self)._run()
1554 self.container, self.client.container = container, container
1555 self._run(container)