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)
180 def _set_account(self):
181 astakos = AstakosClient(self.config.get('astakos', 'url'), self.token)
182 self.account = self['account'] or astakos.term('uuid')
184 """Backwards compatibility"""
185 self.account = self.account\
186 or self.config.get('store', 'account')\
187 or self.config.get('global', 'account')
190 class _store_account_command(_pithos_init):
191 """Base class for account level storage commands"""
193 def __init__(self, arguments={}):
194 super(_store_account_command, self).__init__(arguments)
195 self['account'] = ValueArgument(
196 'Set user account (not permanent)',
200 super(_store_account_command, self)._run()
202 self.client.account = self['account']
209 class _store_container_command(_store_account_command):
210 """Base class for container level storage commands"""
215 def __init__(self, arguments={}):
216 super(_store_container_command, self).__init__(arguments)
217 self['container'] = ValueArgument(
218 'Set container to work with (temporary)',
219 ('-C', '--container'))
221 def extract_container_and_path(
224 path_is_optional=True):
225 """Contains all heuristics for deciding what should be used as
226 container or path. Options are:
227 * user string of the form container:path
228 * self.container, self.path variables set by super constructor, or
229 explicitly by the caller application
230 Error handling is explicit as these error cases happen only here
233 assert isinstance(container_with_path, str)
234 except AssertionError as err:
235 if self['container'] and path_is_optional:
236 self.container = self['container']
237 self.client.container = self['container']
241 user_cont, sep, userpath = container_with_path.partition(':')
245 raiseCLIError(CLISyntaxError(
246 'Container is missing\n',
247 details=errors.pithos.container_howto))
248 alt_cont = self['container']
249 if alt_cont and user_cont != alt_cont:
250 raiseCLIError(CLISyntaxError(
251 'Conflict: 2 containers (%s, %s)' % (user_cont, alt_cont),
252 details=errors.pithos.container_howto)
254 self.container = user_cont
256 raiseCLIError(CLISyntaxError(
257 'Path is missing for object in container %s' % user_cont,
258 details=errors.pithos.container_howto)
262 alt_cont = self['container'] or self.client.container
264 self.container = alt_cont
265 self.path = user_cont
266 elif path_is_optional:
267 self.container = user_cont
270 self.container = user_cont
271 raiseCLIError(CLISyntaxError(
272 'Both container and path are required',
273 details=errors.pithos.container_howto)
277 def _run(self, container_with_path=None, path_is_optional=True):
278 super(_store_container_command, self)._run()
279 if self['container']:
280 self.client.container = self['container']
281 if container_with_path:
282 self.path = container_with_path
283 elif not path_is_optional:
284 raise CLISyntaxError(
285 'Both container and path are required',
286 details=errors.pithos.container_howto)
287 elif container_with_path:
288 self.extract_container_and_path(
291 self.client.container = self.container
292 self.container = self.client.container
294 def main(self, container_with_path=None, path_is_optional=True):
295 self._run(container_with_path, path_is_optional)
298 @command(pithos_cmds)
299 class store_list(_store_container_command):
300 """List containers, object trees or objects in a directory
302 1 no parameters : containers in current account
303 2. one parameter (container) or --container : contents of container
304 3. <container>:<prefix> or --container=<container> <prefix>: objects in
305 . container starting with prefix
309 detail=FlagArgument('detailed output', ('-l', '--list')),
310 limit=IntArgument('limit number of listed items', ('-n', '--number')),
311 marker=ValueArgument('output greater that marker', '--marker'),
312 prefix=ValueArgument('output starting with prefix', '--prefix'),
313 delimiter=ValueArgument('show output up to delimiter', '--delimiter'),
315 'show output starting with prefix up to /',
318 'show output with specified meta keys',
321 if_modified_since=ValueArgument(
322 'show output modified since then',
323 '--if-modified-since'),
324 if_unmodified_since=ValueArgument(
325 'show output not modified since then',
326 '--if-unmodified-since'),
327 until=DateArgument('show metadata until then', '--until'),
328 format=ValueArgument(
329 'format to parse until data (default: d/m/Y H:M:S )',
331 shared=FlagArgument('show only shared', '--shared'),
332 public=FlagArgument('show only public', '--public'),
334 'output results in pages (-n to set items per page, default 10)',
336 exact_match=FlagArgument(
337 'Show only objects that match exactly with path',
341 def print_objects(self, object_list):
342 limit = int(self['limit']) if self['limit'] > 0 else len(object_list)
343 for index, obj in enumerate(object_list):
344 if self['exact_match'] and self.path and not (
345 obj['name'] == self.path or 'content_type' in obj):
347 pretty_obj = obj.copy()
349 empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
350 if obj['content_type'] == 'application/directory':
355 size = format_size(obj['bytes'])
356 pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
357 oname = bold(obj['name'])
359 print('%s%s. %s' % (empty_space, index, oname))
360 print_dict(pretty_keys(pretty_obj), exclude=('name'))
363 oname = '%s%s. %6s %s' % (empty_space, index, size, oname)
364 oname += '/' if isDir else ''
367 page_hold(index, limit, len(object_list))
369 def print_containers(self, container_list):
370 limit = int(self['limit']) if self['limit'] > 0\
371 else len(container_list)
372 for index, container in enumerate(container_list):
373 if 'bytes' in container:
374 size = format_size(container['bytes'])
375 cname = '%s. %s' % (index + 1, bold(container['name']))
378 pretty_c = container.copy()
379 if 'bytes' in container:
380 pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
381 print_dict(pretty_keys(pretty_c), exclude=('name'))
384 if 'count' in container and 'bytes' in container:
385 print('%s (%s, %s objects)' % (
392 page_hold(index + 1, limit, len(container_list))
395 @errors.pithos.connection
396 @errors.pithos.object_path
397 @errors.pithos.container
399 if self.container is None:
400 r = self.client.account_get(
401 limit=False if self['more'] else self['limit'],
402 marker=self['marker'],
403 if_modified_since=self['if_modified_since'],
404 if_unmodified_since=self['if_unmodified_since'],
406 show_only_shared=self['shared'])
407 self.print_containers(r.json)
409 prefix = self.path or self['prefix']
410 r = self.client.container_get(
411 limit=False if self['more'] else self['limit'],
412 marker=self['marker'],
414 delimiter=self['delimiter'],
416 if_modified_since=self['if_modified_since'],
417 if_unmodified_since=self['if_unmodified_since'],
420 show_only_shared=self['shared'])
421 self.print_objects(r.json)
423 def main(self, container____path__=None):
424 super(self.__class__, self)._run(container____path__)
428 @command(pithos_cmds)
429 class store_mkdir(_store_container_command):
430 """Create a directory"""
432 __doc__ += '\n. '.join([
433 'Kamaki hanldes directories the same way as OOS Storage and Pithos+:',
434 'A directory is an object with type "application/directory"',
435 'An object with path dir/name can exist even if dir does not exist',
436 'or even if dir is a non directory object. Users can modify dir',
437 'without affecting the dir/name object in any way.'])
440 @errors.pithos.connection
441 @errors.pithos.container
443 self.client.create_directory(self.path)
445 def main(self, container___directory):
446 super(self.__class__, self)._run(
447 container___directory,
448 path_is_optional=False)
452 @command(pithos_cmds)
453 class store_touch(_store_container_command):
454 """Create an empty object (file)
455 If object exists, this command will reset it to 0 length
459 content_type=ValueArgument(
460 'Set content type (default: application/octet-stream)',
462 default='application/octet-stream')
466 @errors.pithos.connection
467 @errors.pithos.container
469 self.client.create_object(self.path, self['content_type'])
471 def main(self, container___path):
472 super(store_touch, self)._run(
474 path_is_optional=False)
478 @command(pithos_cmds)
479 class store_create(_store_container_command):
480 """Create a container"""
483 versioning=ValueArgument(
484 'set container versioning (auto/none)',
486 quota=IntArgument('set default container quota', '--quota'),
487 meta=KeyValueArgument(
488 'set container metadata (can be repeated)',
493 @errors.pithos.connection
494 @errors.pithos.container
496 self.client.container_put(
498 versioning=self['versioning'],
499 metadata=self['meta'])
501 def main(self, container=None):
502 super(self.__class__, self)._run(container)
503 if container and self.container != container:
504 raiseCLIError('Invalid container name %s' % container, details=[
505 'Did you mean "%s" ?' % self.container,
506 'Use --container for names containing :'])
510 class _source_destination_command(_store_container_command):
513 destination_account=ValueArgument('', ('a', '--dst-account')),
514 recursive=FlagArgument('', ('-R', '--recursive')),
515 prefix=FlagArgument('', '--with-prefix', default=''),
516 suffix=ValueArgument('', '--with-suffix', default=''),
517 add_prefix=ValueArgument('', '--add-prefix', default=''),
518 add_suffix=ValueArgument('', '--add-suffix', default=''),
519 prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
520 suffix_replace=ValueArgument('', '--suffix-to-replace', default='')
523 def __init__(self, arguments={}):
524 self.arguments.update(arguments)
525 super(_source_destination_command, self).__init__(self.arguments)
527 def _run(self, source_container___path, path_is_optional=False):
528 super(_source_destination_command, self)._run(
529 source_container___path,
531 self.dst_client = PithosClient(
532 base_url=self.client.base_url,
533 token=self.client.token,
534 account=self['destination_account'] or self.client.account)
537 @errors.pithos.account
538 def _dest_container_path(self, dest_container_path):
539 if self['destination_container']:
540 self.dst_client.container = self['destination_container']
541 return (self['destination_container'], dest_container_path)
542 if dest_container_path:
543 dst = dest_container_path.split(':')
546 self.dst_client.container = dst[0]
547 self.dst_client.get_container_info(dst[0])
548 except ClientError as err:
549 if err.status in (404, 204):
551 'Destination container %s not found' % dst[0])
554 self.dst_client.container = dst[0]
555 return (dst[0], dst[1])
557 raiseCLIError('No destination container:path provided')
559 def _get_all(self, prefix):
560 return self.client.container_get(prefix=prefix).json
562 def _get_src_objects(self, src_path):
563 """Get a list of the source objects to be called
565 :param src_path: (str) source path
567 :returns: (method, params) a method that returns a list when called
568 or (object) if it is a single object
570 if src_path and src_path[-1] == '/':
571 src_path = src_path[:-1]
574 return (self._get_all, dict(prefix=src_path))
576 srcobj = self.client.get_object_info(src_path)
577 except ClientError as srcerr:
578 if srcerr.status == 404:
580 'Source object %s not in source container %s' % (
582 self.client.container),
583 details=['Hint: --with-prefix to match multiple objects'])
584 elif srcerr.status not in (204,):
586 return (self.client.list_objects, {})
588 if self._is_dir(srcobj):
589 if not self['recursive']:
591 'Object %s of cont. %s is a dir' % (
593 self.client.container),
594 details=['Use --recursive to access directories'])
595 return (self._get_all, dict(prefix=src_path))
596 srcobj['name'] = src_path
599 def src_dst_pairs(self, dst_path):
600 src_iter = self._get_src_objects(self.path)
601 src_N = isinstance(src_iter, tuple)
602 add_prefix = self['add_prefix'].strip('/')
604 if dst_path and dst_path.endswith('/'):
605 dst_path = dst_path[:-1]
608 dstobj = self.dst_client.get_object_info(dst_path)
609 except ClientError as trgerr:
610 if trgerr.status in (404,):
613 'Cannot merge multiple paths to path %s' % dst_path,
615 'Try to use / or a directory as destination',
616 'or create the destination dir (/store mkdir)',
617 'or use a single object as source'])
618 elif trgerr.status not in (204,):
621 if self._is_dir(dstobj):
622 add_prefix = '%s/%s' % (dst_path.strip('/'), add_prefix)
625 'Cannot merge multiple paths to path' % dst_path,
627 'Try to use / or a directory as destination',
628 'or create the destination dir (/store mkdir)',
629 'or use a single object as source'])
632 (method, kwargs) = src_iter
633 for obj in method(**kwargs):
635 if name.endswith(self['suffix']):
636 yield (name, self._get_new_object(name, add_prefix))
637 elif src_iter['name'].endswith(self['suffix']):
638 name = src_iter['name']
639 yield (name, self._get_new_object(dst_path or name, add_prefix))
641 raiseCLIError('Source path %s conflicts with suffix %s' % (
645 def _get_new_object(self, obj, add_prefix):
646 if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
647 obj = obj[len(self['prefix_replace']):]
648 if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
649 obj = obj[:-len(self['suffix_replace'])]
650 return add_prefix + obj + self['add_suffix']
653 @command(pithos_cmds)
654 class store_copy(_source_destination_command):
655 """Copy objects from container to (another) container
658 . transfer path as dir/path
659 copy cont:path cont2:
660 . trasnfer all <obj> prefixed with path to container cont2
661 copy cont:path [cont2:]path2
662 . transfer path to path2
664 1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
665 destination is container1:path2
666 2. <container>:<path1> <path2> : make a copy in the same container
667 3. Can use --container= instead of <container1>
671 destination_account=ValueArgument(
672 'Account to copy to',
673 ('-a', '--dst-account')),
674 destination_container=ValueArgument(
675 'use it if destination container name contains a : character',
676 ('-D', '--dst-container')),
677 source_version=ValueArgument(
678 'copy specific version',
679 ('-S', '--source-version')),
680 public=ValueArgument('make object publicly accessible', '--public'),
681 content_type=ValueArgument(
682 'change object\'s content type',
684 recursive=FlagArgument(
685 'copy directory and contents',
686 ('-R', '--recursive')),
688 'Match objects prefixed with src path (feels like src_path*)',
691 suffix=ValueArgument(
692 'Suffix of source objects (feels like *suffix)',
695 add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
696 add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
697 prefix_replace=ValueArgument(
698 'Prefix of src to replace with dst path + add_prefix, if matched',
699 '--prefix-to-replace',
701 suffix_replace=ValueArgument(
702 'Suffix of src to replace with add_suffix, if matched',
703 '--suffix-to-replace',
708 @errors.pithos.connection
709 @errors.pithos.container
710 @errors.pithos.account
711 def _run(self, dst_path):
712 no_source_object = True
713 src_account = self.client.account if (
714 self['destination_account']) else None
715 for src_obj, dst_obj in self.src_dst_pairs(dst_path):
716 no_source_object = False
717 self.dst_client.copy_object(
718 src_container=self.client.container,
720 dst_container=self.dst_client.container,
722 source_account=src_account,
723 source_version=self['source_version'],
724 public=self['public'],
725 content_type=self['content_type'])
727 raiseCLIError('No object %s in container %s' % (
733 source_container___path,
734 destination_container___path=None):
735 super(store_copy, self)._run(
736 source_container___path,
737 path_is_optional=False)
738 (dst_cont, dst_path) = self._dest_container_path(
739 destination_container___path)
740 self.dst_client.container = dst_cont or self.container
741 self._run(dst_path=dst_path or '')
744 @command(pithos_cmds)
745 class store_move(_source_destination_command):
746 """Move/rename objects from container to (another) container
749 . rename path as dir/path
750 move cont:path cont2:
751 . trasnfer all <obj> prefixed with path to container cont2
752 move cont:path [cont2:]path2
753 . transfer path to path2
755 1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
756 destination is container1:path2
757 2. <container>:<path1> <path2> : move in the same container
758 3. Can use --container= instead of <container1>
762 destination_account=ValueArgument(
763 'Account to move to',
764 ('-a', '--dst-account')),
765 destination_container=ValueArgument(
766 'use it if destination container name contains a : character',
767 ('-D', '--dst-container')),
768 source_version=ValueArgument(
769 'copy specific version',
771 public=ValueArgument('make object publicly accessible', '--public'),
772 content_type=ValueArgument(
773 'change object\'s content type',
775 recursive=FlagArgument(
776 'copy directory and contents',
777 ('-R', '--recursive')),
779 'Match objects prefixed with src path (feels like src_path*)',
782 suffix=ValueArgument(
783 'Suffix of source objects (feels like *suffix)',
786 add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
787 add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
788 prefix_replace=ValueArgument(
789 'Prefix of src to replace with dst path + add_prefix, if matched',
790 '--prefix-to-replace',
792 suffix_replace=ValueArgument(
793 'Suffix of src to replace with add_suffix, if matched',
794 '--suffix-to-replace',
799 @errors.pithos.connection
800 @errors.pithos.container
801 def _run(self, dst_path):
802 no_source_object = True
803 src_account = self.client.account if (
804 self['destination_account']) else None
805 for src_obj, dst_obj in self.src_dst_pairs(dst_path):
806 no_source_object = False
807 self.dst_client.move_object(
808 src_container=self.container,
810 dst_container=self.dst_client.container,
812 source_account=src_account,
813 source_version=self['source_version'],
814 public=self['public'],
815 content_type=self['content_type'])
817 raiseCLIError('No object %s in container %s' % (
823 source_container___path,
824 destination_container___path=None):
825 super(self.__class__, self)._run(
826 source_container___path,
827 path_is_optional=False)
828 (dst_cont, dst_path) = self._dest_container_path(
829 destination_container___path)
830 (dst_cont, dst_path) = self._dest_container_path(
831 destination_container___path)
832 self.dst_client.container = dst_cont or self.container
833 self._run(dst_path=dst_path or '')
836 @command(pithos_cmds)
837 class store_append(_store_container_command):
838 """Append local file to (existing) remote object
839 The remote object should exist.
840 If the remote object is a directory, it is transformed into a file.
841 In the later case, objects under the directory remain intact.
845 progress_bar=ProgressBarArgument(
846 'do not show progress bar',
847 ('-N', '--no-progress-bar'),
852 @errors.pithos.connection
853 @errors.pithos.container
854 @errors.pithos.object_path
855 def _run(self, local_path):
856 (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
858 f = open(local_path, 'rb')
859 self.client.append_object(self.path, f, upload_cb)
861 self._safe_progress_bar_finish(progress_bar)
864 self._safe_progress_bar_finish(progress_bar)
866 def main(self, local_path, container___path):
867 super(self.__class__, self)._run(
869 path_is_optional=False)
870 self._run(local_path)
873 @command(pithos_cmds)
874 class store_truncate(_store_container_command):
875 """Truncate remote file up to a size (default is 0)"""
878 @errors.pithos.connection
879 @errors.pithos.container
880 @errors.pithos.object_path
881 @errors.pithos.object_size
882 def _run(self, size=0):
883 self.client.truncate_object(self.path, size)
885 def main(self, container___path, size=0):
886 super(self.__class__, self)._run(container___path)
890 @command(pithos_cmds)
891 class store_overwrite(_store_container_command):
892 """Overwrite part (from start to end) of a remote file
893 overwrite local-path container 10 20
894 . will overwrite bytes from 10 to 20 of a remote file with the same name
895 . as local-path basename
896 overwrite local-path container:path 10 20
897 . will overwrite as above, but the remote file is named path
901 progress_bar=ProgressBarArgument(
902 'do not show progress bar',
903 ('-N', '--no-progress-bar'),
907 def _open_file(self, local_path, start):
908 f = open(path.abspath(local_path), 'rb')
915 @errors.pithos.connection
916 @errors.pithos.container
917 @errors.pithos.object_path
918 @errors.pithos.object_size
919 def _run(self, local_path, start, end):
920 (start, end) = (int(start), int(end))
921 (f, f_size) = self._open_file(local_path, start)
922 (progress_bar, upload_cb) = self._safe_progress_bar(
923 'Overwrite %s bytes' % (end - start))
925 self.client.overwrite_object(
932 self._safe_progress_bar_finish(progress_bar)
935 self._safe_progress_bar_finish(progress_bar)
937 def main(self, local_path, container___path, start, end):
938 super(self.__class__, self)._run(
940 path_is_optional=None)
941 self.path = self.path or path.basename(local_path)
942 self._run(local_path=local_path, start=start, end=end)
945 @command(pithos_cmds)
946 class store_manifest(_store_container_command):
947 """Create a remote file of uploaded parts by manifestation
948 Remains functional for compatibility with OOS Storage. Users are advised
949 to use the upload command instead.
950 Manifestation is a compliant process for uploading large files. The files
951 have to be chunked in smalled files and uploaded as <prefix><increment>
952 where increment is 1, 2, ...
953 Finally, the manifest command glues partial files together in one file
955 The upload command is faster, easier and more intuitive than manifest
959 etag=ValueArgument('check written data', '--etag'),
960 content_encoding=ValueArgument(
961 'set MIME content type',
962 '--content-encoding'),
963 content_disposition=ValueArgument(
964 'the presentation style of the object',
965 '--content-disposition'),
966 content_type=ValueArgument(
967 'specify content type',
969 default='application/octet-stream'),
970 sharing=SharingArgument(
972 'define object sharing policy',
973 ' ( "read=user1,grp1,user2,... write=user1,grp2,..." )']),
975 public=FlagArgument('make object publicly accessible', '--public')
979 @errors.pithos.connection
980 @errors.pithos.container
981 @errors.pithos.object_path
983 self.client.create_object_by_manifestation(
985 content_encoding=self['content_encoding'],
986 content_disposition=self['content_disposition'],
987 content_type=self['content_type'],
988 sharing=self['sharing'],
989 public=self['public'])
991 def main(self, container___path):
992 super(self.__class__, self)._run(
994 path_is_optional=False)
998 @command(pithos_cmds)
999 class store_upload(_store_container_command):
1003 use_hashes=FlagArgument(
1004 'provide hashmap file instead of data',
1006 etag=ValueArgument('check written data', '--etag'),
1007 unchunked=FlagArgument('avoid chunked transfer mode', '--unchunked'),
1008 content_encoding=ValueArgument(
1009 'set MIME content type',
1010 '--content-encoding'),
1011 content_disposition=ValueArgument(
1012 'specify objects presentation style',
1013 '--content-disposition'),
1014 content_type=ValueArgument('specify content type', '--content-type'),
1015 sharing=SharingArgument(
1017 'define sharing object policy',
1018 '( "read=user1,grp1,user2,... write=user1,grp2,... )']),
1019 parsed_name='--sharing'),
1020 public=FlagArgument('make object publicly accessible', '--public'),
1021 poolsize=IntArgument('set pool size', '--with-pool-size'),
1022 progress_bar=ProgressBarArgument(
1023 'do not show progress bar',
1024 ('-N', '--no-progress-bar'),
1026 overwrite=FlagArgument('Force (over)write', ('-f', '--force'))
1029 def _remote_path(self, remote_path, local_path=''):
1030 if self['overwrite']:
1033 r = self.client.get_object_info(remote_path)
1034 except ClientError as ce:
1035 if ce.status == 404:
1038 ctype = r.get('content-type', '')
1039 if 'application/directory' == ctype.lower():
1040 ret = '%s/%s' % (remote_path, local_path)
1041 return self._remote_path(ret) if local_path else ret
1043 'Object %s already exists' % remote_path,
1045 details=['use -f to overwrite or resume'])
1048 @errors.pithos.connection
1049 @errors.pithos.container
1050 @errors.pithos.object_path
1051 @errors.pithos.local_path
1052 def _run(self, local_path, remote_path):
1053 poolsize = self['poolsize']
1055 self.client.MAX_THREADS = int(poolsize)
1057 content_encoding=self['content_encoding'],
1058 content_type=self['content_type'],
1059 content_disposition=self['content_disposition'],
1060 sharing=self['sharing'],
1061 public=self['public'])
1062 remote_path = self._remote_path(remote_path, local_path)
1063 with open(path.abspath(local_path), 'rb') as f:
1064 if self['unchunked']:
1065 self.client.upload_object_unchunked(
1069 withHashFile=self['use_hashes'],
1073 (progress_bar, upload_cb) = self._safe_progress_bar(
1076 hash_bar = progress_bar.clone()
1077 hash_cb = hash_bar.get_generator(
1078 'Calculating block hashes')
1081 self.client.upload_object(
1085 upload_cb=upload_cb,
1088 self._safe_progress_bar_finish(progress_bar)
1091 self._safe_progress_bar_finish(progress_bar)
1092 print 'Upload completed'
1094 def main(self, local_path, container____path__=None):
1095 super(self.__class__, self)._run(container____path__)
1096 remote_path = self.path or path.basename(local_path)
1097 self._run(local_path=local_path, remote_path=remote_path)
1100 @command(pithos_cmds)
1101 class store_cat(_store_container_command):
1102 """Print remote file contents to console"""
1105 range=RangeArgument('show range of data', '--range'),
1106 if_match=ValueArgument('show output if ETags match', '--if-match'),
1107 if_none_match=ValueArgument(
1108 'show output if ETags match',
1110 if_modified_since=DateArgument(
1111 'show output modified since then',
1112 '--if-modified-since'),
1113 if_unmodified_since=DateArgument(
1114 'show output unmodified since then',
1115 '--if-unmodified-since'),
1116 object_version=ValueArgument(
1117 'get the specific version',
1118 ('-j', '--object-version'))
1122 @errors.pithos.connection
1123 @errors.pithos.container
1124 @errors.pithos.object_path
1126 self.client.download_object(
1129 range_str=self['range'],
1130 version=self['object_version'],
1131 if_match=self['if_match'],
1132 if_none_match=self['if_none_match'],
1133 if_modified_since=self['if_modified_since'],
1134 if_unmodified_since=self['if_unmodified_since'])
1136 def main(self, container___path):
1137 super(self.__class__, self)._run(
1139 path_is_optional=False)
1143 @command(pithos_cmds)
1144 class store_download(_store_container_command):
1145 """Download remote object as local file
1146 If local destination is a directory:
1147 * download <container>:<path> <local dir> -R
1148 will download all files on <container> prefixed as <path>,
1149 to <local dir>/<full path>
1150 * download <container>:<path> <local dir> --exact-match
1151 will download only one file, exactly matching <path>
1152 ATTENTION: to download cont:dir1/dir2/file there must exist objects
1153 cont:dir1 and cont:dir1/dir2 of type application/directory
1154 To create directory objects, use /store mkdir
1158 resume=FlagArgument('Resume instead of overwrite', ('-r', '--resume')),
1159 range=RangeArgument('show range of data', '--range'),
1160 if_match=ValueArgument('show output if ETags match', '--if-match'),
1161 if_none_match=ValueArgument(
1162 'show output if ETags match',
1164 if_modified_since=DateArgument(
1165 'show output modified since then',
1166 '--if-modified-since'),
1167 if_unmodified_since=DateArgument(
1168 'show output unmodified since then',
1169 '--if-unmodified-since'),
1170 object_version=ValueArgument(
1171 'get the specific version',
1172 ('-j', '--object-version')),
1173 poolsize=IntArgument('set pool size', '--with-pool-size'),
1174 progress_bar=ProgressBarArgument(
1175 'do not show progress bar',
1176 ('-N', '--no-progress-bar'),
1178 recursive=FlagArgument(
1179 'Download a remote path and all its contents',
1180 ('-R', '--recursive'))
1183 def _outputs(self, local_path):
1184 """:returns: (local_file, remote_path)"""
1186 if self['recursive']:
1187 r = self.client.container_get(
1188 prefix=self.path or '/',
1189 if_modified_since=self['if_modified_since'],
1190 if_unmodified_since=self['if_unmodified_since'])
1192 for remote in r.json:
1193 rname = remote['name'].strip('/')
1195 for newdir in rname.strip('/').split('/')[:-1]:
1196 tmppath = '/'.join([tmppath, newdir])
1197 dirlist.update({tmppath.strip('/'): True})
1198 remotes.append((rname, store_download._is_dir(remote)))
1199 dir_remotes = [r[0] for r in remotes if r[1]]
1200 if not set(dirlist).issubset(dir_remotes):
1201 badguys = [bg.strip('/') for bg in set(
1202 dirlist).difference(dir_remotes)]
1204 'Some remote paths contain non existing directories',
1205 details=['Missing remote directories:'] + badguys)
1207 r = self.client.get_object_info(
1209 version=self['object_version'])
1210 if store_download._is_dir(r):
1212 'Illegal download: Remote object %s is a directory' % (
1214 details=['To download a directory, try --recursive'])
1215 if '/' in self.path.strip('/') and not local_path:
1217 'Illegal download: remote object %s contains "/"' % (
1220 'To download an object containing "/" characters',
1221 'either create the remote directories or',
1222 'specify a non-directory local path for this object'])
1223 remotes = [(self.path, False)]
1227 'No matching path %s on container %s' % (
1231 'To list the contents of %s, try:' % self.container,
1232 ' /store list %s' % self.container])
1234 'Illegal download of container %s' % self.container,
1236 'To download a whole container, try:',
1237 ' /store download --recursive <container>'])
1239 lprefix = path.abspath(local_path or path.curdir)
1240 if path.isdir(lprefix):
1241 for rpath, remote_is_dir in remotes:
1242 lpath = '/%s/%s' % (lprefix.strip('/'), rpath.strip('/'))
1244 if path.exists(lpath) and path.isdir(lpath):
1247 elif path.exists(lpath):
1248 if not self['resume']:
1249 print('File %s exists, aborting...' % lpath)
1251 with open(lpath, 'rwb+') as f:
1254 with open(lpath, 'wb+') as f:
1256 elif path.exists(lprefix):
1257 if len(remotes) > 1:
1259 '%s remote objects cannot be merged in local file %s' % (
1263 'To download multiple objects, local path should be',
1264 'a directory, or use download without a local path'])
1265 (rpath, remote_is_dir) = remotes[0]
1268 'Remote directory %s should not replace local file %s' % (
1272 with open(lprefix, 'rwb+') as f:
1276 'Local file %s already exist' % local_path,
1277 details=['Try --resume to overwrite it'])
1279 if len(remotes) > 1 or remotes[0][1]:
1281 'Local directory %s does not exist' % local_path)
1282 with open(lprefix, 'wb+') as f:
1283 yield (f, remotes[0][0])
1286 @errors.pithos.connection
1287 @errors.pithos.container
1288 @errors.pithos.object_path
1289 @errors.pithos.local_path
1290 def _run(self, local_path):
1291 #outputs = self._outputs(local_path)
1292 poolsize = self['poolsize']
1294 self.client.MAX_THREADS = int(poolsize)
1297 for f, rpath in self._outputs(local_path):
1300 download_cb) = self._safe_progress_bar(
1301 'Download %s' % rpath)
1302 self.client.download_object(
1305 download_cb=download_cb,
1306 range_str=self['range'],
1307 version=self['object_version'],
1308 if_match=self['if_match'],
1309 resume=self['resume'],
1310 if_none_match=self['if_none_match'],
1311 if_modified_since=self['if_modified_since'],
1312 if_unmodified_since=self['if_unmodified_since'])
1313 except KeyboardInterrupt:
1314 from threading import enumerate as activethreads
1315 stdout.write('\nFinishing active threads ')
1316 for thread in activethreads():
1321 except RuntimeError:
1323 print('\ndownload canceled by user')
1324 if local_path is not None:
1325 print('to resume, re-run with --resume')
1327 self._safe_progress_bar_finish(progress_bar)
1330 self._safe_progress_bar_finish(progress_bar)
1332 def main(self, container___path, local_path=None):
1333 super(self.__class__, self)._run(container___path)
1334 self._run(local_path=local_path)
1337 @command(pithos_cmds)
1338 class store_hashmap(_store_container_command):
1339 """Get the hash-map of an object"""
1342 if_match=ValueArgument('show output if ETags match', '--if-match'),
1343 if_none_match=ValueArgument(
1344 'show output if ETags match',
1346 if_modified_since=DateArgument(
1347 'show output modified since then',
1348 '--if-modified-since'),
1349 if_unmodified_since=DateArgument(
1350 'show output unmodified since then',
1351 '--if-unmodified-since'),
1352 object_version=ValueArgument(
1353 'get the specific version',
1354 ('-j', '--object-version'))
1358 @errors.pithos.connection
1359 @errors.pithos.container
1360 @errors.pithos.object_path
1362 data = self.client.get_object_hashmap(
1364 version=self['object_version'],
1365 if_match=self['if_match'],
1366 if_none_match=self['if_none_match'],
1367 if_modified_since=self['if_modified_since'],
1368 if_unmodified_since=self['if_unmodified_since'])
1371 def main(self, container___path):
1372 super(self.__class__, self)._run(
1374 path_is_optional=False)
1378 @command(pithos_cmds)
1379 class store_delete(_store_container_command):
1380 """Delete a container [or an object]
1381 How to delete a non-empty container:
1382 - empty the container: /store delete -R <container>
1383 - delete it: /store delete <container>
1385 Semantics of directory deletion:
1386 .a preserve the contents: /store delete <container>:<directory>
1387 . objects of the form dir/filename can exist with a dir object
1388 .b delete contents: /store delete -R <container>:<directory>
1389 . all dir/* objects are affected, even if dir does not exist
1391 To restore a deleted object OBJ in a container CONT:
1392 - get object versions: /store versions CONT:OBJ
1393 . and choose the version to be restored
1394 - restore the object: /store copy --source-version=<version> CONT:OBJ OBJ
1398 until=DateArgument('remove history until that date', '--until'),
1399 yes=FlagArgument('Do not prompt for permission', '--yes'),
1400 recursive=FlagArgument(
1401 'empty dir or container and delete (if dir)',
1402 ('-R', '--recursive'))
1405 def __init__(self, arguments={}):
1406 super(self.__class__, self).__init__(arguments)
1407 self['delimiter'] = DelimiterArgument(
1409 parsed_name='--delimiter',
1410 help='delete objects prefixed with <object><delimiter>')
1413 @errors.pithos.connection
1414 @errors.pithos.container
1415 @errors.pithos.object_path
1418 if self['yes'] or ask_user(
1419 'Delete %s:%s ?' % (self.container, self.path)):
1420 self.client.del_object(
1422 until=self['until'],
1423 delimiter=self['delimiter'])
1427 if self['recursive']:
1428 ask_msg = 'Delete container contents'
1430 ask_msg = 'Delete container'
1431 if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1432 self.client.del_container(
1433 until=self['until'],
1434 delimiter=self['delimiter'])
1438 def main(self, container____path__=None):
1439 super(self.__class__, self)._run(container____path__)
1443 @command(pithos_cmds)
1444 class store_purge(_store_container_command):
1445 """Delete a container and release related data blocks
1446 Non-empty containers can not purged.
1447 To purge a container with content:
1448 . /store delete -R <container>
1449 . objects are deleted, but data blocks remain on server
1450 . /store purge <container>
1451 . container and data blocks are released and deleted
1455 yes=FlagArgument('Do not prompt for permission', '--yes'),
1459 @errors.pithos.connection
1460 @errors.pithos.container
1462 if self['yes'] or ask_user('Purge container %s?' % self.container):
1463 self.client.purge_container()
1467 def main(self, container=None):
1468 super(self.__class__, self)._run(container)
1469 if container and self.container != container:
1470 raiseCLIError('Invalid container name %s' % container, details=[
1471 'Did you mean "%s" ?' % self.container,
1472 'Use --container for names containing :'])
1476 @command(pithos_cmds)
1477 class store_publish(_store_container_command):
1478 """Publish the object and print the public url"""
1481 @errors.pithos.connection
1482 @errors.pithos.container
1483 @errors.pithos.object_path
1485 url = self.client.publish_object(self.path)
1488 def main(self, container___path):
1489 super(self.__class__, self)._run(
1491 path_is_optional=False)
1495 @command(pithos_cmds)
1496 class store_unpublish(_store_container_command):
1497 """Unpublish an object"""
1500 @errors.pithos.connection
1501 @errors.pithos.container
1502 @errors.pithos.object_path
1504 self.client.unpublish_object(self.path)
1506 def main(self, container___path):
1507 super(self.__class__, self)._run(
1509 path_is_optional=False)
1513 @command(pithos_cmds)
1514 class store_permissions(_store_container_command):
1515 """Get read and write permissions of an object
1516 Permissions are lists of users and user groups. There is read and write
1517 permissions. Users and groups with write permission have also read
1522 @errors.pithos.connection
1523 @errors.pithos.container
1524 @errors.pithos.object_path
1526 r = self.client.get_object_sharing(self.path)
1529 def main(self, container___path):
1530 super(self.__class__, self)._run(
1532 path_is_optional=False)
1536 @command(pithos_cmds)
1537 class store_setpermissions(_store_container_command):
1538 """Set permissions for an object
1539 New permissions overwrite existing permissions.
1541 - read=<username>[,usergroup[,...]]
1542 - write=<username>[,usegroup[,...]]
1543 E.g. to give read permissions for file F to users A and B and write for C:
1544 . /store setpermissions F read=A,B write=C
1548 def format_permition_dict(self, permissions):
1551 for perms in permissions:
1552 splstr = perms.split('=')
1553 if 'read' == splstr[0]:
1554 read = [ug.strip() for ug in splstr[1].split(',')]
1555 elif 'write' == splstr[0]:
1556 write = [ug.strip() for ug in splstr[1].split(',')]
1558 msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1559 raiseCLIError(None, msg)
1560 return (read, write)
1563 @errors.pithos.connection
1564 @errors.pithos.container
1565 @errors.pithos.object_path
1566 def _run(self, read, write):
1567 self.client.set_object_sharing(
1569 read_permition=read,
1570 write_permition=write)
1572 def main(self, container___path, *permissions):
1573 super(self.__class__, self)._run(
1575 path_is_optional=False)
1576 (read, write) = self.format_permition_dict(permissions)
1577 self._run(read, write)
1580 @command(pithos_cmds)
1581 class store_delpermissions(_store_container_command):
1582 """Delete all permissions set on object
1583 To modify permissions, use /store setpermssions
1587 @errors.pithos.connection
1588 @errors.pithos.container
1589 @errors.pithos.object_path
1591 self.client.del_object_sharing(self.path)
1593 def main(self, container___path):
1594 super(self.__class__, self)._run(
1596 path_is_optional=False)
1600 @command(pithos_cmds)
1601 class store_info(_store_container_command):
1602 """Get detailed information for user account, containers or objects
1603 to get account info: /store info
1604 to get container info: /store info <container>
1605 to get object info: /store info <container>:<path>
1609 object_version=ValueArgument(
1610 'show specific version \ (applies only for objects)',
1611 ('-j', '--object-version'))
1615 @errors.pithos.connection
1616 @errors.pithos.container
1617 @errors.pithos.object_path
1619 if self.container is None:
1620 r = self.client.get_account_info()
1621 elif self.path is None:
1622 r = self.client.get_container_info(self.container)
1624 r = self.client.get_object_info(
1626 version=self['object_version'])
1629 def main(self, container____path__=None):
1630 super(self.__class__, self)._run(container____path__)
1634 @command(pithos_cmds)
1635 class store_meta(_store_container_command):
1636 """Get metadata for account, containers or objects"""
1639 detail=FlagArgument('show detailed output', ('-l', '--details')),
1640 until=DateArgument('show metadata until then', '--until'),
1641 object_version=ValueArgument(
1642 'show specific version \ (applies only for objects)',
1643 ('-j', '--object-version'))
1647 @errors.pithos.connection
1648 @errors.pithos.container
1649 @errors.pithos.object_path
1651 until = self['until']
1652 if self.container is None:
1654 r = self.client.get_account_info(until=until)
1656 r = self.client.get_account_meta(until=until)
1657 r = pretty_keys(r, '-')
1659 print(bold(self.client.account))
1660 elif self.path is None:
1662 r = self.client.get_container_info(until=until)
1664 cmeta = self.client.get_container_meta(until=until)
1665 ometa = self.client.get_container_object_meta(until=until)
1668 r['container-meta'] = pretty_keys(cmeta, '-')
1670 r['object-meta'] = pretty_keys(ometa, '-')
1673 r = self.client.get_object_info(
1675 version=self['object_version'])
1677 r = self.client.get_object_meta(
1679 version=self['object_version'])
1681 r = pretty_keys(pretty_keys(r, '-'))
1685 def main(self, container____path__=None):
1686 super(self.__class__, self)._run(container____path__)
1690 @command(pithos_cmds)
1691 class store_setmeta(_store_container_command):
1692 """Set a piece of metadata for account, container or object
1693 Metadata are formed as key:value pairs
1697 @errors.pithos.connection
1698 @errors.pithos.container
1699 @errors.pithos.object_path
1700 def _run(self, metakey, metaval):
1701 if not self.container:
1702 self.client.set_account_meta({metakey: metaval})
1704 self.client.set_container_meta({metakey: metaval})
1706 self.client.set_object_meta(self.path, {metakey: metaval})
1708 def main(self, metakey, metaval, container____path__=None):
1709 super(self.__class__, self)._run(container____path__)
1710 self._run(metakey=metakey, metaval=metaval)
1713 @command(pithos_cmds)
1714 class store_delmeta(_store_container_command):
1715 """Delete metadata with given key from account, container or object
1716 Metadata are formed as key:value objects
1717 - to get metadata of current account: /store meta
1718 - to get metadata of a container: /store meta <container>
1719 - to get metadata of an object: /store meta <container>:<path>
1723 @errors.pithos.connection
1724 @errors.pithos.container
1725 @errors.pithos.object_path
1726 def _run(self, metakey):
1727 if self.container is None:
1728 self.client.del_account_meta(metakey)
1729 elif self.path is None:
1730 self.client.del_container_meta(metakey)
1732 self.client.del_object_meta(self.path, metakey)
1734 def main(self, metakey, container____path__=None):
1735 super(self.__class__, self)._run(container____path__)
1739 @command(pithos_cmds)
1740 class store_quota(_store_account_command):
1741 """Get quota for account or container"""
1744 in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1748 @errors.pithos.connection
1749 @errors.pithos.container
1752 reply = self.client.get_container_quota(self.container)
1754 reply = self.client.get_account_quota()
1755 if not self['in_bytes']:
1757 reply[k] = format_size(reply[k])
1758 print_dict(pretty_keys(reply, '-'))
1760 def main(self, container=None):
1761 super(self.__class__, self)._run()
1762 self.container = container
1766 @command(pithos_cmds)
1767 class store_setquota(_store_account_command):
1768 """Set new quota for account or container
1769 By default, quota is set in bytes
1770 Users may specify a different unit, e.g:
1771 /store setquota 2.3GB mycontainer
1772 Accepted units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1776 def _calculate_quota(self, user_input):
1779 quota = int(user_input)
1782 digits = [str(num) for num in range(0, 10)] + ['.']
1783 while user_input[index] in digits:
1785 quota = user_input[:index]
1786 format = user_input[index:]
1788 return to_bytes(quota, format)
1789 except Exception as qe:
1790 msg = 'Failed to convert %s to bytes' % user_input,
1791 raiseCLIError(qe, msg, details=[
1792 'Syntax: setquota <quota>[format] [container]',
1793 'e.g.: setquota 2.3GB mycontainer',
1794 'Acceptable formats:',
1795 '(*1024): B, KiB, MiB, GiB, TiB',
1796 '(*1000): B, KB, MB, GB, TB'])
1800 @errors.pithos.connection
1801 @errors.pithos.container
1802 def _run(self, quota):
1804 self.client.container = self.container
1805 self.client.set_container_quota(quota)
1807 self.client.set_account_quota(quota)
1809 def main(self, quota, container=None):
1810 super(self.__class__, self)._run()
1811 quota = self._calculate_quota(quota)
1812 self.container = container
1816 @command(pithos_cmds)
1817 class store_versioning(_store_account_command):
1818 """Get versioning for account or container"""
1821 @errors.pithos.connection
1822 @errors.pithos.container
1825 r = self.client.get_container_versioning(self.container)
1827 r = self.client.get_account_versioning()
1830 def main(self, container=None):
1831 super(self.__class__, self)._run()
1832 self.container = container
1836 @command(pithos_cmds)
1837 class store_setversioning(_store_account_command):
1838 """Set versioning mode (auto, none) for account or container"""
1840 def _check_versioning(self, versioning):
1841 if versioning and versioning.lower() in ('auto', 'none'):
1842 return versioning.lower()
1843 raiseCLIError('Invalid versioning %s' % versioning, details=[
1844 'Versioning can be auto or none'])
1847 @errors.pithos.connection
1848 @errors.pithos.container
1849 def _run(self, versioning):
1851 self.client.container = self.container
1852 self.client.set_container_versioning(versioning)
1854 self.client.set_account_versioning(versioning)
1856 def main(self, versioning, container=None):
1857 super(self.__class__, self)._run()
1858 self._run(self._check_versioning(versioning))
1861 @command(pithos_cmds)
1862 class store_group(_store_account_command):
1863 """Get groups and group members"""
1866 @errors.pithos.connection
1868 r = self.client.get_account_group()
1869 print_dict(pretty_keys(r, '-'))
1872 super(self.__class__, self)._run()
1876 @command(pithos_cmds)
1877 class store_setgroup(_store_account_command):
1878 """Set a user group"""
1881 @errors.pithos.connection
1882 def _run(self, groupname, *users):
1883 self.client.set_account_group(groupname, users)
1885 def main(self, groupname, *users):
1886 super(self.__class__, self)._run()
1888 self._run(groupname, *users)
1890 raiseCLIError('No users to add in group %s' % groupname)
1893 @command(pithos_cmds)
1894 class store_delgroup(_store_account_command):
1895 """Delete a user group"""
1898 @errors.pithos.connection
1899 def _run(self, groupname):
1900 self.client.del_account_group(groupname)
1902 def main(self, groupname):
1903 super(self.__class__, self)._run()
1904 self._run(groupname)
1907 @command(pithos_cmds)
1908 class store_sharers(_store_account_command):
1909 """List the accounts that share objects with current user"""
1912 detail=FlagArgument('show detailed output', ('-l', '--details')),
1913 marker=ValueArgument('show output greater then marker', '--marker')
1917 @errors.pithos.connection
1919 accounts = self.client.get_sharing_accounts(marker=self['marker'])
1921 print_items(accounts)
1923 print_items([acc['name'] for acc in accounts])
1926 super(self.__class__, self)._run()
1930 @command(pithos_cmds)
1931 class store_versions(_store_container_command):
1932 """Get the list of object versions
1933 Deleted objects may still have versions that can be used to restore it and
1934 get information about its previous state.
1935 The version number can be used in a number of other commands, like info,
1936 copy, move, meta. See these commands for more information, e.g.
1941 @errors.pithos.connection
1942 @errors.pithos.container
1943 @errors.pithos.object_path
1945 versions = self.client.get_object_versionlist(self.path)
1946 print_items([dict(id=vitem[0], created=strftime(
1947 '%d-%m-%Y %H:%M:%S',
1948 localtime(float(vitem[1])))) for vitem in versions])
1950 def main(self, container___path):
1951 super(store_versions, self)._run(
1953 path_is_optional=False)