1 # Copyright 2011-2012 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 sys import stdout
35 from time import localtime, strftime
36 from logging import getLogger
37 from os import path, makedirs
39 from kamaki.cli import command
40 from kamaki.cli.command_tree import CommandTree
41 from kamaki.cli.errors import raiseCLIError, CLISyntaxError
42 from kamaki.cli.utils import (
51 from kamaki.cli.argument import FlagArgument, ValueArgument, IntArgument
52 from kamaki.cli.argument import KeyValueArgument, DateArgument
53 from kamaki.cli.argument import ProgressBarArgument
54 from kamaki.cli.commands import _command_init, errors
55 from kamaki.clients.pithos import PithosClient, ClientError
56 from kamaki.clients.astakos import AstakosClient
59 kloger = getLogger('kamaki')
61 pithos_cmds = CommandTree('store', 'Pithos+ storage commands')
62 _commands = [pithos_cmds]
65 # Argument functionality
67 class DelimiterArgument(ValueArgument):
70 :value returns: given string or /
73 def __init__(self, caller_obj, help='', parsed_name=None, default=None):
74 super(DelimiterArgument, self).__init__(help, parsed_name, default)
75 self.caller_obj = caller_obj
79 if self.caller_obj['recursive']:
81 return getattr(self, '_value', self.default)
84 def value(self, newvalue):
85 self._value = newvalue
88 class SharingArgument(ValueArgument):
89 """Set sharing (read and/or write) groups
91 :value type: "read=term1,term2,... write=term1,term2,..."
93 :value returns: {'read':['term1', 'term2', ...],
94 . 'write':['term1', 'term2', ...]}
99 return getattr(self, '_value', self.default)
102 def value(self, newvalue):
105 permlist = newvalue.split(' ')
106 except AttributeError:
110 (key, val) = p.split('=')
111 except ValueError as err:
114 'Error in --sharing',
115 details='Incorrect format',
117 if key.lower() not in ('read', 'write'):
118 msg = 'Error in --sharing'
119 raiseCLIError(err, msg, importance=1, details=[
120 'Invalid permission key %s' % key])
121 val_list = val.split(',')
124 for item in val_list:
125 if item not in perms[key]:
126 perms[key].append(item)
130 class RangeArgument(ValueArgument):
132 :value type: string of the form <start>-<end> where <start> and <end> are
134 :value returns: the input string, after type checking <start> and <end>
139 return getattr(self, '_value', self.default)
142 def value(self, newvalue):
144 self._value = self.default
146 (start, end) = newvalue.split('-')
147 (start, end) = (int(start), int(end))
148 self._value = '%s-%s' % (start, end)
153 class _pithos_init(_command_init):
154 """Initialize a pithos+ kamaki client"""
157 def _is_dir(remote_dict):
158 return 'application/directory' == remote_dict.get(
160 remote_dict.get('content-type', ''))
164 self.token = self.config.get('store', 'token')\
165 or self.config.get('global', 'token')
166 self.base_url = self.config.get('store', 'url')\
167 or self.config.get('global', 'url')
169 self.container = self.config.get('store', 'container')\
170 or self.config.get('global', 'container')
171 self.client = PithosClient(
172 base_url=self.base_url,
174 account=self.account,
175 container=self.container)
176 self._update_low_level_log()
177 self._update_max_threads()
182 def _set_account(self):
183 astakos = AstakosClient(self.config.get('astakos', 'url'), self.token)
184 self.account = self['account'] or astakos.term('uuid')
186 """Backwards compatibility"""
187 self.account = self.account\
188 or self.config.get('store', 'account')\
189 or self.config.get('global', 'account')
192 class _store_account_command(_pithos_init):
193 """Base class for account level storage commands"""
195 def __init__(self, arguments={}):
196 super(_store_account_command, self).__init__(arguments)
197 self['account'] = ValueArgument(
198 'Set user account (not permanent)',
202 super(_store_account_command, self)._run()
204 self.client.account = self['account']
211 class _store_container_command(_store_account_command):
212 """Base class for container level storage commands"""
217 def __init__(self, arguments={}):
218 super(_store_container_command, self).__init__(arguments)
219 self['container'] = ValueArgument(
220 'Set container to work with (temporary)',
221 ('-C', '--container'))
223 def extract_container_and_path(
226 path_is_optional=True):
227 """Contains all heuristics for deciding what should be used as
228 container or path. Options are:
229 * user string of the form container:path
230 * self.container, self.path variables set by super constructor, or
231 explicitly by the caller application
232 Error handling is explicit as these error cases happen only here
235 assert isinstance(container_with_path, str)
236 except AssertionError as err:
237 if self['container'] and path_is_optional:
238 self.container = self['container']
239 self.client.container = self['container']
243 user_cont, sep, userpath = container_with_path.partition(':')
247 raiseCLIError(CLISyntaxError(
248 'Container is missing\n',
249 details=errors.pithos.container_howto))
250 alt_cont = self['container']
251 if alt_cont and user_cont != alt_cont:
252 raiseCLIError(CLISyntaxError(
253 'Conflict: 2 containers (%s, %s)' % (user_cont, alt_cont),
254 details=errors.pithos.container_howto)
256 self.container = user_cont
258 raiseCLIError(CLISyntaxError(
259 'Path is missing for object in container %s' % user_cont,
260 details=errors.pithos.container_howto)
264 alt_cont = self['container'] or self.client.container
266 self.container = alt_cont
267 self.path = user_cont
268 elif path_is_optional:
269 self.container = user_cont
272 self.container = user_cont
273 raiseCLIError(CLISyntaxError(
274 'Both container and path are required',
275 details=errors.pithos.container_howto)
279 def _run(self, container_with_path=None, path_is_optional=True):
280 super(_store_container_command, self)._run()
281 if self['container']:
282 self.client.container = self['container']
283 if container_with_path:
284 self.path = container_with_path
285 elif not path_is_optional:
286 raise CLISyntaxError(
287 'Both container and path are required',
288 details=errors.pithos.container_howto)
289 elif container_with_path:
290 self.extract_container_and_path(
293 self.client.container = self.container
294 self.container = self.client.container
296 def main(self, container_with_path=None, path_is_optional=True):
297 self._run(container_with_path, path_is_optional)
300 @command(pithos_cmds)
301 class store_list(_store_container_command):
302 """List containers, object trees or objects in a directory
304 1 no parameters : containers in current account
305 2. one parameter (container) or --container : contents of container
306 3. <container>:<prefix> or --container=<container> <prefix>: objects in
307 . container starting with prefix
311 detail=FlagArgument('detailed output', ('-l', '--list')),
312 limit=IntArgument('limit number of listed items', ('-n', '--number')),
313 marker=ValueArgument('output greater that marker', '--marker'),
314 prefix=ValueArgument('output starting with prefix', '--prefix'),
315 delimiter=ValueArgument('show output up to delimiter', '--delimiter'),
317 'show output starting with prefix up to /',
320 'show output with specified meta keys',
323 if_modified_since=ValueArgument(
324 'show output modified since then',
325 '--if-modified-since'),
326 if_unmodified_since=ValueArgument(
327 'show output not modified since then',
328 '--if-unmodified-since'),
329 until=DateArgument('show metadata until then', '--until'),
330 format=ValueArgument(
331 'format to parse until data (default: d/m/Y H:M:S )',
333 shared=FlagArgument('show only shared', '--shared'),
335 'output results in pages (-n to set items per page, default 10)',
337 exact_match=FlagArgument(
338 'Show only objects that match exactly with path',
342 def print_objects(self, object_list):
343 limit = int(self['limit']) if self['limit'] > 0 else len(object_list)
344 for index, obj in enumerate(object_list):
345 if self['exact_match'] and self.path and not (
346 obj['name'] == self.path or 'content_type' in obj):
348 pretty_obj = obj.copy()
350 empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
351 if obj['content_type'] == 'application/directory':
356 size = format_size(obj['bytes'])
357 pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
358 oname = bold(obj['name'])
360 print('%s%s. %s' % (empty_space, index, oname))
361 print_dict(pretty_keys(pretty_obj), exclude=('name'))
364 oname = '%s%s. %6s %s' % (empty_space, index, size, oname)
365 oname += '/' if isDir else ''
368 page_hold(index, limit, len(object_list))
370 def print_containers(self, container_list):
371 limit = int(self['limit']) if self['limit'] > 0\
372 else len(container_list)
373 for index, container in enumerate(container_list):
374 if 'bytes' in container:
375 size = format_size(container['bytes'])
376 cname = '%s. %s' % (index + 1, bold(container['name']))
379 pretty_c = container.copy()
380 if 'bytes' in container:
381 pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
382 print_dict(pretty_keys(pretty_c), exclude=('name'))
385 if 'count' in container and 'bytes' in container:
386 print('%s (%s, %s objects)' % (
393 page_hold(index + 1, limit, len(container_list))
396 @errors.pithos.connection
397 @errors.pithos.object_path
398 @errors.pithos.container
400 if self.container is None:
401 r = self.client.account_get(
402 limit=False if self['more'] else self['limit'],
403 marker=self['marker'],
404 if_modified_since=self['if_modified_since'],
405 if_unmodified_since=self['if_unmodified_since'],
407 show_only_shared=self['shared'])
408 self.print_containers(r.json)
410 prefix = self.path or self['prefix']
411 r = self.client.container_get(
412 limit=False if self['more'] else self['limit'],
413 marker=self['marker'],
415 delimiter=self['delimiter'],
417 if_modified_since=self['if_modified_since'],
418 if_unmodified_since=self['if_unmodified_since'],
421 show_only_shared=self['shared'])
422 self.print_objects(r.json)
424 def main(self, container____path__=None):
425 super(self.__class__, self)._run(container____path__)
429 @command(pithos_cmds)
430 class store_mkdir(_store_container_command):
431 """Create a directory"""
433 __doc__ += '\n. '.join([
434 'Kamaki hanldes directories the same way as OOS Storage and Pithos+:',
435 'A directory is an object with type "application/directory"',
436 'An object with path dir/name can exist even if dir does not exist',
437 'or even if dir is a non directory object. Users can modify dir',
438 'without affecting the dir/name object in any way.'])
441 @errors.pithos.connection
442 @errors.pithos.container
444 self.client.create_directory(self.path)
446 def main(self, container___directory):
447 super(self.__class__, self)._run(
448 container___directory,
449 path_is_optional=False)
453 @command(pithos_cmds)
454 class store_touch(_store_container_command):
455 """Create an empty object (file)
456 If object exists, this command will reset it to 0 length
460 content_type=ValueArgument(
461 'Set content type (default: application/octet-stream)',
463 default='application/octet-stream')
467 @errors.pithos.connection
468 @errors.pithos.container
470 self.client.create_object(self.path, self['content_type'])
472 def main(self, container___path):
473 super(store_touch, self)._run(
475 path_is_optional=False)
479 @command(pithos_cmds)
480 class store_create(_store_container_command):
481 """Create a container"""
484 versioning=ValueArgument(
485 'set container versioning (auto/none)',
487 quota=IntArgument('set default container quota', '--quota'),
488 meta=KeyValueArgument(
489 'set container metadata (can be repeated)',
494 @errors.pithos.connection
495 @errors.pithos.container
497 self.client.container_put(
499 versioning=self['versioning'],
500 metadata=self['meta'])
502 def main(self, container=None):
503 super(self.__class__, self)._run(container)
504 if container and self.container != container:
505 raiseCLIError('Invalid container name %s' % container, details=[
506 'Did you mean "%s" ?' % self.container,
507 'Use --container for names containing :'])
511 class _source_destination_command(_store_container_command):
514 destination_account=ValueArgument('', ('a', '--dst-account')),
515 recursive=FlagArgument('', ('-R', '--recursive')),
516 prefix=FlagArgument('', '--with-prefix', default=''),
517 suffix=ValueArgument('', '--with-suffix', default=''),
518 add_prefix=ValueArgument('', '--add-prefix', default=''),
519 add_suffix=ValueArgument('', '--add-suffix', default=''),
520 prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
521 suffix_replace=ValueArgument('', '--suffix-to-replace', default='')
524 def __init__(self, arguments={}):
525 self.arguments.update(arguments)
526 super(_source_destination_command, self).__init__(self.arguments)
528 def _run(self, source_container___path, path_is_optional=False):
529 super(_source_destination_command, self)._run(
530 source_container___path,
532 self.dst_client = PithosClient(
533 base_url=self.client.base_url,
534 token=self.client.token,
535 account=self['destination_account'] or self.client.account)
538 @errors.pithos.account
539 def _dest_container_path(self, dest_container_path):
540 if self['destination_container']:
541 self.dst_client.container = self['destination_container']
542 return (self['destination_container'], dest_container_path)
543 if dest_container_path:
544 dst = dest_container_path.split(':')
547 self.dst_client.container = dst[0]
548 self.dst_client.get_container_info(dst[0])
549 except ClientError as err:
550 if err.status in (404, 204):
552 'Destination container %s not found' % dst[0])
555 self.dst_client.container = dst[0]
556 return (dst[0], dst[1])
558 raiseCLIError('No destination container:path provided')
560 def _get_all(self, prefix):
561 return self.client.container_get(prefix=prefix).json
563 def _get_src_objects(self, src_path):
564 """Get a list of the source objects to be called
566 :param src_path: (str) source path
568 :returns: (method, params) a method that returns a list when called
569 or (object) if it is a single object
571 if src_path and src_path[-1] == '/':
572 src_path = src_path[:-1]
575 return (self._get_all, dict(prefix=src_path))
577 srcobj = self.client.get_object_info(src_path)
578 except ClientError as srcerr:
579 if srcerr.status == 404:
581 'Source object %s not in source container %s' % (
583 self.client.container),
584 details=['Hint: --with-prefix to match multiple objects'])
585 elif srcerr.status not in (204,):
587 return (self.client.list_objects, {})
589 if self._is_dir(srcobj):
590 if not self['recursive']:
592 'Object %s of cont. %s is a dir' % (
594 self.client.container),
595 details=['Use --recursive to access directories'])
596 return (self._get_all, dict(prefix=src_path))
597 srcobj['name'] = src_path
600 def src_dst_pairs(self, dst_path):
601 src_iter = self._get_src_objects(self.path)
602 src_N = isinstance(src_iter, tuple)
603 add_prefix = self['add_prefix'].strip('/')
605 if dst_path and dst_path.endswith('/'):
606 dst_path = dst_path[:-1]
609 dstobj = self.dst_client.get_object_info(dst_path)
610 except ClientError as trgerr:
611 if trgerr.status in (404,):
614 'Cannot merge multiple paths to path %s' % dst_path,
616 'Try to use / or a directory as destination',
617 'or create the destination dir (/store mkdir)',
618 'or use a single object as source'])
619 elif trgerr.status not in (204,):
622 if self._is_dir(dstobj):
623 add_prefix = '%s/%s' % (dst_path.strip('/'), add_prefix)
626 'Cannot merge multiple paths to path' % dst_path,
628 'Try to use / or a directory as destination',
629 'or create the destination dir (/store mkdir)',
630 'or use a single object as source'])
633 (method, kwargs) = src_iter
634 for obj in method(**kwargs):
636 if name.endswith(self['suffix']):
637 yield (name, self._get_new_object(name, add_prefix))
638 elif src_iter['name'].endswith(self['suffix']):
639 name = src_iter['name']
640 yield (name, self._get_new_object(dst_path or name, add_prefix))
642 raiseCLIError('Source path %s conflicts with suffix %s' % (
646 def _get_new_object(self, obj, add_prefix):
647 if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
648 obj = obj[len(self['prefix_replace']):]
649 if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
650 obj = obj[:-len(self['suffix_replace'])]
651 return add_prefix + obj + self['add_suffix']
654 @command(pithos_cmds)
655 class store_copy(_source_destination_command):
656 """Copy objects from container to (another) container
659 . transfer path as dir/path
660 copy cont:path cont2:
661 . trasnfer all <obj> prefixed with path to container cont2
662 copy cont:path [cont2:]path2
663 . transfer path to path2
665 1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
666 destination is container1:path2
667 2. <container>:<path1> <path2> : make a copy in the same container
668 3. Can use --container= instead of <container1>
672 destination_account=ValueArgument(
673 'Account to copy to',
674 ('-a', '--dst-account')),
675 destination_container=ValueArgument(
676 'use it if destination container name contains a : character',
677 ('-D', '--dst-container')),
678 source_version=ValueArgument(
679 'copy specific version',
680 ('-S', '--source-version')),
681 public=ValueArgument('make object publicly accessible', '--public'),
682 content_type=ValueArgument(
683 'change object\'s content type',
685 recursive=FlagArgument(
686 'copy directory and contents',
687 ('-R', '--recursive')),
689 'Match objects prefixed with src path (feels like src_path*)',
692 suffix=ValueArgument(
693 'Suffix of source objects (feels like *suffix)',
696 add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
697 add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
698 prefix_replace=ValueArgument(
699 'Prefix of src to replace with dst path + add_prefix, if matched',
700 '--prefix-to-replace',
702 suffix_replace=ValueArgument(
703 'Suffix of src to replace with add_suffix, if matched',
704 '--suffix-to-replace',
709 @errors.pithos.connection
710 @errors.pithos.container
711 @errors.pithos.account
712 def _run(self, dst_path):
713 no_source_object = True
714 src_account = self.client.account if (
715 self['destination_account']) else None
716 for src_obj, dst_obj in self.src_dst_pairs(dst_path):
717 no_source_object = False
718 self.dst_client.copy_object(
719 src_container=self.client.container,
721 dst_container=self.dst_client.container,
723 source_account=src_account,
724 source_version=self['source_version'],
725 public=self['public'],
726 content_type=self['content_type'])
728 raiseCLIError('No object %s in container %s' % (
734 source_container___path,
735 destination_container___path=None):
736 super(store_copy, self)._run(
737 source_container___path,
738 path_is_optional=False)
739 (dst_cont, dst_path) = self._dest_container_path(
740 destination_container___path)
741 self.dst_client.container = dst_cont or self.container
742 self._run(dst_path=dst_path or '')
745 @command(pithos_cmds)
746 class store_move(_source_destination_command):
747 """Move/rename objects from container to (another) container
750 . rename path as dir/path
751 move cont:path cont2:
752 . trasnfer all <obj> prefixed with path to container cont2
753 move cont:path [cont2:]path2
754 . transfer path to path2
756 1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
757 destination is container1:path2
758 2. <container>:<path1> <path2> : move in the same container
759 3. Can use --container= instead of <container1>
763 destination_account=ValueArgument(
764 'Account to move to',
765 ('-a', '--dst-account')),
766 destination_container=ValueArgument(
767 'use it if destination container name contains a : character',
768 ('-D', '--dst-container')),
769 source_version=ValueArgument(
770 'copy specific version',
772 public=ValueArgument('make object publicly accessible', '--public'),
773 content_type=ValueArgument(
774 'change object\'s content type',
776 recursive=FlagArgument(
777 'copy directory and contents',
778 ('-R', '--recursive')),
780 'Match objects prefixed with src path (feels like src_path*)',
783 suffix=ValueArgument(
784 'Suffix of source objects (feels like *suffix)',
787 add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
788 add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
789 prefix_replace=ValueArgument(
790 'Prefix of src to replace with dst path + add_prefix, if matched',
791 '--prefix-to-replace',
793 suffix_replace=ValueArgument(
794 'Suffix of src to replace with add_suffix, if matched',
795 '--suffix-to-replace',
800 @errors.pithos.connection
801 @errors.pithos.container
802 def _run(self, dst_path):
803 no_source_object = True
804 src_account = self.client.account if (
805 self['destination_account']) else None
806 for src_obj, dst_obj in self.src_dst_pairs(dst_path):
807 no_source_object = False
808 self.dst_client.move_object(
809 src_container=self.container,
811 dst_container=self.dst_client.container,
813 source_account=src_account,
814 source_version=self['source_version'],
815 public=self['public'],
816 content_type=self['content_type'])
818 raiseCLIError('No object %s in container %s' % (
824 source_container___path,
825 destination_container___path=None):
826 super(self.__class__, self)._run(
827 source_container___path,
828 path_is_optional=False)
829 (dst_cont, dst_path) = self._dest_container_path(
830 destination_container___path)
831 (dst_cont, dst_path) = self._dest_container_path(
832 destination_container___path)
833 self.dst_client.container = dst_cont or self.container
834 self._run(dst_path=dst_path or '')
837 @command(pithos_cmds)
838 class store_append(_store_container_command):
839 """Append local file to (existing) remote object
840 The remote object should exist.
841 If the remote object is a directory, it is transformed into a file.
842 In the later case, objects under the directory remain intact.
846 progress_bar=ProgressBarArgument(
847 'do not show progress bar',
848 ('-N', '--no-progress-bar'),
853 @errors.pithos.connection
854 @errors.pithos.container
855 @errors.pithos.object_path
856 def _run(self, local_path):
857 (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
859 f = open(local_path, 'rb')
860 self.client.append_object(self.path, f, upload_cb)
862 self._safe_progress_bar_finish(progress_bar)
865 self._safe_progress_bar_finish(progress_bar)
867 def main(self, local_path, container___path):
868 super(self.__class__, self)._run(
870 path_is_optional=False)
871 self._run(local_path)
874 @command(pithos_cmds)
875 class store_truncate(_store_container_command):
876 """Truncate remote file up to a size (default is 0)"""
879 @errors.pithos.connection
880 @errors.pithos.container
881 @errors.pithos.object_path
882 @errors.pithos.object_size
883 def _run(self, size=0):
884 self.client.truncate_object(self.path, size)
886 def main(self, container___path, size=0):
887 super(self.__class__, self)._run(container___path)
891 @command(pithos_cmds)
892 class store_overwrite(_store_container_command):
893 """Overwrite part (from start to end) of a remote file
894 overwrite local-path container 10 20
895 . will overwrite bytes from 10 to 20 of a remote file with the same name
896 . as local-path basename
897 overwrite local-path container:path 10 20
898 . will overwrite as above, but the remote file is named path
902 progress_bar=ProgressBarArgument(
903 'do not show progress bar',
904 ('-N', '--no-progress-bar'),
908 def _open_file(self, local_path, start):
909 f = open(path.abspath(local_path), 'rb')
916 @errors.pithos.connection
917 @errors.pithos.container
918 @errors.pithos.object_path
919 @errors.pithos.object_size
920 def _run(self, local_path, start, end):
921 (start, end) = (int(start), int(end))
922 (f, f_size) = self._open_file(local_path, start)
923 (progress_bar, upload_cb) = self._safe_progress_bar(
924 'Overwrite %s bytes' % (end - start))
926 self.client.overwrite_object(
933 self._safe_progress_bar_finish(progress_bar)
936 self._safe_progress_bar_finish(progress_bar)
938 def main(self, local_path, container___path, start, end):
939 super(self.__class__, self)._run(
941 path_is_optional=None)
942 self.path = self.path or path.basename(local_path)
943 self._run(local_path=local_path, start=start, end=end)
946 @command(pithos_cmds)
947 class store_manifest(_store_container_command):
948 """Create a remote file of uploaded parts by manifestation
949 Remains functional for compatibility with OOS Storage. Users are advised
950 to use the upload command instead.
951 Manifestation is a compliant process for uploading large files. The files
952 have to be chunked in smalled files and uploaded as <prefix><increment>
953 where increment is 1, 2, ...
954 Finally, the manifest command glues partial files together in one file
956 The upload command is faster, easier and more intuitive than manifest
960 etag=ValueArgument('check written data', '--etag'),
961 content_encoding=ValueArgument(
962 'set MIME content type',
963 '--content-encoding'),
964 content_disposition=ValueArgument(
965 'the presentation style of the object',
966 '--content-disposition'),
967 content_type=ValueArgument(
968 'specify content type',
970 default='application/octet-stream'),
971 sharing=SharingArgument(
973 'define object sharing policy',
974 ' ( "read=user1,grp1,user2,... write=user1,grp2,..." )']),
976 public=FlagArgument('make object publicly accessible', '--public')
980 @errors.pithos.connection
981 @errors.pithos.container
982 @errors.pithos.object_path
984 self.client.create_object_by_manifestation(
986 content_encoding=self['content_encoding'],
987 content_disposition=self['content_disposition'],
988 content_type=self['content_type'],
989 sharing=self['sharing'],
990 public=self['public'])
992 def main(self, container___path):
993 super(self.__class__, self)._run(
995 path_is_optional=False)
999 @command(pithos_cmds)
1000 class store_upload(_store_container_command):
1004 use_hashes=FlagArgument(
1005 'provide hashmap file instead of data',
1007 etag=ValueArgument('check written data', '--etag'),
1008 unchunked=FlagArgument('avoid chunked transfer mode', '--unchunked'),
1009 content_encoding=ValueArgument(
1010 'set MIME content type',
1011 '--content-encoding'),
1012 content_disposition=ValueArgument(
1013 'specify objects presentation style',
1014 '--content-disposition'),
1015 content_type=ValueArgument('specify content type', '--content-type'),
1016 sharing=SharingArgument(
1018 'define sharing object policy',
1019 '( "read=user1,grp1,user2,... write=user1,grp2,... )']),
1020 parsed_name='--sharing'),
1021 public=FlagArgument('make object publicly accessible', '--public'),
1022 poolsize=IntArgument('set pool size', '--with-pool-size'),
1023 progress_bar=ProgressBarArgument(
1024 'do not show progress bar',
1025 ('-N', '--no-progress-bar'),
1027 overwrite=FlagArgument('Force (over)write', ('-f', '--force'))
1030 def _remote_path(self, remote_path, local_path=''):
1031 if self['overwrite']:
1034 r = self.client.get_object_info(remote_path)
1035 except ClientError as ce:
1036 if ce.status == 404:
1039 ctype = r.get('content-type', '')
1040 if 'application/directory' == ctype.lower():
1041 ret = '%s/%s' % (remote_path, local_path)
1042 return self._remote_path(ret) if local_path else ret
1044 'Object %s already exists' % remote_path,
1046 details=['use -f to overwrite or resume'])
1049 @errors.pithos.connection
1050 @errors.pithos.container
1051 @errors.pithos.object_path
1052 @errors.pithos.local_path
1053 def _run(self, local_path, remote_path):
1054 poolsize = self['poolsize']
1056 self.client.MAX_THREADS = int(poolsize)
1058 content_encoding=self['content_encoding'],
1059 content_type=self['content_type'],
1060 content_disposition=self['content_disposition'],
1061 sharing=self['sharing'],
1062 public=self['public'])
1063 remote_path = self._remote_path(remote_path, local_path)
1064 with open(path.abspath(local_path), 'rb') as f:
1065 if self['unchunked']:
1066 self.client.upload_object_unchunked(
1070 withHashFile=self['use_hashes'],
1074 (progress_bar, upload_cb) = self._safe_progress_bar(
1077 hash_bar = progress_bar.clone()
1078 hash_cb = hash_bar.get_generator(
1079 'Calculating block hashes')
1082 self.client.upload_object(
1086 upload_cb=upload_cb,
1089 self._safe_progress_bar_finish(progress_bar)
1092 self._safe_progress_bar_finish(progress_bar)
1093 print 'Upload completed'
1095 def main(self, local_path, container____path__=None):
1096 super(self.__class__, self)._run(container____path__)
1097 remote_path = self.path or path.basename(local_path)
1098 self._run(local_path=local_path, remote_path=remote_path)
1101 @command(pithos_cmds)
1102 class store_cat(_store_container_command):
1103 """Print remote file contents to console"""
1106 range=RangeArgument('show range of data', '--range'),
1107 if_match=ValueArgument('show output if ETags match', '--if-match'),
1108 if_none_match=ValueArgument(
1109 'show output if ETags match',
1111 if_modified_since=DateArgument(
1112 'show output modified since then',
1113 '--if-modified-since'),
1114 if_unmodified_since=DateArgument(
1115 'show output unmodified since then',
1116 '--if-unmodified-since'),
1117 object_version=ValueArgument(
1118 'get the specific version',
1119 ('-j', '--object-version'))
1123 @errors.pithos.connection
1124 @errors.pithos.container
1125 @errors.pithos.object_path
1127 self.client.download_object(
1130 range_str=self['range'],
1131 version=self['object_version'],
1132 if_match=self['if_match'],
1133 if_none_match=self['if_none_match'],
1134 if_modified_since=self['if_modified_since'],
1135 if_unmodified_since=self['if_unmodified_since'])
1137 def main(self, container___path):
1138 super(self.__class__, self)._run(
1140 path_is_optional=False)
1144 @command(pithos_cmds)
1145 class store_download(_store_container_command):
1146 """Download remote object as local file
1147 If local destination is a directory:
1148 * download <container>:<path> <local dir> -R
1149 will download all files on <container> prefixed as <path>,
1150 to <local dir>/<full path>
1151 * download <container>:<path> <local dir> --exact-match
1152 will download only one file, exactly matching <path>
1153 ATTENTION: to download cont:dir1/dir2/file there must exist objects
1154 cont:dir1 and cont:dir1/dir2 of type application/directory
1155 To create directory objects, use /store mkdir
1159 resume=FlagArgument('Resume instead of overwrite', ('-r', '--resume')),
1160 range=RangeArgument('show range of data', '--range'),
1161 if_match=ValueArgument('show output if ETags match', '--if-match'),
1162 if_none_match=ValueArgument(
1163 'show output if ETags match',
1165 if_modified_since=DateArgument(
1166 'show output modified since then',
1167 '--if-modified-since'),
1168 if_unmodified_since=DateArgument(
1169 'show output unmodified since then',
1170 '--if-unmodified-since'),
1171 object_version=ValueArgument(
1172 'get the specific version',
1173 ('-j', '--object-version')),
1174 poolsize=IntArgument('set pool size', '--with-pool-size'),
1175 progress_bar=ProgressBarArgument(
1176 'do not show progress bar',
1177 ('-N', '--no-progress-bar'),
1179 recursive=FlagArgument(
1180 'Download a remote path and all its contents',
1181 ('-R', '--recursive'))
1184 def _outputs(self, local_path):
1185 """:returns: (local_file, remote_path)"""
1187 if self['recursive']:
1188 r = self.client.container_get(
1189 prefix=self.path or '/',
1190 if_modified_since=self['if_modified_since'],
1191 if_unmodified_since=self['if_unmodified_since'])
1193 for remote in r.json:
1194 rname = remote['name'].strip('/')
1196 for newdir in rname.strip('/').split('/')[:-1]:
1197 tmppath = '/'.join([tmppath, newdir])
1198 dirlist.update({tmppath.strip('/'): True})
1199 remotes.append((rname, store_download._is_dir(remote)))
1200 dir_remotes = [r[0] for r in remotes if r[1]]
1201 if not set(dirlist).issubset(dir_remotes):
1202 badguys = [bg.strip('/') for bg in set(
1203 dirlist).difference(dir_remotes)]
1205 'Some remote paths contain non existing directories',
1206 details=['Missing remote directories:'] + badguys)
1208 r = self.client.get_object_info(
1210 version=self['object_version'])
1211 if store_download._is_dir(r):
1213 'Illegal download: Remote object %s is a directory' % (
1215 details=['To download a directory, try --recursive'])
1216 if '/' in self.path.strip('/') and not local_path:
1218 'Illegal download: remote object %s contains "/"' % (
1221 'To download an object containing "/" characters',
1222 'either create the remote directories or',
1223 'specify a non-directory local path for this object'])
1224 remotes = [(self.path, False)]
1228 'No matching path %s on container %s' % (
1232 'To list the contents of %s, try:' % self.container,
1233 ' /store list %s' % self.container])
1235 'Illegal download of container %s' % self.container,
1237 'To download a whole container, try:',
1238 ' /store download --recursive <container>'])
1240 lprefix = path.abspath(local_path or path.curdir)
1241 if path.isdir(lprefix):
1242 for rpath, remote_is_dir in remotes:
1243 lpath = '/%s/%s' % (lprefix.strip('/'), rpath.strip('/'))
1245 if path.exists(lpath) and path.isdir(lpath):
1248 elif path.exists(lpath):
1249 if not self['resume']:
1250 print('File %s exists, aborting...' % lpath)
1252 with open(lpath, 'rwb+') as f:
1255 with open(lpath, 'wb+') as f:
1257 elif path.exists(lprefix):
1258 if len(remotes) > 1:
1260 '%s remote objects cannot be merged in local file %s' % (
1264 'To download multiple objects, local path should be',
1265 'a directory, or use download without a local path'])
1266 (rpath, remote_is_dir) = remotes[0]
1269 'Remote directory %s should not replace local file %s' % (
1273 with open(lprefix, 'rwb+') as f:
1277 'Local file %s already exist' % local_path,
1278 details=['Try --resume to overwrite it'])
1280 if len(remotes) > 1 or remotes[0][1]:
1282 'Local directory %s does not exist' % local_path)
1283 with open(lprefix, 'wb+') as f:
1284 yield (f, remotes[0][0])
1287 @errors.pithos.connection
1288 @errors.pithos.container
1289 @errors.pithos.object_path
1290 @errors.pithos.local_path
1291 def _run(self, local_path):
1292 #outputs = self._outputs(local_path)
1293 poolsize = self['poolsize']
1295 self.client.MAX_THREADS = int(poolsize)
1298 for f, rpath in self._outputs(local_path):
1301 download_cb) = self._safe_progress_bar(
1302 'Download %s' % rpath)
1303 self.client.download_object(
1306 download_cb=download_cb,
1307 range_str=self['range'],
1308 version=self['object_version'],
1309 if_match=self['if_match'],
1310 resume=self['resume'],
1311 if_none_match=self['if_none_match'],
1312 if_modified_since=self['if_modified_since'],
1313 if_unmodified_since=self['if_unmodified_since'])
1314 except KeyboardInterrupt:
1315 from threading import activeCount, enumerate as activethreads
1316 while activeCount() > 1:
1317 stdout.write('\nTry stop %s threads: ' % (activeCount() - 1))
1318 for thread in activethreads():
1323 except RuntimeError:
1325 print('\nDownload canceled by user')
1326 if local_path is not None:
1327 print('to resume, re-run with --resume')
1329 self._safe_progress_bar_finish(progress_bar)
1332 self._safe_progress_bar_finish(progress_bar)
1334 def main(self, container___path, local_path=None):
1335 super(self.__class__, self)._run(container___path)
1336 self._run(local_path=local_path)
1339 @command(pithos_cmds)
1340 class store_hashmap(_store_container_command):
1341 """Get the hash-map of an object"""
1344 if_match=ValueArgument('show output if ETags match', '--if-match'),
1345 if_none_match=ValueArgument(
1346 'show output if ETags match',
1348 if_modified_since=DateArgument(
1349 'show output modified since then',
1350 '--if-modified-since'),
1351 if_unmodified_since=DateArgument(
1352 'show output unmodified since then',
1353 '--if-unmodified-since'),
1354 object_version=ValueArgument(
1355 'get the specific version',
1356 ('-j', '--object-version'))
1360 @errors.pithos.connection
1361 @errors.pithos.container
1362 @errors.pithos.object_path
1364 data = self.client.get_object_hashmap(
1366 version=self['object_version'],
1367 if_match=self['if_match'],
1368 if_none_match=self['if_none_match'],
1369 if_modified_since=self['if_modified_since'],
1370 if_unmodified_since=self['if_unmodified_since'])
1373 def main(self, container___path):
1374 super(self.__class__, self)._run(
1376 path_is_optional=False)
1380 @command(pithos_cmds)
1381 class store_delete(_store_container_command):
1382 """Delete a container [or an object]
1383 How to delete a non-empty container:
1384 - empty the container: /store delete -R <container>
1385 - delete it: /store delete <container>
1387 Semantics of directory deletion:
1388 .a preserve the contents: /store delete <container>:<directory>
1389 . objects of the form dir/filename can exist with a dir object
1390 .b delete contents: /store delete -R <container>:<directory>
1391 . all dir/* objects are affected, even if dir does not exist
1393 To restore a deleted object OBJ in a container CONT:
1394 - get object versions: /store versions CONT:OBJ
1395 . and choose the version to be restored
1396 - restore the object: /store copy --source-version=<version> CONT:OBJ OBJ
1400 until=DateArgument('remove history until that date', '--until'),
1401 yes=FlagArgument('Do not prompt for permission', '--yes'),
1402 recursive=FlagArgument(
1403 'empty dir or container and delete (if dir)',
1404 ('-R', '--recursive'))
1407 def __init__(self, arguments={}):
1408 super(self.__class__, self).__init__(arguments)
1409 self['delimiter'] = DelimiterArgument(
1411 parsed_name='--delimiter',
1412 help='delete objects prefixed with <object><delimiter>')
1415 @errors.pithos.connection
1416 @errors.pithos.container
1417 @errors.pithos.object_path
1420 if self['yes'] or ask_user(
1421 'Delete %s:%s ?' % (self.container, self.path)):
1422 self.client.del_object(
1424 until=self['until'],
1425 delimiter=self['delimiter'])
1429 if self['recursive']:
1430 ask_msg = 'Delete container contents'
1432 ask_msg = 'Delete container'
1433 if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1434 self.client.del_container(
1435 until=self['until'],
1436 delimiter=self['delimiter'])
1440 def main(self, container____path__=None):
1441 super(self.__class__, self)._run(container____path__)
1445 @command(pithos_cmds)
1446 class store_purge(_store_container_command):
1447 """Delete a container and release related data blocks
1448 Non-empty containers can not purged.
1449 To purge a container with content:
1450 . /store delete -R <container>
1451 . objects are deleted, but data blocks remain on server
1452 . /store purge <container>
1453 . container and data blocks are released and deleted
1457 yes=FlagArgument('Do not prompt for permission', '--yes'),
1461 @errors.pithos.connection
1462 @errors.pithos.container
1464 if self['yes'] or ask_user('Purge container %s?' % self.container):
1465 self.client.purge_container()
1469 def main(self, container=None):
1470 super(self.__class__, self)._run(container)
1471 if container and self.container != container:
1472 raiseCLIError('Invalid container name %s' % container, details=[
1473 'Did you mean "%s" ?' % self.container,
1474 'Use --container for names containing :'])
1478 @command(pithos_cmds)
1479 class store_publish(_store_container_command):
1480 """Publish the object and print the public url"""
1483 @errors.pithos.connection
1484 @errors.pithos.container
1485 @errors.pithos.object_path
1487 url = self.client.publish_object(self.path)
1490 def main(self, container___path):
1491 super(self.__class__, self)._run(
1493 path_is_optional=False)
1497 @command(pithos_cmds)
1498 class store_unpublish(_store_container_command):
1499 """Unpublish an object"""
1502 @errors.pithos.connection
1503 @errors.pithos.container
1504 @errors.pithos.object_path
1506 self.client.unpublish_object(self.path)
1508 def main(self, container___path):
1509 super(self.__class__, self)._run(
1511 path_is_optional=False)
1515 @command(pithos_cmds)
1516 class store_permissions(_store_container_command):
1517 """Get read and write permissions of an object
1518 Permissions are lists of users and user groups. There is read and write
1519 permissions. Users and groups with write permission have also read
1524 @errors.pithos.connection
1525 @errors.pithos.container
1526 @errors.pithos.object_path
1528 r = self.client.get_object_sharing(self.path)
1531 def main(self, container___path):
1532 super(self.__class__, self)._run(
1534 path_is_optional=False)
1538 @command(pithos_cmds)
1539 class store_setpermissions(_store_container_command):
1540 """Set permissions for an object
1541 New permissions overwrite existing permissions.
1543 - read=<username>[,usergroup[,...]]
1544 - write=<username>[,usegroup[,...]]
1545 E.g. to give read permissions for file F to users A and B and write for C:
1546 . /store setpermissions F read=A,B write=C
1550 def format_permition_dict(self, permissions):
1553 for perms in permissions:
1554 splstr = perms.split('=')
1555 if 'read' == splstr[0]:
1556 read = [ug.strip() for ug in splstr[1].split(',')]
1557 elif 'write' == splstr[0]:
1558 write = [ug.strip() for ug in splstr[1].split(',')]
1560 msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1561 raiseCLIError(None, msg)
1562 return (read, write)
1565 @errors.pithos.connection
1566 @errors.pithos.container
1567 @errors.pithos.object_path
1568 def _run(self, read, write):
1569 self.client.set_object_sharing(
1571 read_permition=read,
1572 write_permition=write)
1574 def main(self, container___path, *permissions):
1575 super(self.__class__, self)._run(
1577 path_is_optional=False)
1578 (read, write) = self.format_permition_dict(permissions)
1579 self._run(read, write)
1582 @command(pithos_cmds)
1583 class store_delpermissions(_store_container_command):
1584 """Delete all permissions set on object
1585 To modify permissions, use /store setpermssions
1589 @errors.pithos.connection
1590 @errors.pithos.container
1591 @errors.pithos.object_path
1593 self.client.del_object_sharing(self.path)
1595 def main(self, container___path):
1596 super(self.__class__, self)._run(
1598 path_is_optional=False)
1602 @command(pithos_cmds)
1603 class store_info(_store_container_command):
1604 """Get detailed information for user account, containers or objects
1605 to get account info: /store info
1606 to get container info: /store info <container>
1607 to get object info: /store info <container>:<path>
1611 object_version=ValueArgument(
1612 'show specific version \ (applies only for objects)',
1613 ('-j', '--object-version'))
1617 @errors.pithos.connection
1618 @errors.pithos.container
1619 @errors.pithos.object_path
1621 if self.container is None:
1622 r = self.client.get_account_info()
1623 elif self.path is None:
1624 r = self.client.get_container_info(self.container)
1626 r = self.client.get_object_info(
1628 version=self['object_version'])
1631 def main(self, container____path__=None):
1632 super(self.__class__, self)._run(container____path__)
1636 @command(pithos_cmds)
1637 class store_meta(_store_container_command):
1638 """Get metadata for account, containers or objects"""
1641 detail=FlagArgument('show detailed output', ('-l', '--details')),
1642 until=DateArgument('show metadata until then', '--until'),
1643 object_version=ValueArgument(
1644 'show specific version \ (applies only for objects)',
1645 ('-j', '--object-version'))
1649 @errors.pithos.connection
1650 @errors.pithos.container
1651 @errors.pithos.object_path
1653 until = self['until']
1654 if self.container is None:
1656 r = self.client.get_account_info(until=until)
1658 r = self.client.get_account_meta(until=until)
1659 r = pretty_keys(r, '-')
1661 print(bold(self.client.account))
1662 elif self.path is None:
1664 r = self.client.get_container_info(until=until)
1666 cmeta = self.client.get_container_meta(until=until)
1667 ometa = self.client.get_container_object_meta(until=until)
1670 r['container-meta'] = pretty_keys(cmeta, '-')
1672 r['object-meta'] = pretty_keys(ometa, '-')
1675 r = self.client.get_object_info(
1677 version=self['object_version'])
1679 r = self.client.get_object_meta(
1681 version=self['object_version'])
1683 r = pretty_keys(pretty_keys(r, '-'))
1687 def main(self, container____path__=None):
1688 super(self.__class__, self)._run(container____path__)
1692 @command(pithos_cmds)
1693 class store_setmeta(_store_container_command):
1694 """Set a piece of metadata for account, container or object
1695 Metadata are formed as key:value pairs
1699 @errors.pithos.connection
1700 @errors.pithos.container
1701 @errors.pithos.object_path
1702 def _run(self, metakey, metaval):
1703 if not self.container:
1704 self.client.set_account_meta({metakey: metaval})
1706 self.client.set_container_meta({metakey: metaval})
1708 self.client.set_object_meta(self.path, {metakey: metaval})
1710 def main(self, metakey, metaval, container____path__=None):
1711 super(self.__class__, self)._run(container____path__)
1712 self._run(metakey=metakey, metaval=metaval)
1715 @command(pithos_cmds)
1716 class store_delmeta(_store_container_command):
1717 """Delete metadata with given key from account, container or object
1718 Metadata are formed as key:value objects
1719 - to get metadata of current account: /store meta
1720 - to get metadata of a container: /store meta <container>
1721 - to get metadata of an object: /store meta <container>:<path>
1725 @errors.pithos.connection
1726 @errors.pithos.container
1727 @errors.pithos.object_path
1728 def _run(self, metakey):
1729 if self.container is None:
1730 self.client.del_account_meta(metakey)
1731 elif self.path is None:
1732 self.client.del_container_meta(metakey)
1734 self.client.del_object_meta(self.path, metakey)
1736 def main(self, metakey, container____path__=None):
1737 super(self.__class__, self)._run(container____path__)
1741 @command(pithos_cmds)
1742 class store_quota(_store_account_command):
1743 """Get quota for account or container"""
1746 in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1750 @errors.pithos.connection
1751 @errors.pithos.container
1754 reply = self.client.get_container_quota(self.container)
1756 reply = self.client.get_account_quota()
1757 if not self['in_bytes']:
1759 reply[k] = format_size(reply[k])
1760 print_dict(pretty_keys(reply, '-'))
1762 def main(self, container=None):
1763 super(self.__class__, self)._run()
1764 self.container = container
1768 @command(pithos_cmds)
1769 class store_setquota(_store_account_command):
1770 """Set new quota for account or container
1771 By default, quota is set in bytes
1772 Users may specify a different unit, e.g:
1773 /store setquota 2.3GB mycontainer
1774 Accepted units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1778 def _calculate_quota(self, user_input):
1781 quota = int(user_input)
1784 digits = [str(num) for num in range(0, 10)] + ['.']
1785 while user_input[index] in digits:
1787 quota = user_input[:index]
1788 format = user_input[index:]
1790 return to_bytes(quota, format)
1791 except Exception as qe:
1792 msg = 'Failed to convert %s to bytes' % user_input,
1793 raiseCLIError(qe, msg, details=[
1794 'Syntax: setquota <quota>[format] [container]',
1795 'e.g.: setquota 2.3GB mycontainer',
1796 'Acceptable formats:',
1797 '(*1024): B, KiB, MiB, GiB, TiB',
1798 '(*1000): B, KB, MB, GB, TB'])
1802 @errors.pithos.connection
1803 @errors.pithos.container
1804 def _run(self, quota):
1806 self.client.container = self.container
1807 self.client.set_container_quota(quota)
1809 self.client.set_account_quota(quota)
1811 def main(self, quota, container=None):
1812 super(self.__class__, self)._run()
1813 quota = self._calculate_quota(quota)
1814 self.container = container
1818 @command(pithos_cmds)
1819 class store_versioning(_store_account_command):
1820 """Get versioning for account or container"""
1823 @errors.pithos.connection
1824 @errors.pithos.container
1827 r = self.client.get_container_versioning(self.container)
1829 r = self.client.get_account_versioning()
1832 def main(self, container=None):
1833 super(self.__class__, self)._run()
1834 self.container = container
1838 @command(pithos_cmds)
1839 class store_setversioning(_store_account_command):
1840 """Set versioning mode (auto, none) for account or container"""
1842 def _check_versioning(self, versioning):
1843 if versioning and versioning.lower() in ('auto', 'none'):
1844 return versioning.lower()
1845 raiseCLIError('Invalid versioning %s' % versioning, details=[
1846 'Versioning can be auto or none'])
1849 @errors.pithos.connection
1850 @errors.pithos.container
1851 def _run(self, versioning):
1853 self.client.container = self.container
1854 self.client.set_container_versioning(versioning)
1856 self.client.set_account_versioning(versioning)
1858 def main(self, versioning, container=None):
1859 super(self.__class__, self)._run()
1860 self._run(self._check_versioning(versioning))
1863 @command(pithos_cmds)
1864 class store_group(_store_account_command):
1865 """Get groups and group members"""
1868 @errors.pithos.connection
1870 r = self.client.get_account_group()
1871 print_dict(pretty_keys(r, '-'))
1874 super(self.__class__, self)._run()
1878 @command(pithos_cmds)
1879 class store_setgroup(_store_account_command):
1880 """Set a user group"""
1883 @errors.pithos.connection
1884 def _run(self, groupname, *users):
1885 self.client.set_account_group(groupname, users)
1887 def main(self, groupname, *users):
1888 super(self.__class__, self)._run()
1890 self._run(groupname, *users)
1892 raiseCLIError('No users to add in group %s' % groupname)
1895 @command(pithos_cmds)
1896 class store_delgroup(_store_account_command):
1897 """Delete a user group"""
1900 @errors.pithos.connection
1901 def _run(self, groupname):
1902 self.client.del_account_group(groupname)
1904 def main(self, groupname):
1905 super(self.__class__, self)._run()
1906 self._run(groupname)
1909 @command(pithos_cmds)
1910 class store_sharers(_store_account_command):
1911 """List the accounts that share objects with current user"""
1914 detail=FlagArgument('show detailed output', ('-l', '--details')),
1915 marker=ValueArgument('show output greater then marker', '--marker')
1919 @errors.pithos.connection
1921 accounts = self.client.get_sharing_accounts(marker=self['marker'])
1923 print_items(accounts)
1925 print_items([acc['name'] for acc in accounts])
1928 super(self.__class__, self)._run()
1932 @command(pithos_cmds)
1933 class store_versions(_store_container_command):
1934 """Get the list of object versions
1935 Deleted objects may still have versions that can be used to restore it and
1936 get information about its previous state.
1937 The version number can be used in a number of other commands, like info,
1938 copy, move, meta. See these commands for more information, e.g.
1943 @errors.pithos.connection
1944 @errors.pithos.container
1945 @errors.pithos.object_path
1947 versions = self.client.get_object_versionlist(self.path)
1948 print_items([dict(id=vitem[0], created=strftime(
1949 '%d-%m-%Y %H:%M:%S',
1950 localtime(float(vitem[1])))) for vitem in versions])
1952 def main(self, container___path):
1953 super(store_versions, self)._run(
1955 path_is_optional=False)