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,
49 from kamaki.cli.argument import (
50 FlagArgument, IntArgument, ValueArgument, DateArgument, KeyValueArgument,
51 ProgressBarArgument, RepeatableArgument, DataSizeArgument)
52 from kamaki.cli.utils import (
53 format_size, bold, get_path_size, guess_mime_type)
55 file_cmds = CommandTree('file', 'Pithos+/Storage object level API commands')
56 container_cmds = CommandTree(
57 'container', 'Pithos+/Storage container level API commands')
58 sharer_cmds = CommandTree('sharer', 'Pithos+/Storage sharers')
59 group_cmds = CommandTree('group', 'Pithos+/Storage user groups')
60 _commands = [file_cmds, container_cmds, sharer_cmds, group_cmds]
63 class _pithos_init(_command_init):
64 """Initilize a pithos+ client
65 There is always a default account (current user uuid)
66 There is always a default container (pithos)
70 def _custom_container(self):
71 return self.config.get_cloud(self.cloud, 'pithos_container')
74 def _custom_uuid(self):
75 return self.config.get_cloud(self.cloud, 'pithos_uuid')
77 def _set_account(self):
78 self.account = self._custom_uuid()
81 astakos = getattr(self, 'auth_base', None)
83 self.account = astakos.user_term('id', self.token)
85 raise CLIBaseUrlError(service='astakos')
90 cloud = getattr(self, 'cloud', None)
92 self.base_url = self._custom_url('pithos')
94 self.cloud = 'default'
95 self.token = self._custom_token('pithos')
96 self.container = self._custom_container() or 'pithos'
98 astakos = getattr(self, 'auth_base', None)
100 self.token = self.token or astakos.token
101 if not self.base_url:
102 pithos_endpoints = astakos.get_service_endpoints(
103 self._custom_type('pithos') or 'object-store',
104 self._custom_version('pithos') or '')
105 self.base_url = pithos_endpoints['publicURL']
107 raise CLIBaseUrlError(service='astakos')
110 self.client = PithosClient(
111 self.base_url, self.token, self.account, self.container)
117 class _pithos_account(_pithos_init):
120 def __init__(self, arguments={}, auth_base=None, cloud=None):
121 super(_pithos_account, self).__init__(arguments, auth_base, cloud)
122 self['account'] = ValueArgument(
123 'Use (a different) user uuid', ('-A', '--account'))
125 def print_objects(self, object_list):
126 for index, obj in enumerate(object_list):
127 pretty_obj = obj.copy()
129 empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
132 if self._is_dir(obj):
135 size = format_size(obj['bytes'])
136 pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
137 oname = obj['name'] if self['more'] else bold(obj['name'])
138 prfx = ('%s%s. ' % (empty_space, index)) if self['enum'] else ''
140 self.writeln('%s%s' % (prfx, oname))
141 self.print_dict(pretty_obj, exclude=('name'))
144 oname = '%s%9s %s' % (prfx, size, oname)
145 oname += '/' if self._is_dir(obj) else u''
149 def _is_dir(remote_dict):
150 return 'application/directory' in remote_dict.get(
151 'content_type', remote_dict.get('content-type', ''))
154 super(_pithos_account, self)._run()
155 self.client.account = self['account'] or getattr(
156 self, 'account', getattr(self.client, 'account', None))
159 class _pithos_container(_pithos_account):
160 """Setup container"""
162 def __init__(self, arguments={}, auth_base=None, cloud=None):
163 super(_pithos_container, self).__init__(arguments, auth_base, cloud)
164 self['container'] = ValueArgument(
165 'Use this container (default: pithos)', ('-C', '--container'))
168 def _resolve_pithos_url(url):
169 """Match urls of one of the following formats:
170 pithos://ACCOUNT/CONTAINER/OBJECT_PATH
171 /CONTAINER/OBJECT_PATH
172 return account, container, path
174 account, container, obj_path, prefix = '', '', url, 'pithos://'
175 if url.startswith(prefix):
176 account, sep, url = url[len(prefix):].partition('/')
178 if url.startswith('/'):
179 container, sep, obj_path = url[1:].partition('/')
180 return account, container, obj_path
182 def _run(self, url=None):
183 acc, con, self.path = self._resolve_pithos_url(url or '')
184 # self.account = acc or getattr(self, 'account', '')
185 super(_pithos_container, self)._run()
186 self.container = con or self['container'] or getattr(
187 self, 'container', None) or getattr(self.client, 'container', '')
188 self.client.account = acc or self.client.account
189 self.client.container = self.container
193 class file_info(_pithos_container, _optional_json):
194 """Get information/details about a file"""
197 object_version=ValueArgument(
198 'download a file of a specific version', '--object-version'),
199 hashmap=FlagArgument(
200 'Get file hashmap instead of details', '--hashmap'),
201 matching_etag=ValueArgument(
202 'show output if ETags match', '--if-match'),
203 non_matching_etag=ValueArgument(
204 'show output if ETags DO NOT match', '--if-none-match'),
205 modified_since_date=DateArgument(
206 'show output modified since then', '--if-modified-since'),
207 unmodified_since_date=DateArgument(
208 'show output unmodified since then', '--if-unmodified-since'),
209 sharing=FlagArgument(
210 'show object permissions and sharing information', '--sharing'),
211 metadata=FlagArgument('show only object metadata', '--metadata'),
212 versions=FlagArgument(
213 'show the list of versions for the file', '--object-versions')
216 def version_print(self, versions):
217 return {'/%s/%s' % (self.container, self.path): [
218 dict(version_id=vitem[0], created=strftime(
220 localtime(float(vitem[1])))) for vitem in versions]}
223 @errors.pithos.connection
224 @errors.pithos.container
225 @errors.pithos.object_path
228 r = self.client.get_object_hashmap(
230 version=self['object_version'],
231 if_match=self['matching_etag'],
232 if_none_match=self['non_matching_etag'],
233 if_modified_since=self['modified_since_date'],
234 if_unmodified_since=self['unmodified_since_date'])
235 elif self['sharing']:
236 r = self.client.get_object_sharing(self.path)
237 r['public url'] = self.client.get_object_info(
238 self.path, version=self['object_version']).get(
239 'x-object-public', None)
240 elif self['metadata']:
241 r, preflen = dict(), len('x-object-meta-')
242 for k, v in self.client.get_object_meta(self.path).items():
244 elif self['versions']:
245 r = self.version_print(
246 self.client.get_object_versionlist(self.path))
248 r = self.client.get_object_info(
249 self.path, version=self['object_version'])
250 self._print(r, self.print_dict)
252 def main(self, path_or_url):
253 super(self.__class__, self)._run(path_or_url)
258 class file_list(_pithos_container, _optional_json, _name_filter):
259 """List all objects in a container or a directory object"""
262 detail=FlagArgument('detailed output', ('-l', '--list')),
263 limit=IntArgument('limit number of listed items', ('-n', '--number')),
264 marker=ValueArgument('output greater that marker', '--marker'),
265 delimiter=ValueArgument('show output up to delimiter', '--delimiter'),
267 'show output with specified meta keys', '--meta',
269 if_modified_since=ValueArgument(
270 'show output modified since then', '--if-modified-since'),
271 if_unmodified_since=ValueArgument(
272 'show output not modified since then', '--if-unmodified-since'),
273 until=DateArgument('show metadata until then', '--until'),
274 format=ValueArgument(
275 'format to parse until data (default: d/m/Y H:M:S )', '--format'),
276 shared_by_me=FlagArgument(
277 'show only files shared to other users', '--shared-by-me'),
278 public=FlagArgument('show only published objects', '--public'),
279 more=FlagArgument('read long results', '--more'),
280 enum=FlagArgument('Enumerate results', '--enumerate'),
281 recursive=FlagArgument(
282 'Recursively list containers and their contents',
283 ('-R', '--recursive'))
287 @errors.pithos.connection
288 @errors.pithos.container
289 @errors.pithos.object_path
291 r = self.client.container_get(
292 limit=False if self['more'] else self['limit'],
293 marker=self['marker'],
294 prefix=self['name_pref'],
295 delimiter=self['delimiter'],
296 path=self.path or '',
297 show_only_shared=self['shared_by_me'],
298 public=self['public'],
299 if_modified_since=self['if_modified_since'],
300 if_unmodified_since=self['if_unmodified_since'],
304 # REMOVE THIS if version >> 0.12
306 self.error(' NOTE: Since v0.12, use / for containers e.g.,')
307 self.error(' [kamaki] file list /pithos')
309 files = self._filter_by_name(r.json)
311 outbu, self._out = self._out, StringIO()
313 if self['json_output'] or self['output_format']:
316 self.print_objects(files)
319 pager(self._out.getvalue())
322 def main(self, path_or_url=''):
323 super(self.__class__, self)._run(path_or_url)
328 class file_modify(_pithos_container):
329 """Modify the attributes of a file or directory object"""
332 publish=FlagArgument(
333 'Make an object public (returns the public URL)', '--publish'),
334 unpublish=FlagArgument(
335 'Make an object unpublic', '--unpublish'),
336 uuid_for_read_permission=RepeatableArgument(
337 'Give read access to user/group (can be repeated, accumulative). '
338 'Format for users: UUID . Format for groups: UUID:GROUP . '
339 'Use * for all users/groups', '--read-permission'),
340 uuid_for_write_permission=RepeatableArgument(
341 'Give write access to user/group (can be repeated, accumulative). '
342 'Format for users: UUID . Format for groups: UUID:GROUP . '
343 'Use * for all users/groups', '--write-permission'),
344 no_permissions=FlagArgument('Remove permissions', '--no-permissions'),
345 metadata_to_set=KeyValueArgument(
346 'Add metadata (KEY=VALUE) to an object (can be repeated)',
348 metadata_key_to_delete=RepeatableArgument(
349 'Delete object metadata (can be repeated)', '--metadata-del'),
352 'publish', 'unpublish', 'uuid_for_read_permission', 'metadata_to_set',
353 'uuid_for_write_permission', 'no_permissions',
354 'metadata_key_to_delete']
357 @errors.pithos.connection
358 @errors.pithos.container
359 @errors.pithos.object_path
362 self.writeln(self.client.publish_object(self.path))
363 if self['unpublish']:
364 self.client.unpublish_object(self.path)
365 if self['uuid_for_read_permission'] or self[
366 'uuid_for_write_permission']:
367 perms = self.client.get_object_sharing(self.path)
368 read, write = perms.get('read', ''), perms.get('write', '')
369 read = read.split(',') if read else []
370 write = write.split(',') if write else []
371 read += (self['uuid_for_read_permission'] or [])
372 write += (self['uuid_for_write_permission'] or [])
373 self.client.set_object_sharing(
374 self.path, read_permission=read, write_permission=write)
375 self.print_dict(self.client.get_object_sharing(self.path))
376 if self['no_permissions']:
377 self.client.del_object_sharing(self.path)
378 metadata = self['metadata_to_set'] or dict()
379 for k in (self['metadata_key_to_delete'] or []):
382 self.client.set_object_meta(self.path, metadata)
383 self.print_dict(self.client.get_object_meta(self.path))
385 def main(self, path_or_url):
386 super(self.__class__, self)._run(path_or_url)
387 if self['publish'] and self['unpublish']:
388 raise CLIInvalidArgument(
389 'Arguments %s and %s cannot be used together' % (
390 self.arguments['publish'].lvalue,
391 self.arguments['publish'].lvalue))
392 if self['no_permissions'] and (
393 self['uuid_for_read_permission'] or self[
394 'uuid_for_write_permission']):
395 raise CLIInvalidArgument(
396 '%s cannot be used with other permission arguments' % (
397 self.arguments['no_permissions'].lvalue))
401 def _assert_path(self, path_or_url):
404 'Directory path is missing in location %s' % path_or_url,
405 details=['Location format: [[pithos://UUID]/CONTAINER/]PATH'])
409 class file_create(_pithos_container, _optional_output_cmd):
410 """Create an empty file"""
413 content_type=ValueArgument(
414 'Set content type (default: application/octet-stream)',
416 default='application/octet-stream')
420 @errors.pithos.connection
421 @errors.pithos.container
423 self._optional_output(
424 self.client.create_object(self.path, self['content_type']))
426 def main(self, path_or_url):
427 super(self.__class__, self)._run(path_or_url)
428 _assert_path(self, path_or_url)
433 class file_mkdir(_pithos_container, _optional_output_cmd):
434 """Create a directory: /file create --content-type='applcation/directory'
438 @errors.pithos.connection
439 @errors.pithos.container
440 def _run(self, path):
441 self._optional_output(self.client.create_directory(self.path))
443 def main(self, path_or_url):
444 super(self.__class__, self)._run(path_or_url)
445 _assert_path(self, path_or_url)
450 class file_delete(_pithos_container):
451 """Delete a file or directory object"""
454 until_date=DateArgument('remove history until then', '--until'),
455 yes=FlagArgument('Do not prompt for permission', '--yes'),
456 recursive=FlagArgument(
457 'If a directory, empty first', ('-r', '--recursive')),
458 delimiter=ValueArgument(
459 'delete objects prefixed with <object><delimiter>', '--delimiter')
463 @errors.pithos.connection
464 @errors.pithos.container
465 @errors.pithos.object_path
468 if self['yes'] or self.ask_user(
469 'Delete /%s/%s ?' % (self.container, self.path)):
470 self.client.del_object(
472 until=self['until_date'],
473 delimiter='/' if self['recursive'] else self['delimiter'])
475 self.error('Aborted')
477 if self['yes'] or self.ask_user(
478 'Empty container /%s ?' % self.container):
479 self.client.container_delete(self.container, delimiter='/')
481 self.error('Aborted')
483 def main(self, path_or_url):
484 super(self.__class__, self)._run(path_or_url)
488 class _source_destination(_pithos_container, _optional_output_cmd):
491 destination_user_uuid=ValueArgument(
492 'default: current user uuid', '--to-account'),
493 destination_container=ValueArgument(
494 'default: pithos', '--to-container'),
495 source_prefix=FlagArgument(
496 'Transfer all files that are prefixed with SOURCE PATH If the '
497 'destination path is specified, replace SOURCE_PATH with '
499 ('-r', '--recursive')),
501 'Overwrite destination objects, if needed', ('-f', '--force')),
502 source_version=ValueArgument(
503 'The version of the source object', '--source-version')
506 def __init__(self, arguments={}, auth_base=None, cloud=None):
507 self.arguments.update(arguments)
508 self.arguments.update(self.sd_arguments)
509 super(_source_destination, self).__init__(
510 self.arguments, auth_base, cloud)
512 def _report_transfer(self, src, dst, transfer_name):
514 if transfer_name in ('move', ):
515 self.error(' delete source directory %s' % src)
517 dst_prf = '' if self.account == self.dst_client.account else (
518 'pithos://%s' % self.dst_client.account)
520 src_prf = '' if self.account == self.dst_client.account else (
521 'pithos://%s' % self.account)
522 self.error(' %s %s/%s/%s\n --> %s/%s/%s' % (
524 src_prf, self.container, src,
525 dst_prf, self.dst_client.container, dst))
527 self.error(' mkdir %s/%s/%s' % (
528 dst_prf, self.dst_client.container, dst))
531 @errors.pithos.account
532 def _src_dst(self, version=None):
534 self.account, self.container, self.path
535 self.dst_acc, self.dst_con, self.dst_path
536 They should all be configured properly
537 :returns: [(src_path, dst_path), ...], if src_path is None, create
538 destination directory
540 src_objects, dst_objects, pairs = dict(), dict(), []
542 for obj in self.dst_client.list_objects(
543 prefix=self.dst_path or self.path or '/'):
544 dst_objects[obj['name']] = obj
545 except ClientError as ce:
546 if ce.status in (404, ):
548 'Destination container pithos://%s/%s not found' % (
549 self.dst_client.account, self.dst_client.container))
551 if self['source_prefix']:
552 # Copy and replace prefixes
553 for src_obj in self.client.list_objects(prefix=self.path):
554 src_objects[src_obj['name']] = src_obj
555 for src_path, src_obj in src_objects.items():
556 dst_path = '%s%s' % (
557 self.dst_path or self.path, src_path[len(self.path):])
558 dst_obj = dst_objects.get(dst_path, None)
559 if self['force'] or not dst_obj:
562 None if self._is_dir(src_obj) else src_path, dst_path))
563 if self._is_dir(src_obj):
564 pairs.append((self.path or dst_path, None))
565 elif not (self._is_dir(dst_obj) and self._is_dir(src_obj)):
567 'Destination object exists', importance=2, details=[
568 'Failed while transfering:',
569 ' pithos://%s/%s/%s' % (
573 '--> pithos://%s/%s/%s' % (
574 self.dst_client.account,
575 self.dst_client.container,
577 'Use %s to transfer overwrite' % (
578 self.arguments['force'].lvalue)])
580 # One object transfer
582 src_version_arg = self.arguments.get('source_version', None)
583 src_obj = self.client.get_object_info(
585 version=src_version_arg.value if src_version_arg else None)
586 except ClientError as ce:
587 if ce.status in (204, ):
589 'Missing specific path container %s' % self.container,
590 importance=2, details=[
591 'To transfer container contents %s' % (
592 self.arguments['source_prefix'].lvalue)])
594 dst_path = self.dst_path or self.path
595 dst_obj = dst_objects.get(dst_path or self.path, None)
596 if self['force'] or not dst_obj:
598 (None if self._is_dir(src_obj) else self.path, dst_path))
599 if self._is_dir(src_obj):
600 pairs.append((self.path or dst_path, None))
601 elif self._is_dir(src_obj):
603 'Cannot transfer an application/directory object',
604 importance=2, details=[
605 'The object pithos://%s/%s/%s is a directory' % (
609 'To recursively copy a directory, use',
610 ' %s' % self.arguments['source_prefix'].lvalue,
611 'To create a file, use',
612 ' /file create (general purpose)',
613 ' /file mkdir (a directory object)'])
616 'Destination object exists',
617 importance=2, details=[
618 'Failed while transfering:',
619 ' pithos://%s/%s/%s' % (
623 '--> pithos://%s/%s/%s' % (
624 self.dst_client.account,
625 self.dst_client.container,
627 'Use %s to transfer overwrite' % (
628 self.arguments['force'].lvalue)])
631 def _run(self, source_path_or_url, destination_path_or_url=''):
632 super(_source_destination, self)._run(source_path_or_url)
633 dst_acc, dst_con, dst_path = self._resolve_pithos_url(
634 destination_path_or_url)
635 self.dst_client = PithosClient(
636 base_url=self.client.base_url, token=self.client.token,
638 'destination_container'] or dst_con or self.client.container,
640 'destination_user_uuid'] or dst_acc or self.account)
641 self.dst_path = dst_path or self.path
645 class file_copy(_source_destination):
646 """Copy objects, even between different accounts or containers"""
649 public=ValueArgument('publish new object', '--public'),
650 content_type=ValueArgument(
651 'change object\'s content type', '--content-type'),
652 source_version=ValueArgument(
653 'The version of the source object', '--object-version')
657 @errors.pithos.connection
658 @errors.pithos.container
659 @errors.pithos.account
661 for src, dst in self._src_dst(self['source_version']):
662 self._report_transfer(src, dst, 'copy')
664 self.dst_client.copy_object(
665 src_container=self.client.container,
667 dst_container=self.dst_client.container,
669 source_account=self.client.account,
670 source_version=self['source_version'],
671 public=self['public'],
672 content_type=self['content_type'])
674 self.dst_client.create_directory(dst)
676 def main(self, source_path_or_url, destination_path_or_url=None):
677 super(file_copy, self)._run(
678 source_path_or_url, destination_path_or_url or '')
683 class file_move(_source_destination):
684 """Move objects, even between different accounts or containers"""
687 public=ValueArgument('publish new object', '--public'),
688 content_type=ValueArgument(
689 'change object\'s content type', '--content-type')
693 @errors.pithos.connection
694 @errors.pithos.container
695 @errors.pithos.account
697 for src, dst in self._src_dst():
698 self._report_transfer(src, dst, 'move')
700 self.dst_client.move_object(
701 src_container=self.client.container,
703 dst_container=self.dst_client.container,
705 source_account=self.account,
706 public=self['public'],
707 content_type=self['content_type'])
709 self.dst_client.create_directory(dst)
711 self.client.del_object(src)
713 def main(self, source_path_or_url, destination_path_or_url=None):
714 super(file_move, self)._run(
715 source_path_or_url, destination_path_or_url or '')
720 class file_append(_pithos_container, _optional_output_cmd):
721 """Append local file to (existing) remote object
722 The remote object should exist.
723 If the remote object is a directory, it is transformed into a file.
724 In the later case, objects under the directory remain intact.
728 progress_bar=ProgressBarArgument(
729 'do not show progress bar', ('-N', '--no-progress-bar'),
731 max_threads=IntArgument('default: 1', '--threads'),
735 @errors.pithos.connection
736 @errors.pithos.container
737 @errors.pithos.object_path
738 def _run(self, local_path):
739 if self['max_threads'] > 0:
740 self.client.MAX_THREADS = int(self['max_threads'])
741 (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
743 with open(local_path, 'rb') as f:
744 self._optional_output(
745 self.client.append_object(self.path, f, upload_cb))
747 self._safe_progress_bar_finish(progress_bar)
749 def main(self, local_path, remote_path_or_url):
750 super(self.__class__, self)._run(remote_path_or_url)
751 self._run(local_path)
755 class file_truncate(_pithos_container, _optional_output_cmd):
756 """Truncate remote file up to size"""
759 size_in_bytes=IntArgument('Length of file after truncation', '--size')
761 required = ('size_in_bytes', )
764 @errors.pithos.connection
765 @errors.pithos.container
766 @errors.pithos.object_path
767 @errors.pithos.object_size
768 def _run(self, size):
769 self._optional_output(self.client.truncate_object(self.path, size))
771 def main(self, path_or_url):
772 super(self.__class__, self)._run(path_or_url)
773 self._run(size=self['size_in_bytes'])
777 class file_overwrite(_pithos_container, _optional_output_cmd):
778 """Overwrite part of a remote file"""
781 progress_bar=ProgressBarArgument(
782 'do not show progress bar', ('-N', '--no-progress-bar'),
784 start_position=IntArgument('File position in bytes', '--from'),
785 end_position=IntArgument('File position in bytes', '--to'),
787 required = ('start_position', 'end_position')
790 @errors.pithos.connection
791 @errors.pithos.container
792 @errors.pithos.object_path
793 @errors.pithos.object_size
794 def _run(self, local_path, start, end):
795 start, end = int(start), int(end)
796 (progress_bar, upload_cb) = self._safe_progress_bar(
797 'Overwrite %s bytes' % (end - start))
799 with open(path.abspath(local_path), 'rb') as f:
800 self._optional_output(self.client.overwrite_object(
805 upload_cb=upload_cb))
807 self._safe_progress_bar_finish(progress_bar)
809 def main(self, local_path, path_or_url):
810 super(self.__class__, self)._run(path_or_url)
811 self.path = self.path or path.basename(local_path)
813 local_path=local_path,
814 start=self['start_position'],
815 end=self['end_position'])
819 class file_upload(_pithos_container, _optional_output_cmd):
823 max_threads=IntArgument('default: 5', '--threads'),
824 content_encoding=ValueArgument(
825 'set MIME content type', '--content-encoding'),
826 content_disposition=ValueArgument(
827 'specify objects presentation style', '--content-disposition'),
828 content_type=ValueArgument('specify content type', '--content-type'),
829 uuid_for_read_permission=RepeatableArgument(
830 'Give read access to a user or group (can be repeated) '
831 'Use * for all users',
832 '--read-permission'),
833 uuid_for_write_permission=RepeatableArgument(
834 'Give write access to a user or group (can be repeated) '
835 'Use * for all users',
836 '--write-permission'),
837 public=FlagArgument('make object publicly accessible', '--public'),
838 progress_bar=ProgressBarArgument(
839 'do not show progress bar',
840 ('-N', '--no-progress-bar'),
842 overwrite=FlagArgument('Force (over)write', ('-f', '--force')),
843 recursive=FlagArgument(
844 'Recursively upload directory *contents* + subdirectories',
845 ('-r', '--recursive')),
846 unchunked=FlagArgument(
847 'Upload file as one block (not recommended)', '--unchunked'),
848 md5_checksum=ValueArgument(
849 'Confirm upload with a custom checksum (MD5)', '--etag'),
850 use_hashes=FlagArgument(
851 'Source file contains hashmap not data', '--source-is-hashmap'),
856 readlist = self['uuid_for_read_permission']
858 sharing['read'] = self['uuid_for_read_permission']
859 writelist = self['uuid_for_write_permission']
861 sharing['write'] = self['uuid_for_write_permission']
862 return sharing or None
864 def _check_container_limit(self, path):
865 cl_dict = self.client.get_container_limit()
866 container_limit = int(cl_dict['x-container-policy-quota'])
867 r = self.client.container_get()
868 used_bytes = sum(int(o['bytes']) for o in r.json)
869 path_size = get_path_size(path)
870 if container_limit and path_size > (container_limit - used_bytes):
872 'Container %s (limit(%s) - used(%s)) < (size(%s) of %s)' % (
873 self.client.container,
874 format_size(container_limit),
875 format_size(used_bytes),
876 format_size(path_size),
879 'Check accound limit: /file quota',
880 'Check container limit:',
881 '\t/file containerlimit get %s' % self.client.container,
882 'Increase container limit:',
883 '\t/file containerlimit set <new limit> %s' % (
884 self.client.container)])
886 def _src_dst(self, local_path, remote_path, objlist=None):
887 lpath = path.abspath(local_path)
888 short_path = path.basename(path.abspath(local_path))
889 rpath = remote_path or short_path
890 if path.isdir(lpath):
891 if not self['recursive']:
892 raise CLIError('%s is a directory' % lpath, details=[
893 'Use %s to upload directories & contents' % (
894 self.arguments['recursive'].lvalue)])
895 robj = self.client.container_get(path=rpath)
896 if not self['overwrite']:
899 'Objects/files prefixed as %s already exist' % rpath,
900 details=['Existing objects:'] + ['\t/%s/\t%s' % (
902 o['content_type'][12:]) for o in robj.json] + [
903 'Use -f to add, overwrite or resume'])
906 topobj = self.client.get_object_info(rpath)
907 if not self._is_dir(topobj):
909 'Object /%s/%s exists but not a directory' % (
910 self.container, rpath),
911 details=['Use -f to overwrite'])
912 except ClientError as ce:
913 if ce.status not in (404, ):
915 self._check_container_limit(lpath)
917 for top, subdirs, files in walk(lpath):
921 rel_path = rpath + top.split(lpath)[1]
924 self.error('mkdir /%s/%s' % (
925 self.client.container, rel_path))
926 self.client.create_directory(rel_path)
928 fpath = path.join(top, f)
929 if path.isfile(fpath):
930 rel_path = rel_path.replace(path.sep, '/')
931 pathfix = f.replace(path.sep, '/')
932 yield open(fpath, 'rb'), '%s/%s' % (rel_path, pathfix)
934 self.error('%s is not a regular file' % fpath)
936 if not path.isfile(lpath):
937 raise CLIError(('%s is not a regular file' % lpath) if (
938 path.exists(lpath)) else '%s does not exist' % lpath)
940 robj = self.client.get_object_info(rpath)
941 if remote_path and self._is_dir(robj):
942 rpath += '/%s' % (short_path.replace(path.sep, '/'))
943 self.client.get_object_info(rpath)
944 if not self['overwrite']:
946 'Object /%s/%s already exists' % (
947 self.container, rpath),
948 details=['use -f to overwrite / resume'])
949 except ClientError as ce:
950 if ce.status not in (404, ):
952 self._check_container_limit(lpath)
953 yield open(lpath, 'rb'), rpath
955 def _run(self, local_path, remote_path):
956 self.client.MAX_THREADS = int(self['max_threads'] or 5)
958 content_encoding=self['content_encoding'],
959 content_type=self['content_type'],
960 content_disposition=self['content_disposition'],
961 sharing=self._sharing(),
962 public=self['public'])
963 uploaded, container_info_cache = list, dict()
964 rpref = 'pithos://%s' if self['account'] else ''
965 for f, rpath in self._src_dst(local_path, remote_path):
966 self.error('%s --> %s/%s/%s' % (
967 f.name, rpref, self.client.container, rpath))
968 if not (self['content_type'] and self['content_encoding']):
969 ctype, cenc = guess_mime_type(f.name)
970 params['content_type'] = self['content_type'] or ctype
971 params['content_encoding'] = self['content_encoding'] or cenc
972 if self['unchunked']:
973 r = self.client.upload_object_unchunked(
975 etag=self['md5_checksum'], withHashFile=self['use_hashes'],
977 if self['with_output'] or self['json_output']:
978 r['name'] = '/%s/%s' % (self.client.container, rpath)
982 (progress_bar, upload_cb) = self._safe_progress_bar(
983 'Uploading %s' % f.name.split(path.sep)[-1])
985 hash_bar = progress_bar.clone()
986 hash_cb = hash_bar.get_generator(
987 'Calculating block hashes')
990 r = self.client.upload_object(
994 container_info_cache=container_info_cache,
996 if self['with_output'] or self['json_output']:
997 r['name'] = '/%s/%s' % (self.client.container, rpath)
1000 self._safe_progress_bar_finish(progress_bar)
1003 self._safe_progress_bar_finish(progress_bar)
1004 self._optional_output(uploaded)
1005 self.error('Upload completed')
1007 def main(self, local_path, remote_path_or_url):
1008 super(self.__class__, self)._run(remote_path_or_url)
1009 remote_path = self.path or path.basename(path.abspath(local_path))
1010 self._run(local_path=local_path, remote_path=remote_path)
1013 class RangeArgument(ValueArgument):
1015 :value type: string of the form <start>-<end> where <start> and <end> are
1017 :value returns: the input string, after type checking <start> and <end>
1022 return getattr(self, '_value', self.default)
1025 def value(self, newvalues):
1027 self._value = getattr(self, '_value', self.default)
1028 for newvalue in newvalues.split(','):
1029 self._value = ('%s,' % self._value) if self._value else ''
1030 start, sep, end = newvalue.partition('-')
1033 start, end = (int(start), int(end))
1035 raise CLIInvalidArgument(
1036 'Invalid range %s' % newvalue, details=[
1037 'Valid range formats',
1038 ' START-END', ' UP_TO', ' -FROM',
1039 'where all values are integers',
1040 'OR a compination (csv), e.g.,',
1041 ' %s=5,10-20,-5' % self.lvalue])
1042 self._value += '%s-%s' % (start, end)
1044 self._value += '-%s' % int(end)
1046 self._value += '%s' % int(start)
1050 class file_cat(_pithos_container):
1051 """Fetch remote file contents"""
1054 range=RangeArgument('show range of data e.g., 5,10-20,-5', '--range'),
1055 if_match=ValueArgument('show output if ETags match', '--if-match'),
1056 if_none_match=ValueArgument(
1057 'show output if ETags match', '--if-none-match'),
1058 if_modified_since=DateArgument(
1059 'show output modified since then', '--if-modified-since'),
1060 if_unmodified_since=DateArgument(
1061 'show output unmodified since then', '--if-unmodified-since'),
1062 object_version=ValueArgument(
1063 'Get contents of the chosen version', '--object-version')
1067 @errors.pithos.connection
1068 @errors.pithos.container
1069 @errors.pithos.object_path
1071 self.client.download_object(
1072 self.path, self._out,
1073 range_str=self['range'],
1074 version=self['object_version'],
1075 if_match=self['if_match'],
1076 if_none_match=self['if_none_match'],
1077 if_modified_since=self['if_modified_since'],
1078 if_unmodified_since=self['if_unmodified_since'])
1081 def main(self, path_or_url):
1082 super(self.__class__, self)._run(path_or_url)
1087 class file_download(_pithos_container):
1088 """Download a remove file or directory object to local file system"""
1091 resume=FlagArgument(
1092 'Resume/Overwrite (attempt resume, else overwrite)',
1093 ('-f', '--resume')),
1094 range=RangeArgument(
1095 'Download only that range of data e.g., 5,10-20,-5', '--range'),
1096 matching_etag=ValueArgument('download iff ETag match', '--if-match'),
1097 non_matching_etag=ValueArgument(
1098 'download iff ETags DO NOT match', '--if-none-match'),
1099 modified_since_date=DateArgument(
1100 'download iff remote file is modified since then',
1101 '--if-modified-since'),
1102 unmodified_since_date=DateArgument(
1103 'show output iff remote file is unmodified since then',
1104 '--if-unmodified-since'),
1105 object_version=ValueArgument(
1106 'download a file of a specific version', '--object-version'),
1107 max_threads=IntArgument('default: 5', '--threads'),
1108 progress_bar=ProgressBarArgument(
1109 'do not show progress bar', ('-N', '--no-progress-bar'),
1111 recursive=FlagArgument(
1112 'Download a remote directory object and its contents',
1113 ('-r', '--recursive'))
1116 def _src_dst(self, local_path):
1117 """Create a list of (src, dst) where src is a remote location and dst
1118 is an open file descriptor. Directories are denoted as (None, dirpath)
1119 and they are pretended to other objects in a very strict order (shorter
1124 obj = self.client.get_object_info(
1125 self.path, version=self['object_version'])
1126 obj.setdefault('name', self.path.strip('/'))
1129 except ClientError as ce:
1130 if ce.status in (404, ):
1131 raiseCLIError(ce, details=[
1132 'To download an object, it must exist either as a file or'
1134 'For example, to download everything under prefix/ the '
1135 'directory "prefix" must exist.',
1136 'To see if an remote object is actually there:',
1137 ' /file info [/CONTAINER/]OBJECT',
1138 'To create a directory object:',
1139 ' /file mkdir [/CONTAINER/]OBJECT'])
1140 if ce.status in (204, ):
1142 'No file or directory objects to download',
1144 'To download a container (e.g., %s):' % self.container,
1145 ' [kamaki] container download %s [LOCAL_PATH]' % (
1148 rpath = self.path.strip('/')
1149 if local_path and self.path and local_path.endswith('/'):
1150 local_path = local_path[-1:]
1152 if (not obj) or self._is_dir(obj):
1153 if self['recursive']:
1154 if not (self.path or local_path.endswith('/')):
1155 # Download the whole container
1156 local_path = '' if local_path in ('.', ) else local_path
1157 local_path = '%s/' % (local_path or self.container)
1159 name='', content_type='application/directory')
1160 dirs, files = [obj, ], []
1161 objects = self.client.container_get(
1163 if_modified_since=self['modified_since_date'],
1164 if_unmodified_since=self['unmodified_since_date'])
1165 for o in objects.json:
1166 (dirs if self._is_dir(o) else files).append(o)
1168 # Put the directories on top of the list
1169 for dpath in sorted(['%s%s' % (
1170 local_path, d['name'][len(rpath):]) for d in dirs]):
1171 if path.exists(dpath):
1172 if path.isdir(dpath):
1175 'Cannot replace local file %s with a directory '
1176 'of the same name' % dpath,
1178 'Either remove the file or specify a'
1179 'different target location'])
1180 ret.append((None, dpath, None))
1182 # Append the file objects
1183 for opath in [o['name'] for o in files]:
1184 lpath = '%s%s' % (local_path, opath[len(rpath):])
1186 fxists = path.exists(lpath)
1187 if fxists and path.isdir(lpath):
1189 'Cannot change local dir %s info file' % (
1192 'Either remove the file or specify a'
1193 'different target location'])
1194 ret.append((opath, lpath, fxists))
1195 elif path.exists(lpath):
1197 'Cannot overwrite %s' % lpath,
1198 details=['To overwrite/resume, use %s' % (
1199 self.arguments['resume'].lvalue)])
1201 ret.append((opath, lpath, None))
1204 'Remote object /%s/%s is a directory' % (
1205 self.container, local_path),
1206 details=['Use %s to download directories' % (
1207 self.arguments['recursive'].lvalue)])
1209 parsed_name = self.arguments['recursive'].lvalue
1211 'Cannot download container %s' % self.container,
1213 'Use %s to download containers' % parsed_name,
1214 ' [kamaki] file download %s /%s [LOCAL_PATH]' % (
1215 parsed_name, self.container)])
1217 # Remote object is just a file
1218 if path.exists(local_path):
1219 if not self['resume']:
1221 'Cannot overwrite local file %s' % (local_path),
1222 details=['To overwrite/resume, use %s' % (
1223 self.arguments['resume'].lvalue)])
1224 elif '/' in local_path[1:-1]:
1225 dirs = [p for p in local_path.split('/') if p]
1226 pref = '/' if local_path.startswith('/') else ''
1229 if not path.exists(pref):
1230 ret.append((None, d, None))
1231 elif not path.isdir(pref):
1233 'Failed to use %s as a destination' % local_path,
1236 'Local file %s is not a directory' % pref,
1237 'Destination prefix must consist of '
1238 'directories or non-existing names',
1239 'Either remove the file, or choose another '
1241 ret.append((rpath, local_path, self['resume']))
1242 for r, l, resume in ret:
1244 with open(l, 'rwb+' if resume else 'wb+') as f:
1250 @errors.pithos.connection
1251 @errors.pithos.container
1252 @errors.pithos.object_path
1253 @errors.pithos.local_path
1254 @errors.pithos.local_path_download
1255 def _run(self, local_path):
1256 self.client.MAX_THREADS = int(self['max_threads'] or 5)
1259 for rpath, output_file in self._src_dst(local_path):
1261 self.error('Create local directory %s' % output_file)
1262 makedirs(output_file)
1264 self.error('/%s/%s --> %s' % (
1265 self.container, rpath, output_file.name))
1266 progress_bar, download_cb = self._safe_progress_bar(
1268 self.client.download_object(
1270 download_cb=download_cb,
1271 range_str=self['range'],
1272 version=self['object_version'],
1273 if_match=self['matching_etag'],
1274 resume=self['resume'],
1275 if_none_match=self['non_matching_etag'],
1276 if_modified_since=self['modified_since_date'],
1277 if_unmodified_since=self['unmodified_since_date'])
1278 except KeyboardInterrupt:
1279 from threading import activeCount, enumerate as activethreads
1281 while activeCount() > 1:
1282 self._out.write('\nCancel %s threads: ' % (activeCount() - 1))
1284 for thread in activethreads():
1286 thread.join(timeout)
1287 self._out.write('.' if thread.isAlive() else '*')
1288 except RuntimeError:
1293 self.error('\nDownload canceled by user')
1294 if local_path is not None:
1295 self.error('to resume, re-run with --resume')
1297 self._safe_progress_bar_finish(progress_bar)
1299 def main(self, remote_path_or_url, local_path=None):
1300 super(self.__class__, self)._run(remote_path_or_url)
1301 local_path = local_path or self.path or '.'
1302 self._run(local_path=local_path)
1305 @command(container_cmds)
1306 class container_info(_pithos_account, _optional_json):
1307 """Get information about a container"""
1310 until_date=DateArgument('show metadata until then', '--until'),
1311 metadata=FlagArgument('Show only container metadata', '--metadata'),
1312 sizelimit=FlagArgument(
1313 'Show the maximum size limit for container', '--size-limit'),
1314 in_bytes=FlagArgument('Show size limit in bytes', ('-b', '--bytes'))
1318 @errors.pithos.connection
1319 @errors.pithos.container
1320 @errors.pithos.object_path
1322 if self['metadata']:
1323 r, preflen = dict(), len('x-container-meta-')
1324 for k, v in self.client.get_container_meta(
1325 until=self['until_date']).items():
1327 elif self['sizelimit']:
1328 r = self.client.get_container_limit(
1329 self.container)['x-container-policy-quota']
1330 r = {'size limit': 'unlimited' if r in ('0', ) else (
1331 int(r) if self['in_bytes'] else format_size(r))}
1333 r = self.client.get_container_info(self.container)
1334 self._print(r, self.print_dict)
1336 def main(self, container):
1337 super(self.__class__, self)._run()
1338 self.container, self.client.container = container, container
1342 class VersioningArgument(ValueArgument):
1344 schemes = ('auto', 'none')
1348 return getattr(self, '_value', None)
1351 def value(self, new_scheme):
1353 new_scheme = new_scheme.lower()
1354 if new_scheme not in self.schemes:
1355 raise CLIInvalidArgument('Invalid versioning value', details=[
1356 'Valid versioning values are %s' % ', '.join(
1358 self._value = new_scheme
1361 @command(container_cmds)
1362 class container_modify(_pithos_account, _optional_json):
1363 """Modify the properties of a container"""
1366 metadata_to_add=KeyValueArgument(
1367 'Add metadata in the form KEY=VALUE (can be repeated)',
1369 metadata_to_delete=RepeatableArgument(
1370 'Delete metadata by KEY (can be repeated)', '--metadata-del'),
1371 sizelimit=DataSizeArgument(
1372 'Set max size limit (0 for unlimited, '
1373 'use units B, KiB, KB, etc.)', '--size-limit'),
1374 versioning=VersioningArgument(
1375 'Set a versioning scheme (%s)' % ', '.join(
1376 VersioningArgument.schemes), '--versioning')
1379 'metadata_to_add', 'metadata_to_delete', 'sizelimit', 'versioning']
1382 @errors.pithos.connection
1383 @errors.pithos.container
1384 def _run(self, container):
1385 metadata = self['metadata_to_add']
1386 for k in (self['metadata_to_delete'] or []):
1389 self.client.set_container_meta(metadata)
1390 self._print(self.client.get_container_meta(), self.print_dict)
1391 if self['sizelimit'] is not None:
1392 self.client.set_container_limit(self['sizelimit'])
1393 r = self.client.get_container_limit()['x-container-policy-quota']
1394 r = 'unlimited' if r in ('0', ) else format_size(r)
1395 self.writeln('new size limit: %s' % r)
1396 if self['versioning']:
1397 self.client.set_container_versioning(self['versioning'])
1398 self.writeln('new versioning scheme: %s' % (
1399 self.client.get_container_versioning(self.container)[
1400 'x-container-policy-versioning']))
1402 def main(self, container):
1403 super(self.__class__, self)._run()
1404 self.client.container, self.container = container, container
1405 self._run(container=container)
1408 @command(container_cmds)
1409 class container_list(_pithos_account, _optional_json, _name_filter):
1410 """List all containers, or their contents"""
1413 detail=FlagArgument('Containers with details', ('-l', '--list')),
1414 limit=IntArgument('limit number of listed items', ('-n', '--number')),
1415 marker=ValueArgument('output greater that marker', '--marker'),
1416 modified_since_date=ValueArgument(
1417 'show output modified since then', '--if-modified-since'),
1418 unmodified_since_date=ValueArgument(
1419 'show output not modified since then', '--if-unmodified-since'),
1420 until_date=DateArgument('show metadata until then', '--until'),
1421 shared=FlagArgument('show only shared', '--shared'),
1422 more=FlagArgument('read long results', '--more'),
1423 enum=FlagArgument('Enumerate results', '--enumerate'),
1424 recursive=FlagArgument(
1425 'Recursively list containers and their contents',
1426 ('-r', '--recursive')),
1427 shared_by_me=FlagArgument(
1428 'show only files shared to other users', '--shared-by-me'),
1429 public=FlagArgument('show only published objects', '--public'),
1432 def print_containers(self, container_list):
1433 for index, container in enumerate(container_list):
1434 if 'bytes' in container:
1435 size = format_size(container['bytes'])
1436 prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
1437 _cname = container['name'] if (
1438 self['more']) else bold(container['name'])
1439 cname = u'%s%s' % (prfx, _cname)
1442 pretty_c = container.copy()
1443 if 'bytes' in container:
1444 pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
1445 self.print_dict(pretty_c, exclude=('name'))
1448 if 'count' in container and 'bytes' in container:
1449 self.writeln('%s (%s, %s objects)' % (
1450 cname, size, container['count']))
1453 objects = container.get('objects', [])
1455 self.print_objects(objects)
1458 def _create_object_forest(self, container_list):
1460 for container in container_list:
1461 self.client.container = container['name']
1462 objects = self.client.container_get(
1463 limit=False if self['more'] else self['limit'],
1464 if_modified_since=self['modified_since_date'],
1465 if_unmodified_since=self['unmodified_since_date'],
1466 until=self['until_date'],
1467 show_only_shared=self['shared_by_me'],
1468 public=self['public'])
1469 container['objects'] = objects.json
1471 self.client.container = None
1474 @errors.pithos.connection
1475 @errors.pithos.object_path
1476 @errors.pithos.container
1477 def _run(self, container):
1479 r = self.client.container_get(
1480 limit=False if self['more'] else self['limit'],
1481 marker=self['marker'],
1482 if_modified_since=self['modified_since_date'],
1483 if_unmodified_since=self['unmodified_since_date'],
1484 until=self['until_date'],
1485 show_only_shared=self['shared_by_me'],
1486 public=self['public'])
1488 r = self.client.account_get(
1489 limit=False if self['more'] else self['limit'],
1490 marker=self['marker'],
1491 if_modified_since=self['modified_since_date'],
1492 if_unmodified_since=self['unmodified_since_date'],
1493 until=self['until_date'],
1494 show_only_shared=self['shared_by_me'],
1495 public=self['public'])
1496 files = self._filter_by_name(r.json)
1497 if self['recursive'] and not container:
1498 self._create_object_forest(files)
1500 outbu, self._out = self._out, StringIO()
1502 if self['json_output'] or self['output_format']:
1505 (self.print_objects if container else self.print_containers)(
1509 pager(self._out.getvalue())
1512 def main(self, container=None):
1513 super(self.__class__, self)._run()
1514 self.client.container, self.container = container, container
1515 self._run(container)
1518 @command(container_cmds)
1519 class container_create(_pithos_account):
1520 """Create a new container"""
1523 versioning=ValueArgument(
1524 'set container versioning (auto/none)', '--versioning'),
1525 limit=IntArgument('set default container limit', '--limit'),
1526 meta=KeyValueArgument(
1527 'set container metadata (can be repeated)', '--meta')
1531 @errors.pithos.connection
1532 @errors.pithos.container
1533 def _run(self, container):
1535 self.client.create_container(
1536 container=container,
1537 sizelimit=self['limit'],
1538 versioning=self['versioning'],
1539 metadata=self['meta'],
1541 except ClientError as ce:
1542 if ce.status in (202, ):
1544 'Container %s alread exists' % container, details=[
1545 'Either delete %s or choose another name' % (container)])
1548 def main(self, new_container):
1549 super(self.__class__, self)._run()
1550 self._run(container=new_container)
1553 @command(container_cmds)
1554 class container_delete(_pithos_account):
1555 """Delete a container"""
1558 yes=FlagArgument('Do not prompt for permission', '--yes'),
1559 recursive=FlagArgument(
1560 'delete container even if not empty', ('-r', '--recursive'))
1564 @errors.pithos.connection
1565 @errors.pithos.container
1566 def _run(self, container):
1567 num_of_contents = int(self.client.get_container_info(container)[
1568 'x-container-object-count'])
1569 delimiter, msg = None, 'Delete container %s ?' % container
1570 if self['recursive']:
1571 delimiter, msg = '/', 'Empty and d%s' % msg[1:]
1572 elif num_of_contents:
1573 raise CLIError('Container %s is not empty' % container, details=[
1574 'Use %s to delete non-empty containers' % (
1575 self.arguments['recursive'].lvalue)])
1576 if self['yes'] or self.ask_user(msg):
1578 self.client.del_container(delimiter=delimiter)
1579 self.client.purge_container()
1581 def main(self, container):
1582 super(self.__class__, self)._run()
1583 self.container, self.client.container = container, container
1584 self._run(container)
1587 @command(container_cmds)
1588 class container_empty(_pithos_account):
1589 """Empty a container"""
1591 arguments = dict(yes=FlagArgument('Do not prompt for permission', '--yes'))
1594 @errors.pithos.connection
1595 @errors.pithos.container
1596 def _run(self, container):
1597 if self['yes'] or self.ask_user('Empty container %s ?' % container):
1598 self.client.del_container(delimiter='/')
1600 def main(self, container):
1601 super(self.__class__, self)._run()
1602 self.container, self.client.container = container, container
1603 self._run(container)
1606 @command(sharer_cmds)
1607 class sharer_list(_pithos_account, _optional_json):
1608 """List accounts who share file objects with current user"""
1611 detail=FlagArgument('show detailed output', ('-l', '--details')),
1612 marker=ValueArgument('show output greater then marker', '--marker')
1616 @errors.pithos.connection
1618 accounts = self.client.get_sharing_accounts(marker=self['marker'])
1619 if not (self['json_output'] or self['output_format']):
1620 usernames = self._uuids2usernames(
1621 [acc['name'] for acc in accounts])
1622 for item in accounts:
1624 item['id'], item['name'] = uuid, usernames[uuid]
1625 if not self['detail']:
1626 item.pop('last_modified')
1627 self._print(accounts)
1630 super(self.__class__, self)._run()
1634 @command(sharer_cmds)
1635 class sharer_info(_pithos_account, _optional_json):
1636 """Details on a Pithos+ sharer account (default: current account)"""
1639 @errors.pithos.connection
1641 self._print(self.client.get_account_info(), self.print_dict)
1643 def main(self, account_uuid=None):
1644 super(self.__class__, self)._run()
1646 self.client.account, self.account = account_uuid, account_uuid
1650 class _pithos_group(_pithos_account):
1651 prefix = 'x-account-group-'
1652 preflen = len(prefix)
1656 for k, v in self.client.get_account_group().items():
1657 groups[k[self.preflen:]] = v
1661 @command(group_cmds)
1662 class group_list(_pithos_group, _optional_json):
1663 """list all groups and group members"""
1666 @errors.pithos.connection
1668 self._print(self._groups(), self.print_dict)
1671 super(self.__class__, self)._run()
1675 @command(group_cmds)
1676 class group_create(_pithos_group, _optional_json):
1677 """Create a group of users"""
1680 user_uuid=RepeatableArgument('Add a user to the group', '--uuid'),
1681 username=RepeatableArgument('Add a user to the group', '--username')
1683 required = ['user_uuid', 'username']
1686 @errors.pithos.connection
1687 def _run(self, groupname, *users):
1688 if groupname in self._groups() and not self.ask_user(
1689 'Group %s already exists, overwrite?' % groupname):
1690 self.error('Aborted')
1692 self.client.set_account_group(groupname, users)
1693 self._print(self._groups(), self.print_dict)
1695 def main(self, groupname):
1696 super(self.__class__, self)._run()
1697 users = (self['user_uuid'] or []) + self._usernames2uuids(
1698 self['username'] or []).values()
1700 self._run(groupname, *users)
1702 raise CLISyntaxError(
1703 'No valid users specified, use %s or %s' % (
1704 self.arguments['user_uuid'].lvalue,
1705 self.arguments['username'].lvalue),
1707 'Check if a username or uuid is valid with',
1708 ' user uuid2username', 'OR', ' user username2uuid'])
1711 @command(group_cmds)
1712 class group_delete(_pithos_group, _optional_json):
1713 """Delete a user group"""
1716 @errors.pithos.connection
1717 def _run(self, groupname):
1718 self.client.del_account_group(groupname)
1719 self._print(self._groups(), self.print_dict)
1721 def main(self, groupname):
1722 super(self.__class__, self)._run()
1723 self._run(groupname)
1726 # Deprecated commands
1729 class file_publish(_pithos_init):
1730 """DEPRECATED, replaced by [kamaki] file modify OBJECT --publish"""
1732 def main(self, *args):
1733 raise CLISyntaxError('DEPRECATED', details=[
1734 'This command is replaced by:',
1735 ' [kamaki] file modify OBJECT --publish'])
1739 class file_unpublish(_pithos_init):
1740 """DEPRECATED, replaced by [kamaki] file modify OBJECT --unpublish"""
1742 def main(self, *args):
1743 raise CLISyntaxError('DEPRECATED', details=[
1744 'This command is replaced by:',
1745 ' [kamaki] file modify OBJECT --unpublish'])