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
58 kloger = getLogger('kamaki')
60 pithos_cmds = CommandTree('store', 'Pithos+ storage commands')
61 _commands = [pithos_cmds]
65 'Kamaki hanldes directories the same way as OOS Storage and Pithos+:',
66 'A directory is an object with type "application/directory"',
67 'An object with path dir/name can exist even if dir does not exist or',
68 'even if dir is a non directory object. Users can modify dir without',
69 'affecting the dir/name object in any way.']
72 # Argument functionality
74 def raise_connection_errors(e):
75 if e.status in range(200) + [403, 401]:
76 raiseCLIError(e, details=[
77 'Please check the service url and the authentication information',
79 ' to get the service url: /config get store.url',
80 ' to set the service url: /config set store.url <url>',
82 ' to get user the account: /config get store.account',
83 ' or /config get account',
84 ' to set the user account: /config set store.account <account>',
86 ' to get authentication token: /config get token',
87 ' to set authentication token: /config set token <token>'
90 raiseCLIError(e, details=[
92 '- total quota: /store quota',
93 '- container quota: /store quota <container>',
94 'Users shall set a higher container quota, if available:',
95 '- /store setquota <quota>[unit] <container>'
99 class DelimiterArgument(ValueArgument):
102 :value returns: given string or /
105 def __init__(self, caller_obj, help='', parsed_name=None, default=None):
106 super(DelimiterArgument, self).__init__(help, parsed_name, default)
107 self.caller_obj = caller_obj
111 if self.caller_obj['recursive']:
113 return getattr(self, '_value', self.default)
116 def value(self, newvalue):
117 self._value = newvalue
120 class SharingArgument(ValueArgument):
121 """Set sharing (read and/or write) groups
123 :value type: "read=term1,term2,... write=term1,term2,..."
125 :value returns: {'read':['term1', 'term2', ...],
126 . 'write':['term1', 'term2', ...]}
131 return getattr(self, '_value', self.default)
134 def value(self, newvalue):
137 permlist = newvalue.split(' ')
138 except AttributeError:
142 (key, val) = p.split('=')
143 except ValueError as err:
144 raiseCLIError(err, 'Error in --sharing',
145 details='Incorrect format',
147 if key.lower() not in ('read', 'write'):
148 raiseCLIError(err, 'Error in --sharing',
149 details='Invalid permission key %s' % key,
151 val_list = val.split(',')
154 for item in val_list:
155 if item not in perms[key]:
156 perms[key].append(item)
160 class RangeArgument(ValueArgument):
162 :value type: string of the form <start>-<end> where <start> and <end> are
164 :value returns: the input string, after type checking <start> and <end>
169 return getattr(self, '_value', self.default)
172 def value(self, newvalue):
174 self._value = self.default
176 (start, end) = newvalue.split('-')
177 (start, end) = (int(start), int(end))
178 self._value = '%s-%s' % (start, end)
183 class _pithos_init(_command_init):
184 """Initialize a pithos+ kamaki client"""
188 self.token = self.config.get('store', 'token')\
189 or self.config.get('global', 'token')
190 self.base_url = self.config.get('store', 'url')\
191 or self.config.get('global', 'url')
192 self.account = self.config.get('store', 'account')\
193 or self.config.get('global', 'account')
194 self.container = self.config.get('store', 'container')\
195 or self.config.get('global', 'container')
196 self.client = PithosClient(base_url=self.base_url,
198 account=self.account,
199 container=self.container)
205 class _store_account_command(_pithos_init):
206 """Base class for account level storage commands"""
208 def __init__(self, arguments={}):
209 super(_store_account_command, self).__init__(arguments)
210 self['account'] = ValueArgument(
211 'Set user account (not permanent)',
215 super(_store_account_command, self)._run()
217 self.client.account = self['account']
224 class _store_container_command(_store_account_command):
225 """Base class for container level storage commands"""
230 def __init__(self, arguments={}):
231 super(_store_container_command, self).__init__(arguments)
232 self['container'] = ValueArgument(
233 'Set container to work with (temporary)',
237 def _dest_container_path(self, dest_container_path):
238 if self['destination_container']:
239 return (self['destination_container'], dest_container_path)
240 dst = dest_container_path.split(':')
241 return (dst[0], dst[1]) if len(dst) > 1 else (None, dst[0])
243 def extract_container_and_path(self,
245 path_is_optional=True):
246 """Contains all heuristics for deciding what should be used as
247 container or path. Options are:
248 * user string of the form container:path
249 * self.container, self.path variables set by super constructor, or
250 explicitly by the caller application
251 Error handling is explicit as these error cases happen only here
254 assert isinstance(container_with_path, str)
255 except AssertionError as err:
256 if self['container'] and path_is_optional:
257 self.container = self['container']
258 self.client.container = self['container']
262 user_cont, sep, userpath = container_with_path.partition(':')
266 raiseCLIError(CLISyntaxError('Container is missing\n',
267 details=errors.pithos.container_howto))
268 alt_cont = self['container']
269 if alt_cont and user_cont != alt_cont:
270 raiseCLIError(CLISyntaxError(
271 'Conflict: 2 containers (%s, %s)' % (user_cont, alt_cont),
272 details=errors.pithos.container_howto)
274 self.container = user_cont
276 raiseCLIError(CLISyntaxError(
277 'Path is missing for object in container %s' % user_cont,
278 details=errors.pithos.container_howto)
282 alt_cont = self['container'] or self.client.container
284 self.container = alt_cont
285 self.path = user_cont
286 elif path_is_optional:
287 self.container = user_cont
290 self.container = user_cont
291 raiseCLIError(CLISyntaxError(
292 'Both container and path are required',
293 details=errors.pithos.container_howto)
297 def _run(self, container_with_path=None, path_is_optional=True):
298 super(_store_container_command, self)._run()
299 if self['container']:
300 self.client.container = self['container']
301 if container_with_path:
302 self.path = container_with_path
303 elif not path_is_optional:
304 raise CLISyntaxError(
305 'Both container and path are required',
306 details=errors.pithos.container_howto)
307 elif container_with_path:
308 self.extract_container_and_path(
311 self.client.container = self.container
312 self.container = self.client.container
314 def main(self, container_with_path=None, path_is_optional=True):
315 self._run(container_with_path, path_is_optional)
318 @command(pithos_cmds)
319 class store_list(_store_container_command):
320 """List containers, object trees or objects in a directory
322 1 no parameters : containers in set account
323 2. one parameter (container) or --container : contents of container
324 3. <container>:<prefix> or --container=<container> <prefix>: objects in
325 . container starting with prefix
329 detail=FlagArgument('show detailed output', '-l'),
330 limit=IntArgument('limit the number of listed items', '-n'),
331 marker=ValueArgument('show output greater that marker', '--marker'),
332 prefix=ValueArgument('show output starting with prefix', '--prefix'),
333 delimiter=ValueArgument('show output up to delimiter', '--delimiter'),
335 'show output starting with prefix up to /',
338 'show output with specified meta keys',
341 if_modified_since=ValueArgument(
342 'show output modified since then',
343 '--if-modified-since'),
344 if_unmodified_since=ValueArgument(
345 'show output not modified since then',
346 '--if-unmodified-since'),
347 until=DateArgument('show metadata until then', '--until'),
348 format=ValueArgument(
349 'format to parse until data (default: d/m/Y H:M:S )',
351 shared=FlagArgument('show only shared', '--shared'),
352 public=FlagArgument('show only public', '--public'),
354 'output results in pages (-n to set items per page, default 10)',
356 exact_match=FlagArgument(
357 'Show only objects that match exactly with path',
361 def print_objects(self, object_list):
362 limit = int(self['limit']) if self['limit'] > 0 else len(object_list)
363 for index, obj in enumerate(object_list):
364 if (self['exact_match'] and self.path and\
365 obj['name'] != self.path) or 'content_type' not in obj:
367 pretty_obj = obj.copy()
369 empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
370 if obj['content_type'] == 'application/directory':
375 size = format_size(obj['bytes'])
376 pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
377 oname = bold(obj['name'])
379 print('%s%s. %s' % (empty_space, index, oname))
380 print_dict(pretty_keys(pretty_obj), exclude=('name'))
383 oname = '%s%s. %6s %s' % (empty_space, index, size, oname)
384 oname += '/' if isDir else ''
387 page_hold(index, limit, len(object_list))
389 def print_containers(self, container_list):
390 limit = int(self['limit']) if self['limit'] > 0\
391 else len(container_list)
392 for index, container in enumerate(container_list):
393 if 'bytes' in container:
394 size = format_size(container['bytes'])
395 cname = '%s. %s' % (index + 1, bold(container['name']))
398 pretty_c = container.copy()
399 if 'bytes' in container:
400 pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
401 print_dict(pretty_keys(pretty_c), exclude=('name'))
404 if 'count' in container and 'bytes' in container:
405 print('%s (%s, %s objects)'\
406 % (cname, size, container['count']))
410 page_hold(index + 1, limit, len(container_list))
413 @errors.pithos.connection
414 @errors.pithos.object_path
415 @errors.pithos.container
417 if self.container is None:
418 r = self.client.account_get(
419 limit=False if self['more'] else self['limit'],
420 marker=self['marker'],
421 if_modified_since=self['if_modified_since'],
422 if_unmodified_since=self['if_unmodified_since'],
424 show_only_shared=self['shared'])
425 self.print_containers(r.json)
427 prefix = self.path or self['prefix']
428 r = self.client.container_get(
429 limit=False if self['more'] else self['limit'],
430 marker=self['marker'],
432 delimiter=self['delimiter'],
434 if_modified_since=self['if_modified_since'],
435 if_unmodified_since=self['if_unmodified_since'],
438 show_only_shared=self['shared'])
439 self.print_objects(r.json)
441 def main(self, container____path__=None):
442 super(self.__class__, self)._run(container____path__)
446 @command(pithos_cmds)
447 class store_mkdir(_store_container_command):
448 """Create a directory"""
450 __doc__ += '\n. '.join(about_directories)
453 @errors.pithos.connection
454 @errors.pithos.container
456 self.client.create_directory(self.path)
458 def main(self, container___directory):
459 super(self.__class__, self)._run(
460 container___directory,
461 path_is_optional=False)
465 @command(pithos_cmds)
466 class store_touch(_store_container_command):
467 """Create an empty object (file)
468 If object exists, this command will reset it to 0 length
472 content_type=ValueArgument(
473 'Set content type (default: application/octet-stream)',
475 default='application/octet-stream')
479 @errors.pithos.connection
480 @errors.pithos.container
482 self.client.create_object(self.path, self['content_type'])
484 def main(self, container___path):
485 super(store_touch, self)._run(
487 path_is_optional=False)
491 @command(pithos_cmds)
492 class store_create(_store_container_command):
493 """Create a container"""
496 versioning=ValueArgument(
497 'set container versioning (auto/none)',
499 quota=IntArgument('set default container quota', '--quota'),
500 meta=KeyValueArgument(
501 'set container metadata (can be repeated)',
506 @errors.pithos.connection
507 @errors.pithos.container
509 self.client.container_put(quota=self['quota'],
510 versioning=self['versioning'],
511 metadata=self['meta'])
513 def main(self, container=None):
514 super(self.__class__, self)._run(container)
515 if container and self.container != container:
516 raiseCLIError('Invalid container name %s' % container, details=[
517 'Did you mean "%s" ?' % self.container,
518 'Use --container for names containing :'])
522 @command(pithos_cmds)
523 class store_copy(_store_container_command):
524 """Copy objects from container to (another) container
527 . will copy all <obj> prefixed with path, as path2<obj>
528 . or as path2 if path corresponds to just one whole object
529 copy cont:path cont2:
530 . will copy all <obj> prefixed with path to container cont2
531 copy cont:path [cont2:]path2 --exact-match
532 . will copy at most one <obj> as a new object named path2,
533 . provided path corresponds to a whole object path
534 copy cont:path [cont2:]path2 --replace
535 . will copy all <obj> prefixed with path, replacing path with path2
536 where <obj> is a full file or directory object path.
538 1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
539 destination is container1:path2
540 2. <container>:<path1> <path2> : make a copy in the same container
541 3. Can use --container= instead of <container1>
545 destination_container=ValueArgument(
546 'use it if destination container name contains a : character',
548 source_version=ValueArgument(
549 'copy specific version',
551 public=ValueArgument('make object publicly accessible', '--public'),
552 content_type=ValueArgument(
553 'change object\'s content type',
555 recursive=FlagArgument(
556 'mass copy with delimiter /',
557 ('-r', '--recursive')),
558 exact_match=FlagArgument(
559 'Copy only the object that fully matches path',
561 replace=FlagArgument('Replace src. path with dst. path', '--replace')
564 def _objlist(self, dst_path):
565 if self['exact_match']:
566 return [(dst_path or self.path, self.path)]
567 r = self.client.container_get(prefix=self.path)
570 return [(obj['name'], dst_path or obj['name'])]
571 return [(obj['name'], '%s%s' % (
573 obj['name'][len(self.path) if self['replace'] else 0:])
577 @errors.pithos.connection
578 @errors.pithos.container
579 def _run(self, dst_cont, dst_path):
580 no_source_object = True
581 for src_object, dst_object in self._objlist(dst_path):
582 no_source_object = False
583 self.client.copy_object(
584 src_container=self.container,
585 src_object=src_object,
586 dst_container=dst_cont or self.container,
587 dst_object=dst_object,
588 source_version=self['source_version'],
589 public=self['public'],
590 content_type=self['content_type'])
592 raiseCLIError('No object %s in container %s' % (
597 source_container___path,
598 destination_container___path=None):
599 super(self.__class__, self)._run(
600 source_container___path,
601 path_is_optional=False)
602 (dst_cont, dst_path) = self._dest_container_path(
603 destination_container___path)
604 self._run(dst_cont=dst_cont, dst_path=dst_path or '')
607 @command(pithos_cmds)
608 class store_move(_store_container_command):
609 """Move/rename objects
612 . will move all <obj> prefixed with path, as path2<obj>
613 . or as path2 if path corresponds to just one whole object
614 move cont:path cont2:
615 . will move all <obj> prefixed with path to container cont2
616 move cont:path [cont2:]path2 --exact-match
617 . will move at most one <obj> as a new object named path2,
618 . provided path corresponds to a whole object path
619 move cont:path [cont2:]path2 --replace
620 . will move all <obj> prefixed with path, replacing path with path2
621 where <obj> is a full file or directory object path.
623 1. <container1>:<path1> [container2:]<path2> : if container2 not given,
624 destination is container1:path2
625 2. <container>:<path1> path2 : rename
626 3. Can use --container= instead of <container1>
630 destination_container=ValueArgument(
631 'use it if destination container name contains a : character',
633 source_version=ValueArgument('specify version', '--source-version'),
634 public=FlagArgument('make object publicly accessible', '--public'),
635 content_type=ValueArgument('modify content type', '--content-type'),
636 recursive=FlagArgument('up to delimiter /', ('-r', '--recursive')),
637 exact_match=FlagArgument(
638 'Copy only the object that fully matches path',
640 replace=FlagArgument('Replace src. path with dst. path', '--replace')
643 def _objlist(self, dst_path):
644 if self['exact_match']:
645 return [(dst_path or self.path, self.path)]
646 r = self.client.container_get(prefix=self.path)
649 return [(obj['name'], dst_path or obj['name'])]
650 return [(obj['name'], '%s%s' % (
652 obj['name'][len(self.path) if self['replace'] else 0:])
656 @errors.pithos.connection
657 @errors.pithos.container
658 def _run(self, dst_cont, dst_path):
659 no_source_object = True
660 for src_object, dst_object in self._objlist(dst_path):
661 no_source_object = False
662 self.client.move_object(
663 src_container=self.container,
664 src_object=src_object,
665 dst_container=dst_cont or self.container,
666 dst_object=dst_object,
667 source_version=self['source_version'],
668 public=self['public'],
669 content_type=self['content_type'])
671 raiseCLIError('No object %s in container %s' % (
676 source_container___path,
677 destination_container___path=None):
678 super(self.__class__, self)._run(
679 source_container___path,
680 path_is_optional=False)
681 (dst_cont, dst_path) = self._dest_container_path(
682 destination_container___path)
683 self._run(dst_cont=dst_cont, dst_path=dst_path or '')
686 @command(pithos_cmds)
687 class store_append(_store_container_command):
688 """Append local file to (existing) remote object
689 The remote object should exist.
690 If the remote object is a directory, it is transformed into a file.
691 In the later case, objects under the directory remain intact.
695 progress_bar=ProgressBarArgument(
696 'do not show progress bar',
702 @errors.pithos.connection
703 @errors.pithos.container
704 @errors.pithos.object_path
705 def _run(self, local_path):
706 (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
708 f = open(local_path, 'rb')
709 self.client.append_object(self.path, f, upload_cb)
711 self._safe_progress_bar_finish(progress_bar)
714 self._safe_progress_bar_finish(progress_bar)
716 def main(self, local_path, container___path):
717 super(self.__class__, self)._run(
719 path_is_optional=False)
720 self._run(local_path)
723 @command(pithos_cmds)
724 class store_truncate(_store_container_command):
725 """Truncate remote file up to a size (default is 0)"""
728 @errors.pithos.connection
729 @errors.pithos.container
730 @errors.pithos.object_path
731 @errors.pithos.object_size
732 def _run(self, size=0):
733 self.client.truncate_object(self.path, size)
735 def main(self, container___path, size=0):
736 super(self.__class__, self)._run(container___path)
740 @command(pithos_cmds)
741 class store_overwrite(_store_container_command):
742 """Overwrite part (from start to end) of a remote file
743 overwrite local-path container 10 20
744 . will overwrite bytes from 10 to 20 of a remote file with the same name
745 . as local-path basename
746 overwrite local-path container:path 10 20
747 . will overwrite as above, but the remote file is named path
751 progress_bar=ProgressBarArgument(
752 'do not show progress bar',
757 def _open_file(self, local_path, start):
758 f = open(path.abspath(local_path), 'rb')
765 @errors.pithos.connection
766 @errors.pithos.container
767 @errors.pithos.object_path
768 @errors.pithos.object_size
769 def _run(self, local_path, start, end):
770 (start, end) = (int(start), int(end))
771 (f, f_size) = self._open_file(local_path, start)
772 (progress_bar, upload_cb) = self._safe_progress_bar(
773 'Overwrite %s bytes' % (end - start))
775 self.client.overwrite_object(
782 self._safe_progress_bar_finish(progress_bar)
785 self._safe_progress_bar_finish(progress_bar)
787 def main(self, local_path, container___path, start, end):
788 super(self.__class__, self)._run(
790 path_is_optional=None)
791 self.path = self.path or path.basename(local_path)
792 self._run(local_path=local_path, start=start, end=end)
795 @command(pithos_cmds)
796 class store_manifest(_store_container_command):
797 """Create a remote file of uploaded parts by manifestation
798 Remains functional for compatibility with OOS Storage. Users are advised
799 to use the upload command instead.
800 Manifestation is a compliant process for uploading large files. The files
801 have to be chunked in smalled files and uploaded as <prefix><increment>
802 where increment is 1, 2, ...
803 Finally, the manifest command glues partial files together in one file
805 The upload command is faster, easier and more intuitive than manifest
809 etag=ValueArgument('check written data', '--etag'),
810 content_encoding=ValueArgument(
811 'set MIME content type',
812 '--content-encoding'),
813 content_disposition=ValueArgument(
814 'the presentation style of the object',
815 '--content-disposition'),
816 content_type=ValueArgument(
817 'specify content type',
819 default='application/octet-stream'),
820 sharing=SharingArgument(
821 'define object sharing policy \n' +\
822 ' ( "read=user1,grp1,user2,... write=user1,grp2,..." )',
824 public=FlagArgument('make object publicly accessible', '--public')
828 @errors.pithos.connection
829 @errors.pithos.container
830 @errors.pithos.object_path
832 self.client.create_object_by_manifestation(
834 content_encoding=self['content_encoding'],
835 content_disposition=self['content_disposition'],
836 content_type=self['content_type'],
837 sharing=self['sharing'],
838 public=self['public'])
840 def main(self, container___path):
841 super(self.__class__, self)._run(
843 path_is_optional=False)
847 @command(pithos_cmds)
848 class store_upload(_store_container_command):
852 use_hashes=FlagArgument(
853 'provide hashmap file instead of data',
855 etag=ValueArgument('check written data', '--etag'),
856 unchunked=FlagArgument('avoid chunked transfer mode', '--unchunked'),
857 content_encoding=ValueArgument(
858 'set MIME content type',
859 '--content-encoding'),
860 content_disposition=ValueArgument(
861 'specify objects presentation style',
862 '--content-disposition'),
863 content_type=ValueArgument('specify content type', '--content-type'),
864 sharing=SharingArgument(
865 help='define sharing object policy \n' +\
866 '( "read=user1,grp1,user2,... write=user1,grp2,... )',
867 parsed_name='--sharing'),
868 public=FlagArgument('make object publicly accessible', '--public'),
869 poolsize=IntArgument('set pool size', '--with-pool-size'),
870 progress_bar=ProgressBarArgument(
871 'do not show progress bar',
874 overwrite=FlagArgument('Force overwrite, if object exists', '-f')
877 def _remote_path(self, remote_path, local_path=''):
878 if self['overwrite']:
881 r = self.client.get_object_info(remote_path)
882 except ClientError as ce:
886 ctype = r.get('content-type', '')
887 if 'application/directory' == ctype.lower():
888 ret = '%s/%s' % (remote_path, local_path)
889 return self._remote_path(ret) if local_path else ret
891 'Object %s already exists' % remote_path,
893 details=['use -f to overwrite or resume'])
896 @errors.pithos.connection
897 @errors.pithos.container
898 @errors.pithos.object_path
899 @errors.pithos.local_path
900 def _run(self, local_path, remote_path):
901 poolsize = self['poolsize']
903 self.client.POOL_SIZE = int(poolsize)
905 content_encoding=self['content_encoding'],
906 content_type=self['content_type'],
907 content_disposition=self['content_disposition'],
908 sharing=self['sharing'],
909 public=self['public'])
910 remote_path = self._remote_path(remote_path, local_path)
911 with open(path.abspath(local_path), 'rb') as f:
912 if self['unchunked']:
913 self.client.upload_object_unchunked(
917 withHashFile=self['use_hashes'],
921 (progress_bar, upload_cb) = self._safe_progress_bar(
924 hash_bar = progress_bar.clone()
925 hash_cb = hash_bar.get_generator(
926 'Calculating block hashes')
929 self.client.upload_object(
936 self._safe_progress_bar_finish(progress_bar)
939 self._safe_progress_bar_finish(progress_bar)
940 print 'Upload completed'
942 def main(self, local_path, container____path__=None):
943 super(self.__class__, self)._run(container____path__)
944 remote_path = self.path or path.basename(local_path)
945 self._run(local_path=local_path, remote_path=remote_path)
948 @command(pithos_cmds)
949 class store_cat(_store_container_command):
950 """Print remote file contents to console"""
953 range=RangeArgument('show range of data', '--range'),
954 if_match=ValueArgument('show output if ETags match', '--if-match'),
955 if_none_match=ValueArgument(
956 'show output if ETags match',
958 if_modified_since=DateArgument(
959 'show output modified since then',
960 '--if-modified-since'),
961 if_unmodified_since=DateArgument(
962 'show output unmodified since then',
963 '--if-unmodified-since'),
964 object_version=ValueArgument(
965 'get the specific version',
970 @errors.pithos.connection
971 @errors.pithos.container
972 @errors.pithos.object_path
974 self.client.download_object(
978 version=self['object_version'],
979 if_match=self['if_match'],
980 if_none_match=self['if_none_match'],
981 if_modified_since=self['if_modified_since'],
982 if_unmodified_since=self['if_unmodified_since'])
984 def main(self, container___path):
985 super(self.__class__, self)._run(
987 path_is_optional=False)
991 @command(pithos_cmds)
992 class store_download(_store_container_command):
993 """Download remote object as local file
994 If local destination is a directory:
995 * download <container>:<path> <local dir> -r
996 will download all files on <container> prefixed as <path>,
997 to <local dir>/<full path>
998 * download <container>:<path> <local dir> --exact-match
999 will download only one file, exactly matching <path>
1000 ATTENTION: to download cont:dir1/dir2/file there must exist objects
1001 cont:dir1 and cont:dir1/dir2 of type application/directory
1002 To create directory objects, use /store mkdir
1006 resume=FlagArgument('Resume instead of overwrite', '--resume'),
1007 range=RangeArgument('show range of data', '--range'),
1008 if_match=ValueArgument('show output if ETags match', '--if-match'),
1009 if_none_match=ValueArgument(
1010 'show output if ETags match',
1012 if_modified_since=DateArgument(
1013 'show output modified since then',
1014 '--if-modified-since'),
1015 if_unmodified_since=DateArgument(
1016 'show output unmodified since then',
1017 '--if-unmodified-since'),
1018 object_version=ValueArgument(
1019 'get the specific version',
1020 '--object-version'),
1021 poolsize=IntArgument('set pool size', '--with-pool-size'),
1022 progress_bar=ProgressBarArgument(
1023 'do not show progress bar',
1024 '--no-progress-bar',
1026 recursive=FlagArgument(
1027 'Download a remote directory and all its contents',
1031 def _is_dir(self, remote_dict):
1032 return 'application/directory' == remote_dict.get('content_type', '')
1034 def _outputs(self, local_path):
1035 if local_path is None:
1036 return [(None, self.path)]
1037 outpath = path.abspath(local_path)
1038 if not (path.exists(outpath) or path.isdir(outpath)):
1039 return [(outpath, self.path)]
1040 elif self['recursive']:
1041 remotes = self.client.container_get(
1043 if_modified_since=self['if_modified_since'],
1044 if_unmodified_since=self['if_unmodified_since'])
1046 '%s/%s' % (outpath, remote['name']),
1047 None if self._is_dir(remote) else remote['name'])\
1048 for remote in remotes.json]
1049 raiseCLIError('Illegal destination location %s' % local_path)
1052 @errors.pithos.connection
1053 @errors.pithos.container
1054 @errors.pithos.object_path
1055 @errors.pithos.local_path
1056 def _run(self, local_path):
1057 outputs = self._outputs(local_path)
1058 poolsize = self['poolsize']
1060 self.client.POOL_SIZE = int(poolsize)
1062 raiseCLIError('No objects prefixed as %s on container %s' % (
1067 for lpath, rpath in sorted(outputs):
1069 if not path.isdir(lpath):
1070 print('Create directory %s' % lpath)
1073 wmode = 'rwb+' if path.exists(lpath) and self['resume']\
1075 print('\nFrom %s:%s to %s' % (
1080 download_cb) = self._safe_progress_bar('Downloading')
1081 self.client.download_object(
1083 open(lpath, wmode) if lpath else stdout,
1084 download_cb=download_cb,
1085 range=self['range'],
1086 version=self['object_version'],
1087 if_match=self['if_match'],
1088 resume=self['resume'],
1089 if_none_match=self['if_none_match'],
1090 if_modified_since=self['if_modified_since'],
1091 if_unmodified_since=self['if_unmodified_since'])
1092 except KeyboardInterrupt:
1093 from threading import enumerate as activethreads
1094 stdout.write('\nFinishing active threads ')
1095 for thread in activethreads():
1100 except RuntimeError:
1102 print('\ndownload canceled by user')
1103 if local_path is not None:
1104 print('to resume, re-run with --resume')
1106 self._safe_progress_bar_finish(progress_bar)
1109 self._safe_progress_bar_finish(progress_bar)
1111 def main(self, container___path, local_path=None):
1112 super(self.__class__, self)._run(
1114 path_is_optional=False)
1115 self._run(local_path=local_path)
1118 @command(pithos_cmds)
1119 class store_hashmap(_store_container_command):
1120 """Get the hash-map of an object"""
1123 if_match=ValueArgument('show output if ETags match', '--if-match'),
1124 if_none_match=ValueArgument(
1125 'show output if ETags match',
1127 if_modified_since=DateArgument(
1128 'show output modified since then',
1129 '--if-modified-since'),
1130 if_unmodified_since=DateArgument(
1131 'show output unmodified since then',
1132 '--if-unmodified-since'),
1133 object_version=ValueArgument(
1134 'get the specific version',
1139 @errors.pithos.connection
1140 @errors.pithos.container
1141 @errors.pithos.object_path
1143 data = self.client.get_object_hashmap(
1145 version=self['object_version'],
1146 if_match=self['if_match'],
1147 if_none_match=self['if_none_match'],
1148 if_modified_since=self['if_modified_since'],
1149 if_unmodified_since=self['if_unmodified_since'])
1152 def main(self, container___path):
1153 super(self.__class__, self)._run(
1155 path_is_optional=False)
1159 @command(pithos_cmds)
1160 class store_delete(_store_container_command):
1161 """Delete a container [or an object]
1162 How to delete a non-empty container:
1163 - empty the container: /store delete -r <container>
1164 - delete it: /store delete <container>
1166 Semantics of directory deletion:
1167 .a preserve the contents: /store delete <container>:<directory>
1168 . objects of the form dir/filename can exist with a dir object
1169 .b delete contents: /store delete -r <container>:<directory>
1170 . all dir/* objects are affected, even if dir does not exist
1172 To restore a deleted object OBJ in a container CONT:
1173 - get object versions: /store versions CONT:OBJ
1174 . and choose the version to be restored
1175 - restore the object: /store copy --source-version=<version> CONT:OBJ OBJ
1179 until=DateArgument('remove history until that date', '--until'),
1180 yes=FlagArgument('Do not prompt for permission', '--yes'),
1181 recursive=FlagArgument(
1182 'empty dir or container and delete (if dir)',
1183 ('-r', '--recursive'))
1186 def __init__(self, arguments={}):
1187 super(self.__class__, self).__init__(arguments)
1188 self['delimiter'] = DelimiterArgument(
1190 parsed_name='--delimiter',
1191 help='delete objects prefixed with <object><delimiter>')
1194 @errors.pithos.connection
1195 @errors.pithos.container
1196 @errors.pithos.object_path
1199 if self['yes'] or ask_user(
1200 'Delete %s:%s ?' % (self.container, self.path)):
1201 self.client.del_object(
1203 until=self['until'],
1204 delimiter=self['delimiter'])
1208 ask_msg = 'Delete contents of container'\
1209 if self['recursive'] else 'Delete container'
1210 if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1211 self.client.del_container(
1212 until=self['until'],
1213 delimiter=self['delimiter'])
1217 def main(self, container____path__=None):
1218 super(self.__class__, self)._run(container____path__)
1222 @command(pithos_cmds)
1223 class store_purge(_store_container_command):
1224 """Delete a container and release related data blocks
1225 Non-empty containers can not purged.
1226 To purge a container with content:
1227 . /store delete -r <container>
1228 . objects are deleted, but data blocks remain on server
1229 . /store purge <container>
1230 . container and data blocks are released and deleted
1234 yes=FlagArgument('Do not prompt for permission', '--yes'),
1238 @errors.pithos.connection
1239 @errors.pithos.container
1241 if self['yes'] or ask_user('Purge container %s?' % self.container):
1242 self.client.purge_container()
1246 def main(self, container=None):
1247 super(self.__class__, self)._run(container)
1248 if container and self.container != container:
1249 raiseCLIError('Invalid container name %s' % container, details=[
1250 'Did you mean "%s" ?' % self.container,
1251 'Use --container for names containing :'])
1255 @command(pithos_cmds)
1256 class store_publish(_store_container_command):
1257 """Publish the object and print the public url"""
1260 @errors.pithos.connection
1261 @errors.pithos.container
1262 @errors.pithos.object_path
1264 url = self.client.publish_object(self.path)
1267 def main(self, container___path):
1268 super(self.__class__, self)._run(
1270 path_is_optional=False)
1274 @command(pithos_cmds)
1275 class store_unpublish(_store_container_command):
1276 """Unpublish an object"""
1279 @errors.pithos.connection
1280 @errors.pithos.container
1281 @errors.pithos.object_path
1283 self.client.unpublish_object(self.path)
1285 def main(self, container___path):
1286 super(self.__class__, self)._run(
1288 path_is_optional=False)
1292 @command(pithos_cmds)
1293 class store_permissions(_store_container_command):
1294 """Get read and write permissions of an object
1295 Permissions are lists of users and user groups. There is read and write
1296 permissions. Users and groups with write permission have also read
1301 @errors.pithos.connection
1302 @errors.pithos.container
1303 @errors.pithos.object_path
1305 r = self.client.get_object_sharing(self.path)
1308 def main(self, container___path):
1309 super(self.__class__, self)._run(
1311 path_is_optional=False)
1315 @command(pithos_cmds)
1316 class store_setpermissions(_store_container_command):
1317 """Set permissions for an object
1318 New permissions overwrite existing permissions.
1320 - read=<username>[,usergroup[,...]]
1321 - write=<username>[,usegroup[,...]]
1322 E.g. to give read permissions for file F to users A and B and write for C:
1323 . /store setpermissions F read=A,B write=C
1327 def format_permition_dict(self, permissions):
1330 for perms in permissions:
1331 splstr = perms.split('=')
1332 if 'read' == splstr[0]:
1333 read = [user_or_group.strip() \
1334 for user_or_group in splstr[1].split(',')]
1335 elif 'write' == splstr[0]:
1336 write = [user_or_group.strip() \
1337 for user_or_group in splstr[1].split(',')]
1341 if not (read or write):
1343 'Usage:\tread=<groups,users> write=<groups,users>')
1344 return (read, write)
1347 @errors.pithos.connection
1348 @errors.pithos.container
1349 @errors.pithos.object_path
1350 def _run(self, read, write):
1351 self.client.set_object_sharing(
1353 read_permition=read,
1354 write_permition=write)
1356 def main(self, container___path, *permissions):
1357 super(self.__class__, self)._run(
1359 path_is_optional=False)
1360 (read, write) = self.format_permition_dict(permissions)
1361 self._run(read, write)
1364 @command(pithos_cmds)
1365 class store_delpermissions(_store_container_command):
1366 """Delete all permissions set on object
1367 To modify permissions, use /store setpermssions
1371 @errors.pithos.connection
1372 @errors.pithos.container
1373 @errors.pithos.object_path
1375 self.client.del_object_sharing(self.path)
1377 def main(self, container___path):
1378 super(self.__class__, self)._run(
1380 path_is_optional=False)
1384 @command(pithos_cmds)
1385 class store_info(_store_container_command):
1386 """Get detailed information for user account, containers or objects
1387 to get account info: /store info
1388 to get container info: /store info <container>
1389 to get object info: /store info <container>:<path>
1393 object_version=ValueArgument(
1394 'show specific version \ (applies only for objects)',
1399 @errors.pithos.connection
1400 @errors.pithos.container
1401 @errors.pithos.object_path
1403 if self.container is None:
1404 r = self.client.get_account_info()
1405 elif self.path is None:
1406 r = self.client.get_container_info(self.container)
1408 r = self.client.get_object_info(
1410 version=self['object_version'])
1413 def main(self, container____path__=None):
1414 super(self.__class__, self)._run(container____path__)
1418 @command(pithos_cmds)
1419 class store_meta(_store_container_command):
1420 """Get metadata for account, containers or objects"""
1423 detail=FlagArgument('show detailed output', '-l'),
1424 until=DateArgument('show metadata until then', '--until'),
1425 object_version=ValueArgument(
1426 'show specific version \ (applies only for objects)',
1431 @errors.pithos.connection
1432 @errors.pithos.container
1433 @errors.pithos.object_path
1435 until = self['until']
1436 if self.container is None:
1438 r = self.client.get_account_info(until=until)
1440 r = self.client.get_account_meta(until=until)
1441 r = pretty_keys(r, '-')
1443 print(bold(self.client.account))
1444 elif self.path is None:
1446 r = self.client.get_container_info(until=until)
1448 cmeta = self.client.get_container_meta(until=until)
1449 ometa = self.client.get_container_object_meta(until=until)
1452 r['container-meta'] = pretty_keys(cmeta, '-')
1454 r['object-meta'] = pretty_keys(ometa, '-')
1457 r = self.client.get_object_info(self.path,
1458 version=self['object_version'])
1460 r = self.client.get_object_meta(self.path,
1461 version=self['object_version'])
1463 r = pretty_keys(pretty_keys(r, '-'))
1467 def main(self, container____path__=None):
1468 super(self.__class__, self)._run(container____path__)
1472 @command(pithos_cmds)
1473 class store_setmeta(_store_container_command):
1474 """Set a piece of metadata for account, container or object
1475 Metadata are formed as key:value pairs
1479 @errors.pithos.connection
1480 @errors.pithos.container
1481 @errors.pithos.object_path
1482 def _run(self, metakey, metaval):
1483 if not self.container:
1484 self.client.set_account_meta({metakey: metaval})
1486 self.client.set_container_meta({metakey: metaval})
1488 self.client.set_object_meta(self.path, {metakey: metaval})
1490 def main(self, metakey, metaval, container____path__=None):
1491 super(self.__class__, self)._run(container____path__)
1492 self._run(metakey=metakey, metaval=metaval)
1495 @command(pithos_cmds)
1496 class store_delmeta(_store_container_command):
1497 """Delete metadata with given key from account, container or object
1498 Metadata are formed as key:value objects
1499 - to get metadata of current account: /store meta
1500 - to get metadata of a container: /store meta <container>
1501 - to get metadata of an object: /store meta <container>:<path>
1505 @errors.pithos.connection
1506 @errors.pithos.container
1507 @errors.pithos.object_path
1508 def _run(self, metakey):
1509 if self.container is None:
1510 self.client.del_account_meta(metakey)
1511 elif self.path is None:
1512 self.client.del_container_meta(metakey)
1514 self.client.del_object_meta(self.path, metakey)
1516 def main(self, metakey, container____path__=None):
1517 super(self.__class__, self)._run(container____path__)
1521 @command(pithos_cmds)
1522 class store_quota(_store_account_command):
1523 """Get quota for account or container"""
1526 in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1530 @errors.pithos.connection
1531 @errors.pithos.container
1534 reply = self.client.get_container_quota(self.container)
1536 reply = self.client.get_account_quota()
1537 if not self['in_bytes']:
1539 reply[k] = format_size(reply[k])
1540 print_dict(pretty_keys(reply, '-'))
1542 def main(self, container=None):
1543 super(self.__class__, self)._run()
1544 self.container = container
1548 @command(pithos_cmds)
1549 class store_setquota(_store_account_command):
1550 """Set new quota for account or container
1551 By default, quota is set in bytes
1552 Users may specify a different unit, e.g:
1553 /store setquota 2.3GB mycontainer
1554 Accepted units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1558 def _calculate_quota(self, user_input):
1561 quota = int(user_input)
1564 digits = [str(num) for num in range(0, 10)] + ['.']
1565 while user_input[index] in digits:
1567 quota = user_input[:index]
1568 format = user_input[index:]
1570 return to_bytes(quota, format)
1571 except Exception as qe:
1573 'Failed to convert %s to bytes' % user_input,
1575 'Syntax: setquota <quota>[format] [container]',
1576 'e.g.: setquota 2.3GB mycontainer',
1577 'Acceptable formats:',
1578 '(*1024): B, KiB, MiB, GiB, TiB',
1579 '(*1000): B, KB, MB, GB, TB'])
1583 @errors.pithos.connection
1584 @errors.pithos.container
1585 def _run(self, quota):
1587 self.client.container = self.container
1588 self.client.set_container_quota(quota)
1590 self.client.set_account_quota(quota)
1592 def main(self, quota, container=None):
1593 super(self.__class__, self)._run()
1594 quota = self._calculate_quota(quota)
1595 self.container = container
1599 @command(pithos_cmds)
1600 class store_versioning(_store_account_command):
1601 """Get versioning for account or container"""
1604 @errors.pithos.connection
1605 @errors.pithos.container
1608 r = self.client.get_container_versioning(self.container)
1610 r = self.client.get_account_versioning()
1613 def main(self, container=None):
1614 super(self.__class__, self)._run()
1615 self.container = container
1619 @command(pithos_cmds)
1620 class store_setversioning(_store_account_command):
1621 """Set versioning mode (auto, none) for account or container"""
1623 def _check_versioning(self, versioning):
1624 if versioning and versioning.lower() in ('auto', 'none'):
1625 return versioning.lower()
1626 raiseCLIError('Invalid versioning %s' % versioning, details=[
1627 'Versioning can be auto or none'])
1630 @errors.pithos.connection
1631 @errors.pithos.container
1632 def _run(self, versioning):
1634 self.client.container = self.container
1635 self.client.set_container_versioning(versioning)
1637 self.client.set_account_versioning(versioning)
1639 def main(self, versioning, container=None):
1640 super(self.__class__, self)._run()
1641 self._run(self._check_versioning(versioning))
1644 @command(pithos_cmds)
1645 class store_group(_store_account_command):
1646 """Get groups and group members"""
1649 @errors.pithos.connection
1651 r = self.client.get_account_group()
1652 print_dict(pretty_keys(r, '-'))
1655 super(self.__class__, self)._run()
1659 @command(pithos_cmds)
1660 class store_setgroup(_store_account_command):
1661 """Set a user group"""
1664 @errors.pithos.connection
1665 def _run(self, groupname, *users):
1666 self.client.set_account_group(groupname, users)
1668 def main(self, groupname, *users):
1669 super(self.__class__, self)._run()
1671 self._run(groupname, *users)
1673 raiseCLIError('No users to add in group %s' % groupname)
1676 @command(pithos_cmds)
1677 class store_delgroup(_store_account_command):
1678 """Delete a user group"""
1681 @errors.pithos.connection
1682 def _run(self, groupname):
1683 self.client.del_account_group(groupname)
1685 def main(self, groupname):
1686 super(self.__class__, self)._run()
1687 self._run(groupname)
1690 @command(pithos_cmds)
1691 class store_sharers(_store_account_command):
1692 """List the accounts that share objects with current user"""
1695 detail=FlagArgument('show detailed output', '-l'),
1696 marker=ValueArgument('show output greater then marker', '--marker')
1700 @errors.pithos.connection
1702 accounts = self.client.get_sharing_accounts(marker=self['marker'])
1703 print_items(accounts if self['detail']\
1704 else [acc['name'] for acc in accounts])
1707 super(self.__class__, self)._run()
1711 @command(pithos_cmds)
1712 class store_versions(_store_container_command):
1713 """Get the list of object versions
1714 Deleted objects may still have versions that can be used to restore it and
1715 get information about its previous state.
1716 The version number can be used in a number of other commands, like info,
1717 copy, move, meta. See these commands for more information, e.g.
1722 @errors.pithos.connection
1723 @errors.pithos.container
1724 @errors.pithos.object_path
1726 versions = self.client.get_object_versionlist(self.path)
1729 created=strftime('%d-%m-%Y %H:%M:%S', localtime(float(vitem[1])))
1730 ) for vitem in versions])
1732 def main(self, container___path):
1733 super(store_versions, self)._run(
1735 path_is_optional=False)