download --resume can create new local files
[kamaki] / kamaki / cli / commands / pithos_cli.py
1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
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.
15 #
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.
28 #
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
33
34 from sys import stdout
35 from time import localtime, strftime
36 from logging import getLogger
37 from os import path, makedirs
38
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 (
43     format_size,
44     to_bytes,
45     print_dict,
46     print_items,
47     pretty_keys,
48     page_hold,
49     bold,
50     ask_user)
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
57
58 kloger = getLogger('kamaki')
59
60 pithos_cmds = CommandTree('store', 'Pithos+ storage commands')
61 _commands = [pithos_cmds]
62
63
64 about_directories = [
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.']
70
71
72 # Argument functionality
73
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',
78             ' ',
79             '  to get the service url: /config get store.url',
80             '  to set the service url: /config set store.url <url>',
81             ' ',
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>',
85             ' ',
86             '  to get authentication token: /config get token',
87             '  to set authentication token: /config set token <token>'
88             ])
89     elif e.status == 413:
90         raiseCLIError(e, details=[
91             'Get quotas:',
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>'
96             ])
97
98
99 class DelimiterArgument(ValueArgument):
100     """
101     :value type: string
102     :value returns: given string or /
103     """
104
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
108
109     @property
110     def value(self):
111         if self.caller_obj['recursive']:
112             return '/'
113         return getattr(self, '_value', self.default)
114
115     @value.setter
116     def value(self, newvalue):
117         self._value = newvalue
118
119
120 class SharingArgument(ValueArgument):
121     """Set sharing (read and/or write) groups
122     .
123     :value type: "read=term1,term2,... write=term1,term2,..."
124     .
125     :value returns: {'read':['term1', 'term2', ...],
126     .   'write':['term1', 'term2', ...]}
127     """
128
129     @property
130     def value(self):
131         return getattr(self, '_value', self.default)
132
133     @value.setter
134     def value(self, newvalue):
135         perms = {}
136         try:
137             permlist = newvalue.split(' ')
138         except AttributeError:
139             return
140         for p in permlist:
141             try:
142                 (key, val) = p.split('=')
143             except ValueError as err:
144                 raiseCLIError(err, 'Error in --sharing',
145                     details='Incorrect format',
146                     importance=1)
147             if key.lower() not in ('read', 'write'):
148                 raiseCLIError(err, 'Error in --sharing',
149                     details='Invalid permission key %s' % key,
150                     importance=1)
151             val_list = val.split(',')
152             if not key in perms:
153                 perms[key] = []
154             for item in val_list:
155                 if item not in perms[key]:
156                     perms[key].append(item)
157         self._value = perms
158
159
160 class RangeArgument(ValueArgument):
161     """
162     :value type: string of the form <start>-<end> where <start> and <end> are
163         integers
164     :value returns: the input string, after type checking <start> and <end>
165     """
166
167     @property
168     def value(self):
169         return getattr(self, '_value', self.default)
170
171     @value.setter
172     def value(self, newvalue):
173         if newvalue is None:
174             self._value = self.default
175             return
176         (start, end) = newvalue.split('-')
177         (start, end) = (int(start), int(end))
178         self._value = '%s-%s' % (start, end)
179
180 # Command specs
181
182
183 class _pithos_init(_command_init):
184     """Initialize a pithos+ kamaki client"""
185
186     @errors.generic.all
187     def _run(self):
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,
197             token=self.token,
198             account=self.account,
199             container=self.container)
200
201     def main(self):
202         self._run()
203
204
205 class _store_account_command(_pithos_init):
206     """Base class for account level storage commands"""
207
208     def __init__(self, arguments={}):
209         super(_store_account_command, self).__init__(arguments)
210         self['account'] = ValueArgument(
211             'Set user account (not permanent)',
212             '--account')
213
214     def _run(self):
215         super(_store_account_command, self)._run()
216         if self['account']:
217             self.client.account = self['account']
218
219     @errors.generic.all
220     def main(self):
221         self._run()
222
223
224 class _store_container_command(_store_account_command):
225     """Base class for container level storage commands"""
226
227     container = None
228     path = None
229
230     def __init__(self, arguments={}):
231         super(_store_container_command, self).__init__(arguments)
232         self['container'] = ValueArgument(
233             'Set container to work with (temporary)',
234             '--container')
235
236     @errors.generic.all
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])
242
243     def extract_container_and_path(self,
244         container_with_path,
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
252         """
253         try:
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']
259                 return
260             raiseCLIError(err)
261
262         user_cont, sep, userpath = container_with_path.partition(':')
263
264         if sep:
265             if not user_cont:
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)
273                 )
274             self.container = user_cont
275             if not userpath:
276                 raiseCLIError(CLISyntaxError(
277                     'Path is missing for object in container %s' % user_cont,
278                     details=errors.pithos.container_howto)
279                 )
280             self.path = userpath
281         else:
282             alt_cont = self['container'] or self.client.container
283             if alt_cont:
284                 self.container = alt_cont
285                 self.path = user_cont
286             elif path_is_optional:
287                 self.container = user_cont
288                 self.path = None
289             else:
290                 self.container = user_cont
291                 raiseCLIError(CLISyntaxError(
292                     'Both container and path are required',
293                     details=errors.pithos.container_howto)
294                 )
295
296     @errors.generic.all
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(
309                 container_with_path,
310                 path_is_optional)
311             self.client.container = self.container
312         self.container = self.client.container
313
314     def main(self, container_with_path=None, path_is_optional=True):
315         self._run(container_with_path, path_is_optional)
316
317
318 @command(pithos_cmds)
319 class store_list(_store_container_command):
320     """List containers, object trees or objects in a directory
321     Use with:
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
326     """
327
328     arguments = dict(
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'),
334         path=ValueArgument(
335             'show output starting with prefix up to /',
336             '--path'),
337         meta=ValueArgument(
338             'show output with specified meta keys',
339             '--meta',
340             default=[]),
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 )',
350             '--format'),
351         shared=FlagArgument('show only shared', '--shared'),
352         public=FlagArgument('show only public', '--public'),
353         more=FlagArgument(
354             'output results in pages (-n to set items per page, default 10)',
355             '--more'),
356         exact_match=FlagArgument(
357             'Show only objects that match exactly with path',
358             '--exact-match')
359     )
360
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:
366                 continue
367             pretty_obj = obj.copy()
368             index += 1
369             empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
370             if obj['content_type'] == 'application/directory':
371                 isDir = True
372                 size = 'D'
373             else:
374                 isDir = False
375                 size = format_size(obj['bytes'])
376                 pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
377             oname = bold(obj['name'])
378             if self['detail']:
379                 print('%s%s. %s' % (empty_space, index, oname))
380                 print_dict(pretty_keys(pretty_obj), exclude=('name'))
381                 print
382             else:
383                 oname = '%s%s. %6s %s' % (empty_space, index, size, oname)
384                 oname += '/' if isDir else ''
385                 print(oname)
386             if self['more']:
387                 page_hold(index, limit, len(object_list))
388
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']))
396             if self['detail']:
397                 print(cname)
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'))
402                 print
403             else:
404                 if 'count' in container and 'bytes' in container:
405                     print('%s (%s, %s objects)'\
406                     % (cname, size, container['count']))
407                 else:
408                     print(cname)
409             if self['more']:
410                 page_hold(index + 1, limit, len(container_list))
411
412     @errors.generic.all
413     @errors.pithos.connection
414     @errors.pithos.object_path
415     @errors.pithos.container
416     def _run(self):
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'],
423                 until=self['until'],
424                 show_only_shared=self['shared'])
425             self.print_containers(r.json)
426         else:
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'],
431                 prefix=prefix,
432                 delimiter=self['delimiter'],
433                 path=self['path'],
434                 if_modified_since=self['if_modified_since'],
435                 if_unmodified_since=self['if_unmodified_since'],
436                 until=self['until'],
437                 meta=self['meta'],
438                 show_only_shared=self['shared'])
439             self.print_objects(r.json)
440
441     def main(self, container____path__=None):
442         super(self.__class__, self)._run(container____path__)
443         self._run()
444
445
446 @command(pithos_cmds)
447 class store_mkdir(_store_container_command):
448     """Create a directory"""
449
450     __doc__ += '\n. '.join(about_directories)
451
452     @errors.generic.all
453     @errors.pithos.connection
454     @errors.pithos.container
455     def _run(self):
456         self.client.create_directory(self.path)
457
458     def main(self, container___directory):
459         super(self.__class__, self)._run(
460             container___directory,
461             path_is_optional=False)
462         self._run()
463
464
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
469     """
470
471     arguments = dict(
472         content_type=ValueArgument(
473             'Set content type (default: application/octet-stream)',
474             '--content-type',
475             default='application/octet-stream')
476     )
477
478     @errors.generic.all
479     @errors.pithos.connection
480     @errors.pithos.container
481     def _run(self):
482         self.client.create_object(self.path, self['content_type'])
483
484     def main(self, container___path):
485         super(store_touch, self)._run(
486             container___path,
487             path_is_optional=False)
488         self._run()
489
490
491 @command(pithos_cmds)
492 class store_create(_store_container_command):
493     """Create a container"""
494
495     arguments = dict(
496         versioning=ValueArgument(
497             'set container versioning (auto/none)',
498             '--versioning'),
499         quota=IntArgument('set default container quota', '--quota'),
500         meta=KeyValueArgument(
501             'set container metadata (can be repeated)',
502             '--meta')
503     )
504
505     @errors.generic.all
506     @errors.pithos.connection
507     @errors.pithos.container
508     def _run(self):
509         self.client.container_put(quota=self['quota'],
510             versioning=self['versioning'],
511             metadata=self['meta'])
512
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 :'])
519         self._run()
520
521
522 @command(pithos_cmds)
523 class store_copy(_store_container_command):
524     """Copy objects from container to (another) container
525     Semantics:
526     copy cont:path path2
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.
537     Use options:
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>
542     """
543
544     arguments = dict(
545         destination_container=ValueArgument(
546             'use it if destination container name contains a : character',
547             '--dst-container'),
548         source_version=ValueArgument(
549             'copy specific version',
550             '--source-version'),
551         public=ValueArgument('make object publicly accessible', '--public'),
552         content_type=ValueArgument(
553             'change object\'s content type',
554             '--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',
560             '--exact-match'),
561         replace=FlagArgument('Replace src. path with dst. path', '--replace')
562     )
563
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)
568         if len(r.json) == 1:
569             obj = r.json[0]
570             return [(obj['name'], dst_path or obj['name'])]
571         return [(obj['name'], '%s%s' % (
572                     dst_path,
573                     obj['name'][len(self.path) if self['replace'] else 0:])
574                 ) for obj in r.json]
575
576     @errors.generic.all
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'])
591         if no_source_object:
592             raiseCLIError('No object %s in container %s' % (
593                 self.path,
594                 self.container))
595
596     def main(self,
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 '')
605
606
607 @command(pithos_cmds)
608 class store_move(_store_container_command):
609     """Move/rename objects
610     Semantics:
611     move cont:path path2
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.
622     Use options:
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>
627     """
628
629     arguments = dict(
630         destination_container=ValueArgument(
631             'use it if destination container name contains a : character',
632             '--dst-container'),
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',
639             '--exact-match'),
640         replace=FlagArgument('Replace src. path with dst. path', '--replace')
641     )
642
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)
647         if len(r.json) == 1:
648             obj = r.json[0]
649             return [(obj['name'], dst_path or obj['name'])]
650         return [(obj['name'], '%s%s' % (
651                     dst_path,
652                     obj['name'][len(self.path) if self['replace'] else 0:])
653                 ) for obj in r.json]
654
655     @errors.generic.all
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'])
670         if no_source_object:
671             raiseCLIError('No object %s in container %s' % (
672                 self.path,
673                 self.container))
674
675     def main(self,
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 '')
684
685
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.
692     """
693
694     arguments = dict(
695         progress_bar=ProgressBarArgument(
696             'do not show progress bar',
697             '--no-progress-bar',
698             default=False)
699     )
700
701     @errors.generic.all
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')
707         try:
708             f = open(local_path, 'rb')
709             self.client.append_object(self.path, f, upload_cb)
710         except Exception:
711             self._safe_progress_bar_finish(progress_bar)
712             raise
713         finally:
714             self._safe_progress_bar_finish(progress_bar)
715
716     def main(self, local_path, container___path):
717         super(self.__class__, self)._run(
718             container___path,
719             path_is_optional=False)
720         self._run(local_path)
721
722
723 @command(pithos_cmds)
724 class store_truncate(_store_container_command):
725     """Truncate remote file up to a size (default is 0)"""
726
727     @errors.generic.all
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)
734
735     def main(self, container___path, size=0):
736         super(self.__class__, self)._run(container___path)
737         self._run(size=size)
738
739
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
748     """
749
750     arguments = dict(
751         progress_bar=ProgressBarArgument(
752             'do not show progress bar',
753             '--no-progress-bar',
754             default=False)
755     )
756
757     def _open_file(self, local_path, start):
758         f = open(path.abspath(local_path), 'rb')
759         f.seek(0, 2)
760         f_size = f.tell()
761         f.seek(start, 0)
762         return (f, f_size)
763
764     @errors.generic.all
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))
774         try:
775             self.client.overwrite_object(
776                 obj=self.path,
777                 start=start,
778                 end=end,
779                 source_file=f,
780                 upload_cb=upload_cb)
781         except Exception:
782             self._safe_progress_bar_finish(progress_bar)
783             raise
784         finally:
785             self._safe_progress_bar_finish(progress_bar)
786
787     def main(self, local_path, container___path, start, end):
788         super(self.__class__, self)._run(
789             container___path,
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)
793
794
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
804     named <prefix>
805     The upload command is faster, easier and more intuitive than manifest
806     """
807
808     arguments = dict(
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',
818             '--content-type',
819             default='application/octet-stream'),
820         sharing=SharingArgument(
821             'define object sharing policy \n' +\
822             '    ( "read=user1,grp1,user2,... write=user1,grp2,..." )',
823             '--sharing'),
824         public=FlagArgument('make object publicly accessible', '--public')
825     )
826
827     @errors.generic.all
828     @errors.pithos.connection
829     @errors.pithos.container
830     @errors.pithos.object_path
831     def _run(self):
832         self.client.create_object_by_manifestation(
833             self.path,
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'])
839
840     def main(self, container___path):
841         super(self.__class__, self)._run(
842             container___path,
843             path_is_optional=False)
844         self.run()
845
846
847 @command(pithos_cmds)
848 class store_upload(_store_container_command):
849     """Upload a file"""
850
851     arguments = dict(
852         use_hashes=FlagArgument(
853             'provide hashmap file instead of data',
854             '--use-hashes'),
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',
872             '--no-progress-bar',
873             default=False),
874         overwrite=FlagArgument('Force overwrite, if object exists', '-f')
875     )
876
877     def _remote_path(self, remote_path, local_path=''):
878         if self['overwrite']:
879             return remote_path
880         try:
881             r = self.client.get_object_info(remote_path)
882         except ClientError as ce:
883             if ce.status == 404:
884                 return remote_path
885             raise 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
890         raiseCLIError(
891             'Object %s already exists' % remote_path,
892             importance=1,
893             details=['use -f to overwrite or resume'])
894
895     @errors.generic.all
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']
902         if poolsize > 0:
903             self.client.POOL_SIZE = int(poolsize)
904         params = dict(
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(
914                     remote_path,
915                     f,
916                     etag=self['etag'],
917                     withHashFile=self['use_hashes'],
918                     **params)
919             else:
920                 try:
921                     (progress_bar, upload_cb) = self._safe_progress_bar(
922                         'Uploading')
923                     if progress_bar:
924                         hash_bar = progress_bar.clone()
925                         hash_cb = hash_bar.get_generator(
926                                     'Calculating block hashes')
927                     else:
928                         hash_cb = None
929                     self.client.upload_object(
930                         remote_path,
931                         f,
932                         hash_cb=hash_cb,
933                         upload_cb=upload_cb,
934                         **params)
935                 except Exception:
936                     self._safe_progress_bar_finish(progress_bar)
937                     raise
938                 finally:
939                     self._safe_progress_bar_finish(progress_bar)
940         print 'Upload completed'
941
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)
946
947
948 @command(pithos_cmds)
949 class store_cat(_store_container_command):
950     """Print remote file contents to console"""
951
952     arguments = dict(
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',
957             '--if-none-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',
966             '--object-version')
967     )
968
969     @errors.generic.all
970     @errors.pithos.connection
971     @errors.pithos.container
972     @errors.pithos.object_path
973     def _run(self):
974         self.client.download_object(
975             self.path,
976             stdout,
977             range=self['range'],
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'])
983
984     def main(self, container___path):
985         super(self.__class__, self)._run(
986             container___path,
987             path_is_optional=False)
988         self._run()
989
990
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
1003     """
1004
1005     arguments = dict(
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',
1011             '--if-none-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',
1025             default=False),
1026         recursive=FlagArgument(
1027             'Download a remote directory and all its contents',
1028             '-r, --resursive')
1029     )
1030
1031     def _is_dir(self, remote_dict):
1032         return 'application/directory' == remote_dict.get('content_type', '')
1033
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(
1042                 prefix=self.path,
1043                 if_modified_since=self['if_modified_since'],
1044                 if_unmodified_since=self['if_unmodified_since'])
1045             return [(
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)
1050
1051     @errors.generic.all
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']
1059         if poolsize:
1060             self.client.POOL_SIZE = int(poolsize)
1061         if not outputs:
1062             raiseCLIError('No objects prefixed as %s on container %s' % (
1063                 self.path,
1064                 self.container))
1065         progress_bar = None
1066         try:
1067             for lpath, rpath in sorted(outputs):
1068                 if not rpath:
1069                     if not path.isdir(lpath):
1070                         print('Create directory %s' % lpath)
1071                         makedirs(lpath)
1072                     continue
1073                 wmode = 'rwb+' if path.exists(lpath) and self['resume']\
1074                     else 'wb+'
1075                 print('\nFrom %s:%s to %s' % (
1076                     self.container,
1077                     rpath,
1078                     lpath))
1079                 (progress_bar,
1080                     download_cb) = self._safe_progress_bar('Downloading')
1081                 self.client.download_object(
1082                     rpath,
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():
1096                 stdout.flush()
1097                 try:
1098                     thread.join()
1099                     stdout.write('.')
1100                 except RuntimeError:
1101                     continue
1102             print('\ndownload canceled by user')
1103             if local_path is not None:
1104                 print('to resume, re-run with --resume')
1105         except Exception:
1106             self._safe_progress_bar_finish(progress_bar)
1107             raise
1108         finally:
1109             self._safe_progress_bar_finish(progress_bar)
1110
1111     def main(self, container___path, local_path=None):
1112         super(self.__class__, self)._run(
1113             container___path,
1114             path_is_optional=False)
1115         self._run(local_path=local_path)
1116
1117
1118 @command(pithos_cmds)
1119 class store_hashmap(_store_container_command):
1120     """Get the hash-map of an object"""
1121
1122     arguments = dict(
1123         if_match=ValueArgument('show output if ETags match', '--if-match'),
1124         if_none_match=ValueArgument(
1125             'show output if ETags match',
1126             '--if-none-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',
1135             '--object-version')
1136     )
1137
1138     @errors.generic.all
1139     @errors.pithos.connection
1140     @errors.pithos.container
1141     @errors.pithos.object_path
1142     def _run(self):
1143         data = self.client.get_object_hashmap(
1144             self.path,
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'])
1150         print_dict(data)
1151
1152     def main(self, container___path):
1153         super(self.__class__, self)._run(
1154             container___path,
1155             path_is_optional=False)
1156         self._run()
1157
1158
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>
1165     .
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
1171     .
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
1176     """
1177
1178     arguments = dict(
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'))
1184     )
1185
1186     def __init__(self, arguments={}):
1187         super(self.__class__, self).__init__(arguments)
1188         self['delimiter'] = DelimiterArgument(
1189             self,
1190             parsed_name='--delimiter',
1191             help='delete objects prefixed with <object><delimiter>')
1192
1193     @errors.generic.all
1194     @errors.pithos.connection
1195     @errors.pithos.container
1196     @errors.pithos.object_path
1197     def _run(self):
1198         if self.path:
1199             if self['yes'] or ask_user(
1200                 'Delete %s:%s ?' % (self.container, self.path)):
1201                 self.client.del_object(
1202                     self.path,
1203                     until=self['until'],
1204                     delimiter=self['delimiter'])
1205             else:
1206                 print('Aborted')
1207         else:
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'])
1214             else:
1215                 print('Aborted')
1216
1217     def main(self, container____path__=None):
1218         super(self.__class__, self)._run(container____path__)
1219         self._run()
1220
1221
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
1231     """
1232
1233     arguments = dict(
1234         yes=FlagArgument('Do not prompt for permission', '--yes'),
1235     )
1236
1237     @errors.generic.all
1238     @errors.pithos.connection
1239     @errors.pithos.container
1240     def _run(self):
1241         if self['yes'] or ask_user('Purge container %s?' % self.container):
1242                 self.client.purge_container()
1243         else:
1244             print('Aborted')
1245
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 :'])
1252         self._run()
1253
1254
1255 @command(pithos_cmds)
1256 class store_publish(_store_container_command):
1257     """Publish the object and print the public url"""
1258
1259     @errors.generic.all
1260     @errors.pithos.connection
1261     @errors.pithos.container
1262     @errors.pithos.object_path
1263     def _run(self):
1264         url = self.client.publish_object(self.path)
1265         print(url)
1266
1267     def main(self, container___path):
1268         super(self.__class__, self)._run(
1269             container___path,
1270             path_is_optional=False)
1271         self._run()
1272
1273
1274 @command(pithos_cmds)
1275 class store_unpublish(_store_container_command):
1276     """Unpublish an object"""
1277
1278     @errors.generic.all
1279     @errors.pithos.connection
1280     @errors.pithos.container
1281     @errors.pithos.object_path
1282     def _run(self):
1283             self.client.unpublish_object(self.path)
1284
1285     def main(self, container___path):
1286         super(self.__class__, self)._run(
1287             container___path,
1288             path_is_optional=False)
1289         self._run()
1290
1291
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
1297     permission.
1298     """
1299
1300     @errors.generic.all
1301     @errors.pithos.connection
1302     @errors.pithos.container
1303     @errors.pithos.object_path
1304     def _run(self):
1305         r = self.client.get_object_sharing(self.path)
1306         print_dict(r)
1307
1308     def main(self, container___path):
1309         super(self.__class__, self)._run(
1310             container___path,
1311             path_is_optional=False)
1312         self._run()
1313
1314
1315 @command(pithos_cmds)
1316 class store_setpermissions(_store_container_command):
1317     """Set permissions for an object
1318     New permissions overwrite existing permissions.
1319     Permission format:
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
1324     """
1325
1326     @errors.generic.all
1327     def format_permition_dict(self, permissions):
1328         read = False
1329         write = False
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(',')]
1338             else:
1339                 read = False
1340                 write = False
1341         if not (read or write):
1342             raiseCLIError(None,
1343             'Usage:\tread=<groups,users> write=<groups,users>')
1344         return (read, write)
1345
1346     @errors.generic.all
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(
1352             self.path,
1353             read_permition=read,
1354             write_permition=write)
1355
1356     def main(self, container___path, *permissions):
1357         super(self.__class__, self)._run(
1358             container___path,
1359             path_is_optional=False)
1360         (read, write) = self.format_permition_dict(permissions)
1361         self._run(read, write)
1362
1363
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
1368     """
1369
1370     @errors.generic.all
1371     @errors.pithos.connection
1372     @errors.pithos.container
1373     @errors.pithos.object_path
1374     def _run(self):
1375         self.client.del_object_sharing(self.path)
1376
1377     def main(self, container___path):
1378         super(self.__class__, self)._run(
1379             container___path,
1380             path_is_optional=False)
1381         self._run()
1382
1383
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>
1390     """
1391
1392     arguments = dict(
1393         object_version=ValueArgument(
1394             'show specific version \ (applies only for objects)',
1395             '--object-version')
1396     )
1397
1398     @errors.generic.all
1399     @errors.pithos.connection
1400     @errors.pithos.container
1401     @errors.pithos.object_path
1402     def _run(self):
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)
1407         else:
1408             r = self.client.get_object_info(
1409                 self.path,
1410                 version=self['object_version'])
1411         print_dict(r)
1412
1413     def main(self, container____path__=None):
1414         super(self.__class__, self)._run(container____path__)
1415         self._run()
1416
1417
1418 @command(pithos_cmds)
1419 class store_meta(_store_container_command):
1420     """Get metadata for account, containers or objects"""
1421
1422     arguments = dict(
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)',
1427             '--object-version')
1428     )
1429
1430     @errors.generic.all
1431     @errors.pithos.connection
1432     @errors.pithos.container
1433     @errors.pithos.object_path
1434     def _run(self):
1435         until = self['until']
1436         if self.container is None:
1437             if self['detail']:
1438                 r = self.client.get_account_info(until=until)
1439             else:
1440                 r = self.client.get_account_meta(until=until)
1441                 r = pretty_keys(r, '-')
1442             if r:
1443                 print(bold(self.client.account))
1444         elif self.path is None:
1445             if self['detail']:
1446                 r = self.client.get_container_info(until=until)
1447             else:
1448                 cmeta = self.client.get_container_meta(until=until)
1449                 ometa = self.client.get_container_object_meta(until=until)
1450                 r = {}
1451                 if cmeta:
1452                     r['container-meta'] = pretty_keys(cmeta, '-')
1453                 if ometa:
1454                     r['object-meta'] = pretty_keys(ometa, '-')
1455         else:
1456             if self['detail']:
1457                 r = self.client.get_object_info(self.path,
1458                     version=self['object_version'])
1459             else:
1460                 r = self.client.get_object_meta(self.path,
1461                     version=self['object_version'])
1462             if r:
1463                 r = pretty_keys(pretty_keys(r, '-'))
1464         if r:
1465             print_dict(r)
1466
1467     def main(self, container____path__=None):
1468         super(self.__class__, self)._run(container____path__)
1469         self._run()
1470
1471
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
1476     """
1477
1478     @errors.generic.all
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})
1485         elif not self.path:
1486             self.client.set_container_meta({metakey: metaval})
1487         else:
1488             self.client.set_object_meta(self.path, {metakey: metaval})
1489
1490     def main(self, metakey, metaval, container____path__=None):
1491         super(self.__class__, self)._run(container____path__)
1492         self._run(metakey=metakey, metaval=metaval)
1493
1494
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>
1502     """
1503
1504     @errors.generic.all
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)
1513         else:
1514             self.client.del_object_meta(self.path, metakey)
1515
1516     def main(self, metakey, container____path__=None):
1517         super(self.__class__, self)._run(container____path__)
1518         self._run(metakey)
1519
1520
1521 @command(pithos_cmds)
1522 class store_quota(_store_account_command):
1523     """Get quota for account or container"""
1524
1525     arguments = dict(
1526         in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1527         )
1528
1529     @errors.generic.all
1530     @errors.pithos.connection
1531     @errors.pithos.container
1532     def _run(self):
1533         if self.container:
1534             reply = self.client.get_container_quota(self.container)
1535         else:
1536             reply = self.client.get_account_quota()
1537         if not self['in_bytes']:
1538             for k in reply:
1539                 reply[k] = format_size(reply[k])
1540         print_dict(pretty_keys(reply, '-'))
1541
1542     def main(self, container=None):
1543         super(self.__class__, self)._run()
1544         self.container = container
1545         self._run()
1546
1547
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
1555     """
1556
1557     @errors.generic.all
1558     def _calculate_quota(self, user_input):
1559         quota = 0
1560         try:
1561             quota = int(user_input)
1562         except ValueError:
1563             index = 0
1564             digits = [str(num) for num in range(0, 10)] + ['.']
1565             while user_input[index] in digits:
1566                 index += 1
1567             quota = user_input[:index]
1568             format = user_input[index:]
1569             try:
1570                 return to_bytes(quota, format)
1571             except Exception as qe:
1572                 raiseCLIError(qe,
1573                     'Failed to convert %s to bytes' % user_input,
1574                     details=[
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'])
1580         return quota
1581
1582     @errors.generic.all
1583     @errors.pithos.connection
1584     @errors.pithos.container
1585     def _run(self, quota):
1586         if self.container:
1587             self.client.container = self.container
1588             self.client.set_container_quota(quota)
1589         else:
1590             self.client.set_account_quota(quota)
1591
1592     def main(self, quota, container=None):
1593         super(self.__class__, self)._run()
1594         quota = self._calculate_quota(quota)
1595         self.container = container
1596         self._run(quota)
1597
1598
1599 @command(pithos_cmds)
1600 class store_versioning(_store_account_command):
1601     """Get  versioning for account or container"""
1602
1603     @errors.generic.all
1604     @errors.pithos.connection
1605     @errors.pithos.container
1606     def _run(self):
1607         if self.container:
1608             r = self.client.get_container_versioning(self.container)
1609         else:
1610             r = self.client.get_account_versioning()
1611         print_dict(r)
1612
1613     def main(self, container=None):
1614         super(self.__class__, self)._run()
1615         self.container = container
1616         self._run()
1617
1618
1619 @command(pithos_cmds)
1620 class store_setversioning(_store_account_command):
1621     """Set versioning mode (auto, none) for account or container"""
1622
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'])
1628
1629     @errors.generic.all
1630     @errors.pithos.connection
1631     @errors.pithos.container
1632     def _run(self, versioning):
1633         if self.container:
1634             self.client.container = self.container
1635             self.client.set_container_versioning(versioning)
1636         else:
1637             self.client.set_account_versioning(versioning)
1638
1639     def main(self, versioning, container=None):
1640         super(self.__class__, self)._run()
1641         self._run(self._check_versioning(versioning))
1642
1643
1644 @command(pithos_cmds)
1645 class store_group(_store_account_command):
1646     """Get groups and group members"""
1647
1648     @errors.generic.all
1649     @errors.pithos.connection
1650     def _run(self):
1651         r = self.client.get_account_group()
1652         print_dict(pretty_keys(r, '-'))
1653
1654     def main(self):
1655         super(self.__class__, self)._run()
1656         self._run()
1657
1658
1659 @command(pithos_cmds)
1660 class store_setgroup(_store_account_command):
1661     """Set a user group"""
1662
1663     @errors.generic.all
1664     @errors.pithos.connection
1665     def _run(self, groupname, *users):
1666         self.client.set_account_group(groupname, users)
1667
1668     def main(self, groupname, *users):
1669         super(self.__class__, self)._run()
1670         if users:
1671             self._run(groupname, *users)
1672         else:
1673             raiseCLIError('No users to add in group %s' % groupname)
1674
1675
1676 @command(pithos_cmds)
1677 class store_delgroup(_store_account_command):
1678     """Delete a user group"""
1679
1680     @errors.generic.all
1681     @errors.pithos.connection
1682     def _run(self, groupname):
1683         self.client.del_account_group(groupname)
1684
1685     def main(self, groupname):
1686         super(self.__class__, self)._run()
1687         self._run(groupname)
1688
1689
1690 @command(pithos_cmds)
1691 class store_sharers(_store_account_command):
1692     """List the accounts that share objects with current user"""
1693
1694     arguments = dict(
1695         detail=FlagArgument('show detailed output', '-l'),
1696         marker=ValueArgument('show output greater then marker', '--marker')
1697     )
1698
1699     @errors.generic.all
1700     @errors.pithos.connection
1701     def _run(self):
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])
1705
1706     def main(self):
1707         super(self.__class__, self)._run()
1708         self._run()
1709
1710
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.
1718     /store info -h
1719     """
1720
1721     @errors.generic.all
1722     @errors.pithos.connection
1723     @errors.pithos.container
1724     @errors.pithos.object_path
1725     def _run(self):
1726         versions = self.client.get_object_versionlist(self.path)
1727         print_items([dict(
1728             id=vitem[0],
1729             created=strftime('%d-%m-%Y %H:%M:%S', localtime(float(vitem[1])))
1730             ) for vitem in versions])
1731
1732     def main(self, container___path):
1733         super(store_versions, self)._run(
1734             container___path,
1735             path_is_optional=False)
1736         self._run()