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' == 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))
402 class file_create(_pithos_container, _optional_output_cmd):
403 """Create an empty file"""
406 content_type=ValueArgument(
407 'Set content type (default: application/octet-stream)',
409 default='application/octet-stream')
413 @errors.pithos.connection
414 @errors.pithos.container
416 self._optional_output(
417 self.client.create_object(self.path, self['content_type']))
419 def main(self, path_or_url):
420 super(self.__class__, self)._run(path_or_url)
425 class file_mkdir(_pithos_container, _optional_output_cmd):
426 """Create a directory: /file create --content-type='applcation/directory'
430 @errors.pithos.connection
431 @errors.pithos.container
433 self._optional_output(self.client.create_directory(self.path))
435 def main(self, path_or_url):
436 super(self.__class__, self)._run(path_or_url)
441 class file_delete(_pithos_container):
442 """Delete a file or directory object"""
445 until_date=DateArgument('remove history until then', '--until'),
446 yes=FlagArgument('Do not prompt for permission', '--yes'),
447 recursive=FlagArgument(
448 'If a directory, empty first', ('-r', '--recursive')),
449 delimiter=ValueArgument(
450 'delete objects prefixed with <object><delimiter>', '--delimiter')
454 @errors.pithos.connection
455 @errors.pithos.container
456 @errors.pithos.object_path
459 if self['yes'] or self.ask_user(
460 'Delete /%s/%s ?' % (self.container, self.path)):
461 self.client.del_object(
463 until=self['until_date'],
464 delimiter='/' if self['recursive'] else self['delimiter'])
466 self.error('Aborted')
468 if self['yes'] or self.ask_user(
469 'Empty container /%s ?' % self.container):
470 self.client.container_delete(self.container, delimiter='/')
472 self.error('Aborted')
474 def main(self, path_or_url):
475 super(self.__class__, self)._run(path_or_url)
479 class _source_destination(_pithos_container, _optional_output_cmd):
482 destination_user_uuid=ValueArgument(
483 'default: current user uuid', '--to-account'),
484 destination_container=ValueArgument(
485 'default: pithos', '--to-container'),
486 source_prefix=FlagArgument(
487 'Transfer all files that are prefixed with SOURCE PATH If the '
488 'destination path is specified, replace SOURCE_PATH with '
490 ('-r', '--recursive')),
492 'Overwrite destination objects, if needed', ('-f', '--force')),
493 source_version=ValueArgument(
494 'The version of the source object', '--source-version')
497 def __init__(self, arguments={}, auth_base=None, cloud=None):
498 self.arguments.update(arguments)
499 self.arguments.update(self.sd_arguments)
500 super(_source_destination, self).__init__(
501 self.arguments, auth_base, cloud)
503 def _report_transfer(self, src, dst, transfer_name):
505 if transfer_name in ('move', ):
506 self.error(' delete source directory %s' % src)
508 dst_prf = '' if self.account == self.dst_client.account else (
509 'pithos://%s' % self.dst_client.account)
511 src_prf = '' if self.account == self.dst_client.account else (
512 'pithos://%s' % self.account)
513 self.error(' %s %s/%s/%s\n --> %s/%s/%s' % (
515 src_prf, self.container, src,
516 dst_prf, self.dst_client.container, dst))
518 self.error(' mkdir %s/%s/%s' % (
519 dst_prf, self.dst_client.container, dst))
522 @errors.pithos.account
523 def _src_dst(self, version=None):
525 self.account, self.container, self.path
526 self.dst_acc, self.dst_con, self.dst_path
527 They should all be configured properly
528 :returns: [(src_path, dst_path), ...], if src_path is None, create
529 destination directory
531 src_objects, dst_objects, pairs = dict(), dict(), []
533 for obj in self.dst_client.list_objects(
534 prefix=self.dst_path or self.path or '/'):
535 dst_objects[obj['name']] = obj
536 except ClientError as ce:
537 if ce.status in (404, ):
539 'Destination container pithos://%s/%s not found' % (
540 self.dst_client.account, self.dst_client.container))
542 if self['source_prefix']:
543 # Copy and replace prefixes
544 for src_obj in self.client.list_objects(prefix=self.path):
545 src_objects[src_obj['name']] = src_obj
546 for src_path, src_obj in src_objects.items():
547 dst_path = '%s%s' % (
548 self.dst_path or self.path, src_path[len(self.path):])
549 dst_obj = dst_objects.get(dst_path, None)
550 if self['force'] or not dst_obj:
553 None if self._is_dir(src_obj) else src_path, dst_path))
554 if self._is_dir(src_obj):
555 pairs.append((self.path or dst_path, None))
556 elif not (self._is_dir(dst_obj) and self._is_dir(src_obj)):
558 'Destination object exists', importance=2, details=[
559 'Failed while transfering:',
560 ' pithos://%s/%s/%s' % (
564 '--> pithos://%s/%s/%s' % (
565 self.dst_client.account,
566 self.dst_client.container,
568 'Use %s to transfer overwrite' % (
569 self.arguments['force'].lvalue)])
571 # One object transfer
573 src_version_arg = self.arguments.get('source_version', None)
574 src_obj = self.client.get_object_info(
576 version=src_version_arg.value if src_version_arg else None)
577 except ClientError as ce:
578 if ce.status in (204, ):
580 'Missing specific path container %s' % self.container,
581 importance=2, details=[
582 'To transfer container contents %s' % (
583 self.arguments['source_prefix'].lvalue)])
585 dst_path = self.dst_path or self.path
586 dst_obj = dst_objects.get(dst_path or self.path, None)
587 if self['force'] or not dst_obj:
589 (None if self._is_dir(src_obj) else self.path, dst_path))
590 if self._is_dir(src_obj):
591 pairs.append((self.path or dst_path, None))
592 elif self._is_dir(src_obj):
594 'Cannot transfer an application/directory object',
595 importance=2, details=[
596 'The object pithos://%s/%s/%s is a directory' % (
600 'To recursively copy a directory, use',
601 ' %s' % self.arguments['source_prefix'].lvalue,
602 'To create a file, use',
603 ' /file create (general purpose)',
604 ' /file mkdir (a directory object)'])
607 'Destination object exists',
608 importance=2, details=[
609 'Failed while transfering:',
610 ' pithos://%s/%s/%s' % (
614 '--> pithos://%s/%s/%s' % (
615 self.dst_client.account,
616 self.dst_client.container,
618 'Use %s to transfer overwrite' % (
619 self.arguments['force'].lvalue)])
622 def _run(self, source_path_or_url, destination_path_or_url=''):
623 super(_source_destination, self)._run(source_path_or_url)
624 dst_acc, dst_con, dst_path = self._resolve_pithos_url(
625 destination_path_or_url)
626 self.dst_client = PithosClient(
627 base_url=self.client.base_url, token=self.client.token,
629 'destination_container'] or dst_con or self.client.container,
631 'destination_user_uuid'] or dst_acc or self.account)
632 self.dst_path = dst_path or self.path
636 class file_copy(_source_destination):
637 """Copy objects, even between different accounts or containers"""
640 public=ValueArgument('publish new object', '--public'),
641 content_type=ValueArgument(
642 'change object\'s content type', '--content-type'),
643 source_version=ValueArgument(
644 'The version of the source object', '--object-version')
648 @errors.pithos.connection
649 @errors.pithos.container
650 @errors.pithos.account
652 for src, dst in self._src_dst(self['source_version']):
653 self._report_transfer(src, dst, 'copy')
655 self.dst_client.copy_object(
656 src_container=self.client.container,
658 dst_container=self.dst_client.container,
660 source_account=self.client.account,
661 source_version=self['source_version'],
662 public=self['public'],
663 content_type=self['content_type'])
665 self.dst_client.create_directory(dst)
667 def main(self, source_path_or_url, destination_path_or_url=None):
668 super(file_copy, self)._run(
669 source_path_or_url, destination_path_or_url or '')
674 class file_move(_source_destination):
675 """Move objects, even between different accounts or containers"""
678 public=ValueArgument('publish new object', '--public'),
679 content_type=ValueArgument(
680 'change object\'s content type', '--content-type')
684 @errors.pithos.connection
685 @errors.pithos.container
686 @errors.pithos.account
688 for src, dst in self._src_dst():
689 self._report_transfer(src, dst, 'move')
691 self.dst_client.move_object(
692 src_container=self.client.container,
694 dst_container=self.dst_client.container,
696 source_account=self.account,
697 public=self['public'],
698 content_type=self['content_type'])
700 self.dst_client.create_directory(dst)
702 self.client.del_object(src)
704 def main(self, source_path_or_url, destination_path_or_url=None):
705 super(file_move, self)._run(
706 source_path_or_url, destination_path_or_url or '')
711 class file_append(_pithos_container, _optional_output_cmd):
712 """Append local file to (existing) remote object
713 The remote object should exist.
714 If the remote object is a directory, it is transformed into a file.
715 In the later case, objects under the directory remain intact.
719 progress_bar=ProgressBarArgument(
720 'do not show progress bar', ('-N', '--no-progress-bar'),
722 max_threads=IntArgument('default: 1', '--threads'),
726 @errors.pithos.connection
727 @errors.pithos.container
728 @errors.pithos.object_path
729 def _run(self, local_path):
730 if self['max_threads'] > 0:
731 self.client.MAX_THREADS = int(self['max_threads'])
732 (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
734 with open(local_path, 'rb') as f:
735 self._optional_output(
736 self.client.append_object(self.path, f, upload_cb))
738 self._safe_progress_bar_finish(progress_bar)
740 def main(self, local_path, remote_path_or_url):
741 super(self.__class__, self)._run(remote_path_or_url)
742 self._run(local_path)
746 class file_truncate(_pithos_container, _optional_output_cmd):
747 """Truncate remote file up to size"""
750 size_in_bytes=IntArgument('Length of file after truncation', '--size')
752 required = ('size_in_bytes', )
755 @errors.pithos.connection
756 @errors.pithos.container
757 @errors.pithos.object_path
758 @errors.pithos.object_size
759 def _run(self, size):
760 self._optional_output(self.client.truncate_object(self.path, size))
762 def main(self, path_or_url):
763 super(self.__class__, self)._run(path_or_url)
764 self._run(size=self['size_in_bytes'])
768 class file_overwrite(_pithos_container, _optional_output_cmd):
769 """Overwrite part of a remote file"""
772 progress_bar=ProgressBarArgument(
773 'do not show progress bar', ('-N', '--no-progress-bar'),
775 start_position=IntArgument('File position in bytes', '--from'),
776 end_position=IntArgument('File position in bytes', '--to')
778 required = ('start_position', 'end_position')
781 @errors.pithos.connection
782 @errors.pithos.container
783 @errors.pithos.object_path
784 @errors.pithos.object_size
785 def _run(self, local_path, start, end):
786 start, end = int(start), int(end)
787 (progress_bar, upload_cb) = self._safe_progress_bar(
788 'Overwrite %s bytes' % (end - start))
790 with open(path.abspath(local_path), 'rb') as f:
791 self._optional_output(self.client.overwrite_object(
796 upload_cb=upload_cb))
798 self._safe_progress_bar_finish(progress_bar)
800 def main(self, local_path, path_or_url):
801 super(self.__class__, self)._run(path_or_url)
802 self.path = self.path or path.basename(local_path)
804 local_path=local_path,
805 start=self['start_position'],
806 end=self['end_position'])
810 class file_upload(_pithos_container, _optional_output_cmd):
814 max_threads=IntArgument('default: 5', '--threads'),
815 content_encoding=ValueArgument(
816 'set MIME content type', '--content-encoding'),
817 content_disposition=ValueArgument(
818 'specify objects presentation style', '--content-disposition'),
819 content_type=ValueArgument('specify content type', '--content-type'),
820 uuid_for_read_permission=RepeatableArgument(
821 'Give read access to a user or group (can be repeated) '
822 'Use * for all users',
823 '--read-permission'),
824 uuid_for_write_permission=RepeatableArgument(
825 'Give write access to a user or group (can be repeated) '
826 'Use * for all users',
827 '--write-permission'),
828 public=FlagArgument('make object publicly accessible', '--public'),
829 progress_bar=ProgressBarArgument(
830 'do not show progress bar',
831 ('-N', '--no-progress-bar'),
833 overwrite=FlagArgument('Force (over)write', ('-f', '--force')),
834 recursive=FlagArgument(
835 'Recursively upload directory *contents* + subdirectories',
836 ('-r', '--recursive')),
837 unchunked=FlagArgument(
838 'Upload file as one block (not recommended)', '--unchunked'),
839 md5_checksum=ValueArgument(
840 'Confirm upload with a custom checksum (MD5)', '--etag'),
841 use_hashes=FlagArgument(
842 'Source file contains hashmap not data', '--source-is-hashmap'),
847 readlist = self['uuid_for_read_permission']
849 sharing['read'] = self['uuid_for_read_permission']
850 writelist = self['uuid_for_write_permission']
852 sharing['write'] = self['uuid_for_write_permission']
853 return sharing or None
855 def _check_container_limit(self, path):
856 cl_dict = self.client.get_container_limit()
857 container_limit = int(cl_dict['x-container-policy-quota'])
858 r = self.client.container_get()
859 used_bytes = sum(int(o['bytes']) for o in r.json)
860 path_size = get_path_size(path)
861 if container_limit and path_size > (container_limit - used_bytes):
863 'Container %s (limit(%s) - used(%s)) < (size(%s) of %s)' % (
864 self.client.container,
865 format_size(container_limit),
866 format_size(used_bytes),
867 format_size(path_size),
870 'Check accound limit: /file quota',
871 'Check container limit:',
872 '\t/file containerlimit get %s' % self.client.container,
873 'Increase container limit:',
874 '\t/file containerlimit set <new limit> %s' % (
875 self.client.container)])
877 def _src_dst(self, local_path, remote_path, objlist=None):
878 lpath = path.abspath(local_path)
879 short_path = path.basename(path.abspath(local_path))
880 rpath = remote_path or short_path
881 if path.isdir(lpath):
882 if not self['recursive']:
883 raise CLIError('%s is a directory' % lpath, details=[
884 'Use %s to upload directories & contents' % (
885 self.arguments['recursive'].lvalue)])
886 robj = self.client.container_get(path=rpath)
887 if not self['overwrite']:
890 'Objects/files prefixed as %s already exist' % rpath,
891 details=['Existing objects:'] + ['\t/%s/\t%s' % (
893 o['content_type'][12:]) for o in robj.json] + [
894 'Use -f to add, overwrite or resume'])
897 topobj = self.client.get_object_info(rpath)
898 if not self._is_dir(topobj):
900 'Object /%s/%s exists but not a directory' % (
901 self.container, rpath),
902 details=['Use -f to overwrite'])
903 except ClientError as ce:
904 if ce.status not in (404, ):
906 self._check_container_limit(lpath)
908 for top, subdirs, files in walk(lpath):
912 rel_path = rpath + top.split(lpath)[1]
915 self.error('mkdir /%s/%s' % (
916 self.client.container, rel_path))
917 self.client.create_directory(rel_path)
919 fpath = path.join(top, f)
920 if path.isfile(fpath):
921 rel_path = rel_path.replace(path.sep, '/')
922 pathfix = f.replace(path.sep, '/')
923 yield open(fpath, 'rb'), '%s/%s' % (rel_path, pathfix)
925 self.error('%s is not a regular file' % fpath)
927 if not path.isfile(lpath):
928 raise CLIError(('%s is not a regular file' % lpath) if (
929 path.exists(lpath)) else '%s does not exist' % lpath)
931 robj = self.client.get_object_info(rpath)
932 if remote_path and self._is_dir(robj):
933 rpath += '/%s' % (short_path.replace(path.sep, '/'))
934 self.client.get_object_info(rpath)
935 if not self['overwrite']:
937 'Object /%s/%s already exists' % (
938 self.container, rpath),
939 details=['use -f to overwrite / resume'])
940 except ClientError as ce:
941 if ce.status not in (404, ):
943 self._check_container_limit(lpath)
944 yield open(lpath, 'rb'), rpath
946 def _run(self, local_path, remote_path):
947 self.client.MAX_THREADS = int(self['max_threads'] or 5)
949 content_encoding=self['content_encoding'],
950 content_type=self['content_type'],
951 content_disposition=self['content_disposition'],
952 sharing=self._sharing(),
953 public=self['public'])
954 uploaded, container_info_cache = list, dict()
955 rpref = 'pithos://%s' if self['account'] else ''
956 for f, rpath in self._src_dst(local_path, remote_path):
957 self.error('%s --> %s/%s/%s' % (
958 f.name, rpref, self.client.container, rpath))
959 if not (self['content_type'] and self['content_encoding']):
960 ctype, cenc = guess_mime_type(f.name)
961 params['content_type'] = self['content_type'] or ctype
962 params['content_encoding'] = self['content_encoding'] or cenc
963 if self['unchunked']:
964 r = self.client.upload_object_unchunked(
966 etag=self['md5_checksum'], withHashFile=self['use_hashes'],
968 if self['with_output'] or self['json_output']:
969 r['name'] = '/%s/%s' % (self.client.container, rpath)
973 (progress_bar, upload_cb) = self._safe_progress_bar(
974 'Uploading %s' % f.name.split(path.sep)[-1])
976 hash_bar = progress_bar.clone()
977 hash_cb = hash_bar.get_generator(
978 'Calculating block hashes')
981 r = self.client.upload_object(
985 container_info_cache=container_info_cache,
987 if self['with_output'] or self['json_output']:
988 r['name'] = '/%s/%s' % (self.client.container, rpath)
991 self._safe_progress_bar_finish(progress_bar)
994 self._safe_progress_bar_finish(progress_bar)
995 self._optional_output(uploaded)
996 self.error('Upload completed')
998 def main(self, local_path, remote_path_or_url):
999 super(self.__class__, self)._run(remote_path_or_url)
1000 remote_path = self.path or path.basename(path.abspath(local_path))
1001 self._run(local_path=local_path, remote_path=remote_path)
1004 class RangeArgument(ValueArgument):
1006 :value type: string of the form <start>-<end> where <start> and <end> are
1008 :value returns: the input string, after type checking <start> and <end>
1013 return getattr(self, '_value', self.default)
1016 def value(self, newvalues):
1018 self._value = getattr(self, '_value', self.default)
1019 for newvalue in newvalues.split(','):
1020 self._value = ('%s,' % self._value) if self._value else ''
1021 start, sep, end = newvalue.partition('-')
1024 start, end = (int(start), int(end))
1026 raise CLIInvalidArgument(
1027 'Invalid range %s' % newvalue, details=[
1028 'Valid range formats',
1029 ' START-END', ' UP_TO', ' -FROM',
1030 'where all values are integers',
1031 'OR a compination (csv), e.g.,',
1032 ' %s=5,10-20,-5' % self.lvalue])
1033 self._value += '%s-%s' % (start, end)
1035 self._value += '-%s' % int(end)
1037 self._value += '%s' % int(start)
1041 class file_cat(_pithos_container):
1042 """Fetch remote file contents"""
1045 range=RangeArgument('show range of data e.g., 5,10-20,-5', '--range'),
1046 if_match=ValueArgument('show output if ETags match', '--if-match'),
1047 if_none_match=ValueArgument(
1048 'show output if ETags match', '--if-none-match'),
1049 if_modified_since=DateArgument(
1050 'show output modified since then', '--if-modified-since'),
1051 if_unmodified_since=DateArgument(
1052 'show output unmodified since then', '--if-unmodified-since'),
1053 object_version=ValueArgument(
1054 'Get contents of the chosen version', '--object-version')
1058 @errors.pithos.connection
1059 @errors.pithos.container
1060 @errors.pithos.object_path
1062 r = self.client.download_object(
1063 self.path, self._out,
1064 range_str=self['range'],
1065 version=self['object_version'],
1066 if_match=self['if_match'],
1067 if_none_match=self['if_none_match'],
1068 if_modified_since=self['if_modified_since'],
1069 if_unmodified_since=self['if_unmodified_since'])
1072 def main(self, path_or_url):
1073 super(self.__class__, self)._run(path_or_url)
1078 class file_download(_pithos_container):
1079 """Download a remove file or directory object to local file system"""
1082 resume=FlagArgument(
1083 'Resume/Overwrite (attempt resume, else overwrite)',
1084 ('-f', '--resume')),
1085 range=RangeArgument(
1086 'Download only that range of data e.g., 5,10-20,-5', '--range'),
1087 matching_etag=ValueArgument('download iff ETag match', '--if-match'),
1088 non_matching_etag=ValueArgument(
1089 'download iff ETags DO NOT match', '--if-none-match'),
1090 modified_since_date=DateArgument(
1091 'download iff remote file is modified since then',
1092 '--if-modified-since'),
1093 unmodified_since_date=DateArgument(
1094 'show output iff remote file is unmodified since then',
1095 '--if-unmodified-since'),
1096 object_version=ValueArgument(
1097 'download a file of a specific version', '--object-version'),
1098 max_threads=IntArgument('default: 5', '--threads'),
1099 progress_bar=ProgressBarArgument(
1100 'do not show progress bar', ('-N', '--no-progress-bar'),
1102 recursive=FlagArgument(
1103 'Download a remote directory object and its contents',
1104 ('-r', '--recursive'))
1107 def _src_dst(self, local_path):
1108 """Create a list of (src, dst) where src is a remote location and dst
1109 is an open file descriptor. Directories are denoted as (None, dirpath)
1110 and they are pretended to other objects in a very strict order (shorter
1115 obj = self.client.get_object_info(
1116 self.path, version=self['object_version'])
1117 obj.setdefault('name', self.path.strip('/'))
1120 except ClientError as ce:
1121 if ce.status in (404, ):
1122 raiseCLIError(ce, details=[
1123 'To download an object, it must exist either as a file or'
1125 'For example, to download everything under prefix/ the '
1126 'directory "prefix" must exist.',
1127 'To see if an remote object is actually there:',
1128 ' /file info [/CONTAINER/]OBJECT',
1129 'To create a directory object:',
1130 ' /file mkdir [/CONTAINER/]OBJECT'])
1131 if ce.status in (204, ):
1133 'No file or directory objects to download',
1135 'To download a container (e.g., %s):' % self.container,
1136 ' [kamaki] container download %s [LOCAL_PATH]' % (
1139 rpath = self.path.strip('/')
1140 if local_path and self.path and local_path.endswith('/'):
1141 local_path = local_path[-1:]
1143 if (not obj) or self._is_dir(obj):
1144 if self['recursive']:
1145 if not (self.path or local_path.endswith('/')):
1146 # Download the whole container
1147 local_path = '' if local_path in ('.', ) else local_path
1148 local_path = '%s/' % (local_path or self.container)
1150 name='', content_type='application/directory')
1151 dirs, files = [obj, ], []
1152 objects = self.client.container_get(
1154 if_modified_since=self['modified_since_date'],
1155 if_unmodified_since=self['unmodified_since_date'])
1156 for o in objects.json:
1157 (dirs if self._is_dir(o) else files).append(o)
1159 # Put the directories on top of the list
1160 for dpath in sorted(['%s%s' % (
1161 local_path, d['name'][len(rpath):]) for d in dirs]):
1162 if path.exists(dpath):
1163 if path.isdir(dpath):
1166 'Cannot replace local file %s with a directory '
1167 'of the same name' % dpath,
1169 'Either remove the file or specify a'
1170 'different target location'])
1171 ret.append((None, dpath, None))
1173 # Append the file objects
1174 for opath in [o['name'] for o in files]:
1175 lpath = '%s%s' % (local_path, opath[len(rpath):])
1177 fxists = path.exists(lpath)
1178 if fxists and path.isdir(lpath):
1180 'Cannot change local dir %s info file' % (
1183 'Either remove the file or specify a'
1184 'different target location'])
1185 ret.append((opath, lpath, fxists))
1186 elif path.exists(lpath):
1188 'Cannot overwrite %s' % lpath,
1189 details=['To overwrite/resume, use %s' % (
1190 self.arguments['resume'].lvalue)])
1192 ret.append((opath, lpath, None))
1195 'Remote object /%s/%s is a directory' % (
1196 self.container, local_path),
1197 details=['Use %s to download directories' % (
1198 self.arguments['recursive'].lvalue)])
1200 parsed_name = self.arguments['recursive'].lvalue
1202 'Cannot download container %s' % self.container,
1204 'Use %s to download containers' % parsed_name,
1205 ' [kamaki] file download %s /%s [LOCAL_PATH]' % (
1206 parsed_name, self.container)])
1208 # Remote object is just a file
1209 if path.exists(local_path) and not self['resume']:
1211 'Cannot overwrite local file %s' % (lpath),
1212 details=['To overwrite/resume, use %s' % (
1213 self.arguments['resume'].lvalue)])
1214 ret.append((rpath, local_path, self['resume']))
1215 for r, l, resume in ret:
1217 with open(l, 'rwb+' if resume else 'wb+') as f:
1223 @errors.pithos.connection
1224 @errors.pithos.container
1225 @errors.pithos.object_path
1226 @errors.pithos.local_path
1227 @errors.pithos.local_path_download
1228 def _run(self, local_path):
1229 self.client.MAX_THREADS = int(self['max_threads'] or 5)
1232 for rpath, output_file in self._src_dst(local_path):
1234 self.error('Create local directory %s' % output_file)
1235 makedirs(output_file)
1237 self.error('/%s/%s --> %s' % (
1238 self.container, rpath, output_file.name))
1239 progress_bar, download_cb = self._safe_progress_bar(
1241 self.client.download_object(
1243 download_cb=download_cb,
1244 range_str=self['range'],
1245 version=self['object_version'],
1246 if_match=self['matching_etag'],
1247 resume=self['resume'],
1248 if_none_match=self['non_matching_etag'],
1249 if_modified_since=self['modified_since_date'],
1250 if_unmodified_since=self['unmodified_since_date'])
1251 except KeyboardInterrupt:
1252 from threading import activeCount, enumerate as activethreads
1254 while activeCount() > 1:
1255 self._out.write('\nCancel %s threads: ' % (activeCount() - 1))
1257 for thread in activethreads():
1259 thread.join(timeout)
1260 self._out.write('.' if thread.isAlive() else '*')
1261 except RuntimeError:
1266 self.error('\nDownload canceled by user')
1267 if local_path is not None:
1268 self.error('to resume, re-run with --resume')
1270 self._safe_progress_bar_finish(progress_bar)
1273 self._safe_progress_bar_finish(progress_bar)
1275 def main(self, remote_path_or_url, local_path=None):
1276 super(self.__class__, self)._run(remote_path_or_url)
1277 local_path = local_path or self.path or '.'
1278 self._run(local_path=local_path)
1281 @command(container_cmds)
1282 class container_info(_pithos_account, _optional_json):
1283 """Get information about a container"""
1286 until_date=DateArgument('show metadata until then', '--until'),
1287 metadata=FlagArgument('Show only container metadata', '--metadata'),
1288 sizelimit=FlagArgument(
1289 'Show the maximum size limit for container', '--size-limit'),
1290 in_bytes=FlagArgument('Show size limit in bytes', ('-b', '--bytes'))
1294 @errors.pithos.connection
1295 @errors.pithos.container
1296 @errors.pithos.object_path
1298 if self['metadata']:
1299 r, preflen = dict(), len('x-container-meta-')
1300 for k, v in self.client.get_container_meta(
1301 until=self['until_date']).items():
1303 elif self['sizelimit']:
1304 r = self.client.get_container_limit(
1305 self.container)['x-container-policy-quota']
1306 r = {'size limit': 'unlimited' if r in ('0', ) else (
1307 int(r) if self['in_bytes'] else format_size(r))}
1309 r = self.client.get_container_info(self.container)
1310 self._print(r, self.print_dict)
1312 def main(self, container):
1313 super(self.__class__, self)._run()
1314 self.container, self.client.container = container, container
1318 class VersioningArgument(ValueArgument):
1320 schemes = ('auto', 'none')
1324 return getattr(self, '_value', None)
1327 def value(self, new_scheme):
1329 new_scheme = new_scheme.lower()
1330 if new_scheme not in self.schemes:
1331 raise CLIInvalidArgument('Invalid versioning value', details=[
1332 'Valid versioning values are %s' % ', '.join(
1334 self._value = new_scheme
1337 @command(container_cmds)
1338 class container_modify(_pithos_account, _optional_json):
1339 """Modify the properties of a container"""
1342 metadata_to_add=KeyValueArgument(
1343 'Add metadata in the form KEY=VALUE (can be repeated)',
1345 metadata_to_delete=RepeatableArgument(
1346 'Delete metadata by KEY (can be repeated)', '--metadata-del'),
1347 sizelimit=DataSizeArgument(
1348 'Set max size limit (0 for unlimited, '
1349 'use units B, KiB, KB, etc.)', '--size-limit'),
1350 versioning=VersioningArgument(
1351 'Set a versioning scheme (%s)' % ', '.join(
1352 VersioningArgument.schemes), '--versioning')
1355 'metadata_to_add', 'metadata_to_delete', 'sizelimit', 'versioning']
1358 @errors.pithos.connection
1359 @errors.pithos.container
1360 def _run(self, container):
1361 metadata = self['metadata_to_add']
1362 for k in (self['metadata_to_delete'] or []):
1365 self.client.set_container_meta(metadata)
1366 self._print(self.client.get_container_meta(), self.print_dict)
1367 if self['sizelimit'] is not None:
1368 self.client.set_container_limit(self['sizelimit'])
1369 r = self.client.get_container_limit()['x-container-policy-quota']
1370 r = 'unlimited' if r in ('0', ) else format_size(r)
1371 self.writeln('new size limit: %s' % r)
1372 if self['versioning']:
1373 self.client.set_container_versioning(self['versioning'])
1374 self.writeln('new versioning scheme: %s' % (
1375 self.client.get_container_versioning(self.container)[
1376 'x-container-policy-versioning']))
1378 def main(self, container):
1379 super(self.__class__, self)._run()
1380 self.client.container, self.container = container, container
1381 self._run(container=container)
1384 @command(container_cmds)
1385 class container_list(_pithos_account, _optional_json, _name_filter):
1386 """List all containers, or their contents"""
1389 detail=FlagArgument('Containers with details', ('-l', '--list')),
1390 limit=IntArgument('limit number of listed items', ('-n', '--number')),
1391 marker=ValueArgument('output greater that marker', '--marker'),
1392 modified_since_date=ValueArgument(
1393 'show output modified since then', '--if-modified-since'),
1394 unmodified_since_date=ValueArgument(
1395 'show output not modified since then', '--if-unmodified-since'),
1396 until_date=DateArgument('show metadata until then', '--until'),
1397 shared=FlagArgument('show only shared', '--shared'),
1398 more=FlagArgument('read long results', '--more'),
1399 enum=FlagArgument('Enumerate results', '--enumerate'),
1400 recursive=FlagArgument(
1401 'Recursively list containers and their contents',
1402 ('-r', '--recursive')),
1403 shared_by_me=FlagArgument(
1404 'show only files shared to other users', '--shared-by-me'),
1405 public=FlagArgument('show only published objects', '--public'),
1408 def print_containers(self, container_list):
1409 for index, container in enumerate(container_list):
1410 if 'bytes' in container:
1411 size = format_size(container['bytes'])
1412 prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
1413 _cname = container['name'] if (
1414 self['more']) else bold(container['name'])
1415 cname = u'%s%s' % (prfx, _cname)
1418 pretty_c = container.copy()
1419 if 'bytes' in container:
1420 pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
1421 self.print_dict(pretty_c, exclude=('name'))
1424 if 'count' in container and 'bytes' in container:
1425 self.writeln('%s (%s, %s objects)' % (
1426 cname, size, container['count']))
1429 objects = container.get('objects', [])
1431 self.print_objects(objects)
1434 def _create_object_forest(self, container_list):
1436 for container in container_list:
1437 self.client.container = container['name']
1438 objects = self.client.container_get(
1439 limit=False if self['more'] else self['limit'],
1440 if_modified_since=self['modified_since_date'],
1441 if_unmodified_since=self['unmodified_since_date'],
1442 until=self['until_date'],
1443 show_only_shared=self['shared_by_me'],
1444 public=self['public'])
1445 container['objects'] = objects.json
1447 self.client.container = None
1450 @errors.pithos.connection
1451 @errors.pithos.object_path
1452 @errors.pithos.container
1453 def _run(self, container):
1455 r = self.client.container_get(
1456 limit=False if self['more'] else self['limit'],
1457 marker=self['marker'],
1458 if_modified_since=self['modified_since_date'],
1459 if_unmodified_since=self['unmodified_since_date'],
1460 until=self['until_date'],
1461 show_only_shared=self['shared_by_me'],
1462 public=self['public'])
1464 r = self.client.account_get(
1465 limit=False if self['more'] else self['limit'],
1466 marker=self['marker'],
1467 if_modified_since=self['modified_since_date'],
1468 if_unmodified_since=self['unmodified_since_date'],
1469 until=self['until_date'],
1470 show_only_shared=self['shared_by_me'],
1471 public=self['public'])
1472 files = self._filter_by_name(r.json)
1473 if self['recursive'] and not container:
1474 self._create_object_forest(files)
1476 outbu, self._out = self._out, StringIO()
1478 if self['json_output'] or self['output_format']:
1481 (self.print_objects if container else self.print_containers)(
1485 pager(self._out.getvalue())
1488 def main(self, container=None):
1489 super(self.__class__, self)._run()
1490 self.client.container, self.container = container, container
1491 self._run(container)
1494 @command(container_cmds)
1495 class container_create(_pithos_account):
1496 """Create a new container"""
1499 versioning=ValueArgument(
1500 'set container versioning (auto/none)', '--versioning'),
1501 limit=IntArgument('set default container limit', '--limit'),
1502 meta=KeyValueArgument(
1503 'set container metadata (can be repeated)', '--meta')
1507 @errors.pithos.connection
1508 @errors.pithos.container
1509 def _run(self, container):
1511 self.client.create_container(
1512 container=container,
1513 sizelimit=self['limit'],
1514 versioning=self['versioning'],
1515 metadata=self['meta'],
1517 except ClientError as ce:
1518 if ce.status in (202, ):
1520 'Container %s alread exists' % container, details=[
1521 'Either delete %s or choose another name' % (container)])
1524 def main(self, new_container):
1525 super(self.__class__, self)._run()
1526 self._run(container=new_container)
1529 @command(container_cmds)
1530 class container_delete(_pithos_account):
1531 """Delete a container"""
1534 yes=FlagArgument('Do not prompt for permission', '--yes'),
1535 recursive=FlagArgument(
1536 'delete container even if not empty', ('-r', '--recursive'))
1540 @errors.pithos.connection
1541 @errors.pithos.container
1542 def _run(self, container):
1543 num_of_contents = int(self.client.get_container_info(container)[
1544 'x-container-object-count'])
1545 delimiter, msg = None, 'Delete container %s ?' % container
1546 if self['recursive']:
1547 delimiter, msg = '/', 'Empty and d%s' % msg[1:]
1548 elif num_of_contents:
1549 raise CLIError('Container %s is not empty' % container, details=[
1550 'Use %s to delete non-empty containers' % (
1551 self.arguments['recursive'].lvalue)])
1552 if self['yes'] or self.ask_user(msg):
1554 self.client.del_container(delimiter=delimiter)
1555 self.client.purge_container()
1557 def main(self, container):
1558 super(self.__class__, self)._run()
1559 self.container, self.client.container = container, container
1560 self._run(container)
1563 @command(container_cmds)
1564 class container_empty(_pithos_account):
1565 """Empty a container"""
1567 arguments = dict(yes=FlagArgument('Do not prompt for permission', '--yes'))
1570 @errors.pithos.connection
1571 @errors.pithos.container
1572 def _run(self, container):
1573 if self['yes'] or self.ask_user('Empty container %s ?' % container):
1574 self.client.del_container(delimiter='/')
1576 def main(self, container):
1577 super(self.__class__, self)._run()
1578 self.container, self.client.container = container, container
1579 self._run(container)
1582 @command(sharer_cmds)
1583 class sharer_list(_pithos_account, _optional_json):
1584 """List accounts who share file objects with current user"""
1587 detail=FlagArgument('show detailed output', ('-l', '--details')),
1588 marker=ValueArgument('show output greater then marker', '--marker')
1592 @errors.pithos.connection
1594 accounts = self.client.get_sharing_accounts(marker=self['marker'])
1595 if not (self['json_output'] or self['output_format']):
1596 usernames = self._uuids2usernames(
1597 [acc['name'] for acc in accounts])
1598 for item in accounts:
1600 item['id'], item['name'] = uuid, usernames[uuid]
1601 if not self['detail']:
1602 item.pop('last_modified')
1603 self._print(accounts)
1606 super(self.__class__, self)._run()
1610 @command(sharer_cmds)
1611 class sharer_info(_pithos_account, _optional_json):
1612 """Details on a Pithos+ sharer account (default: current account)"""
1615 @errors.pithos.connection
1617 self._print(self.client.get_account_info(), self.print_dict)
1619 def main(self, account_uuid=None):
1620 super(self.__class__, self)._run()
1622 self.client.account, self.account = account_uuid, account_uuid
1626 class _pithos_group(_pithos_account):
1627 prefix = 'x-account-group-'
1628 preflen = len(prefix)
1632 for k, v in self.client.get_account_group().items():
1633 groups[k[self.preflen:]] = v
1637 @command(group_cmds)
1638 class group_list(_pithos_group, _optional_json):
1639 """list all groups and group members"""
1642 @errors.pithos.connection
1644 self._print(self._groups(), self.print_dict)
1647 super(self.__class__, self)._run()
1651 @command(group_cmds)
1652 class group_create(_pithos_group, _optional_json):
1653 """Create a group of users"""
1656 user_uuid=RepeatableArgument('Add a user to the group', '--uuid'),
1657 username=RepeatableArgument('Add a user to the group', '--username')
1659 required = ['user_uuid', 'user_name']
1662 @errors.pithos.connection
1663 def _run(self, groupname, *users):
1664 if groupname in self._groups() and not self.ask_user(
1665 'Group %s already exists, overwrite?' % groupname):
1666 self.error('Aborted')
1668 self.client.set_account_group(groupname, users)
1669 self._print(self._groups(), self.print_dict)
1671 def main(self, groupname):
1672 super(self.__class__, self)._run()
1673 users = (self['user_uuid'] or []) + self._usernames2uuids(
1674 self['username'] or []).values()
1676 self._run(groupname, *users)
1678 raise CLISyntaxError(
1679 'No valid users specified, use %s or %s' % (
1680 self.arguments['user_uuid'].lvalue,
1681 self.arguments['username'].lvalue),
1683 'Check if a username or uuid is valid with',
1684 ' user uuid2username', 'OR', ' user username2uuid'])
1687 @command(group_cmds)
1688 class group_delete(_pithos_group, _optional_json):
1689 """Delete a user group"""
1692 @errors.pithos.connection
1693 def _run(self, groupname):
1694 self.client.del_account_group(groupname)
1695 self._print(self._groups(), self.print_dict)
1697 def main(self, groupname):
1698 super(self.__class__, self)._run()
1699 self._run(groupname)
1702 # Deprecated commands
1705 class file_publish(_pithos_init):
1706 """DEPRECATED, replaced by [kamaki] file modify OBJECT --publish"""
1708 def main(self, *args):
1709 raise CLISyntaxError('DEPRECATED', details=[
1710 'This command is replaced by:',
1711 ' [kamaki] file modify OBJECT --publish'])
1715 class file_unpublish(_pithos_init):
1716 """DEPRECATED, replaced by [kamaki] file modify OBJECT --unpublish"""
1718 def main(self, *args):
1719 raise CLISyntaxError('DEPRECATED', details=[
1720 'This command is replaced by:',
1721 ' [kamaki] file modify OBJECT --unpublish'])