4cdd73cb1022a3dcb9ce7be375f7ff304d886048
[kamaki] / kamaki / cli / commands / pithos.py
1 # Copyright 2011-2013 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 os import path, makedirs, walk
37
38 from kamaki.cli import command
39 from kamaki.cli.command_tree import CommandTree
40 from kamaki.cli.errors import raiseCLIError, CLISyntaxError, CLIBaseUrlError
41 from kamaki.cli.utils import (
42     format_size, to_bytes, print_dict, print_items, page_hold, bold, ask_user,
43     get_path_size, print_json, guess_mime_type)
44 from kamaki.cli.argument import FlagArgument, ValueArgument, IntArgument
45 from kamaki.cli.argument import KeyValueArgument, DateArgument
46 from kamaki.cli.argument import ProgressBarArgument
47 from kamaki.cli.commands import _command_init, errors
48 from kamaki.cli.commands import addLogSettings, DontRaiseKeyError
49 from kamaki.cli.commands import _optional_output_cmd, _optional_json
50 from kamaki.clients.pithos import PithosClient, ClientError
51 from kamaki.clients.astakos import AstakosClient
52
53 pithos_cmds = CommandTree('file', 'Pithos+/Storage API commands')
54 _commands = [pithos_cmds]
55
56
57 # Argument functionality
58
59 class DelimiterArgument(ValueArgument):
60     """
61     :value type: string
62     :value returns: given string or /
63     """
64
65     def __init__(self, caller_obj, help='', parsed_name=None, default=None):
66         super(DelimiterArgument, self).__init__(help, parsed_name, default)
67         self.caller_obj = caller_obj
68
69     @property
70     def value(self):
71         if self.caller_obj['recursive']:
72             return '/'
73         return getattr(self, '_value', self.default)
74
75     @value.setter
76     def value(self, newvalue):
77         self._value = newvalue
78
79
80 class SharingArgument(ValueArgument):
81     """Set sharing (read and/or write) groups
82     .
83     :value type: "read=term1,term2,... write=term1,term2,..."
84     .
85     :value returns: {'read':['term1', 'term2', ...],
86     .   'write':['term1', 'term2', ...]}
87     """
88
89     @property
90     def value(self):
91         return getattr(self, '_value', self.default)
92
93     @value.setter
94     def value(self, newvalue):
95         perms = {}
96         try:
97             permlist = newvalue.split(' ')
98         except AttributeError:
99             return
100         for p in permlist:
101             try:
102                 (key, val) = p.split('=')
103             except ValueError as err:
104                 raiseCLIError(
105                     err,
106                     'Error in --sharing',
107                     details='Incorrect format',
108                     importance=1)
109             if key.lower() not in ('read', 'write'):
110                 msg = 'Error in --sharing'
111                 raiseCLIError(err, msg, importance=1, details=[
112                     'Invalid permission key %s' % key])
113             val_list = val.split(',')
114             if not key in perms:
115                 perms[key] = []
116             for item in val_list:
117                 if item not in perms[key]:
118                     perms[key].append(item)
119         self._value = perms
120
121
122 class RangeArgument(ValueArgument):
123     """
124     :value type: string of the form <start>-<end> where <start> and <end> are
125         integers
126     :value returns: the input string, after type checking <start> and <end>
127     """
128
129     @property
130     def value(self):
131         return getattr(self, '_value', self.default)
132
133     @value.setter
134     def value(self, newvalues):
135         if not newvalues:
136             self._value = self.default
137             return
138         self._value = ''
139         for newvalue in newvalues.split(','):
140             self._value = ('%s,' % self._value) if self._value else ''
141             start, sep, end = newvalue.partition('-')
142             if sep:
143                 if start:
144                     start, end = (int(start), int(end))
145                     assert start <= end, 'Invalid range value %s' % newvalue
146                     self._value += '%s-%s' % (int(start), int(end))
147                 else:
148                     self._value += '-%s' % int(end)
149             else:
150                 self._value += '%s' % int(start)
151
152
153 # Command specs
154
155
156 class _pithos_init(_command_init):
157     """Initialize a pithos+ kamaki client"""
158
159     @staticmethod
160     def _is_dir(remote_dict):
161         return 'application/directory' == remote_dict.get(
162             'content_type', remote_dict.get('content-type', ''))
163
164     @DontRaiseKeyError
165     def _custom_container(self):
166         return self.config.get_cloud(self.cloud, 'pithos_container')
167
168     @DontRaiseKeyError
169     def _custom_uuid(self):
170         return self.config.get_cloud(self.cloud, 'pithos_uuid')
171
172     def _set_account(self):
173         self.account = self._custom_uuid()
174         if self.account:
175             return
176         if getattr(self, 'auth_base', False):
177             self.account = self.auth_base.user_term('id', self.token)
178         else:
179             astakos_url = self._custom_url('astakos')
180             astakos_token = self._custom_token('astakos') or self.token
181             if not astakos_url:
182                 raise CLIBaseUrlError(service='astakos')
183             astakos = AstakosClient(astakos_url, astakos_token)
184             self.account = astakos.user_term('id')
185
186     @errors.generic.all
187     @addLogSettings
188     def _run(self):
189         self.base_url = None
190         if getattr(self, 'cloud', None):
191             self.base_url = self._custom_url('pithos')
192         else:
193             self.cloud = 'default'
194         self.token = self._custom_token('pithos')
195         self.container = self._custom_container()
196
197         if getattr(self, 'auth_base', False):
198             self.token = self.token or self.auth_base.token
199             if not self.base_url:
200                 pithos_endpoints = self.auth_base.get_service_endpoints(
201                     self._custom_type('pithos') or 'object-store',
202                     self._custom_version('pithos') or '')
203                 self.base_url = pithos_endpoints['publicURL']
204         elif not self.base_url:
205             raise CLIBaseUrlError(service='pithos')
206
207         self._set_account()
208         self.client = PithosClient(
209             base_url=self.base_url,
210             token=self.token,
211             account=self.account,
212             container=self.container)
213
214     def main(self):
215         self._run()
216
217
218 class _file_account_command(_pithos_init):
219     """Base class for account level storage commands"""
220
221     def __init__(self, arguments={}, auth_base=None, cloud=None):
222         super(_file_account_command, self).__init__(
223             arguments, auth_base, cloud)
224         self['account'] = ValueArgument(
225             'Set user account (not permanent)', ('-A', '--account'))
226
227     def _run(self, custom_account=None):
228         super(_file_account_command, self)._run()
229         if custom_account:
230             self.client.account = custom_account
231         elif self['account']:
232             self.client.account = self['account']
233
234     @errors.generic.all
235     def main(self):
236         self._run()
237
238
239 class _file_container_command(_file_account_command):
240     """Base class for container level storage commands"""
241
242     container = None
243     path = None
244
245     def __init__(self, arguments={}, auth_base=None, cloud=None):
246         super(_file_container_command, self).__init__(
247             arguments, auth_base, cloud)
248         self['container'] = ValueArgument(
249             'Set container to work with (temporary)', ('-C', '--container'))
250
251     def extract_container_and_path(
252             self,
253             container_with_path,
254             path_is_optional=True):
255         """Contains all heuristics for deciding what should be used as
256         container or path. Options are:
257         * user string of the form container:path
258         * self.container, self.path variables set by super constructor, or
259         explicitly by the caller application
260         Error handling is explicit as these error cases happen only here
261         """
262         try:
263             assert isinstance(container_with_path, str)
264         except AssertionError as err:
265             if self['container'] and path_is_optional:
266                 self.container = self['container']
267                 self.client.container = self['container']
268                 return
269             raiseCLIError(err)
270
271         user_cont, sep, userpath = container_with_path.partition(':')
272
273         if sep:
274             if not user_cont:
275                 raiseCLIError(CLISyntaxError(
276                     'Container is missing\n',
277                     details=errors.pithos.container_howto))
278             alt_cont = self['container']
279             if alt_cont and user_cont != alt_cont:
280                 raiseCLIError(CLISyntaxError(
281                     'Conflict: 2 containers (%s, %s)' % (user_cont, alt_cont),
282                     details=errors.pithos.container_howto)
283                 )
284             self.container = user_cont
285             if not userpath:
286                 raiseCLIError(CLISyntaxError(
287                     'Path is missing for object in container %s' % user_cont,
288                     details=errors.pithos.container_howto)
289                 )
290             self.path = userpath
291         else:
292             alt_cont = self['container'] or self.client.container
293             if alt_cont:
294                 self.container = alt_cont
295                 self.path = user_cont
296             elif path_is_optional:
297                 self.container = user_cont
298                 self.path = None
299             else:
300                 self.container = user_cont
301                 raiseCLIError(CLISyntaxError(
302                     'Both container and path are required',
303                     details=errors.pithos.container_howto)
304                 )
305
306     @errors.generic.all
307     def _run(self, container_with_path=None, path_is_optional=True):
308         super(_file_container_command, self)._run()
309         if self['container']:
310             self.client.container = self['container']
311             if container_with_path:
312                 self.path = container_with_path
313             elif not path_is_optional:
314                 raise CLISyntaxError(
315                     'Both container and path are required',
316                     details=errors.pithos.container_howto)
317         elif container_with_path:
318             self.extract_container_and_path(
319                 container_with_path,
320                 path_is_optional)
321             self.client.container = self.container
322         self.container = self.client.container
323
324     def main(self, container_with_path=None, path_is_optional=True):
325         self._run(container_with_path, path_is_optional)
326
327
328 @command(pithos_cmds)
329 class file_list(_file_container_command, _optional_json):
330     """List containers, object trees or objects in a directory
331     Use with:
332     1 no parameters : containers in current account
333     2. one parameter (container) or --container : contents of container
334     3. <container>:<prefix> or --container=<container> <prefix>: objects in
335     .   container starting with prefix
336     """
337
338     arguments = dict(
339         detail=FlagArgument('detailed output', ('-l', '--list')),
340         limit=IntArgument('limit number of listed items', ('-n', '--number')),
341         marker=ValueArgument('output greater that marker', '--marker'),
342         prefix=ValueArgument('output starting with prefix', '--prefix'),
343         delimiter=ValueArgument('show output up to delimiter', '--delimiter'),
344         path=ValueArgument(
345             'show output starting with prefix up to /', '--path'),
346         meta=ValueArgument(
347             'show output with specified meta keys', '--meta',
348             default=[]),
349         if_modified_since=ValueArgument(
350             'show output modified since then', '--if-modified-since'),
351         if_unmodified_since=ValueArgument(
352             'show output not modified since then', '--if-unmodified-since'),
353         until=DateArgument('show metadata until then', '--until'),
354         format=ValueArgument(
355             'format to parse until data (default: d/m/Y H:M:S )', '--format'),
356         shared=FlagArgument('show only shared', '--shared'),
357         more=FlagArgument(
358             'output results in pages (-n to set items per page, default 10)',
359             '--more'),
360         exact_match=FlagArgument(
361             'Show only objects that match exactly with path',
362             '--exact-match'),
363         enum=FlagArgument('Enumerate results', '--enumerate')
364     )
365
366     def print_objects(self, object_list):
367         if self['json_output']:
368             print_json(object_list)
369             return
370         limit = int(self['limit']) if self['limit'] > 0 else len(object_list)
371         for index, obj in enumerate(object_list):
372             if self['exact_match'] and self.path and not (
373                     obj['name'] == self.path or 'content_type' in obj):
374                 continue
375             pretty_obj = obj.copy()
376             index += 1
377             empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
378             if 'subdir' in obj:
379                 continue
380             if obj['content_type'] == 'application/directory':
381                 isDir = True
382                 size = 'D'
383             else:
384                 isDir = False
385                 size = format_size(obj['bytes'])
386                 pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
387             oname = bold(obj['name'])
388             prfx = ('%s%s. ' % (empty_space, index)) if self['enum'] else ''
389             if self['detail']:
390                 print('%s%s' % (prfx, oname))
391                 print_dict(pretty_obj, exclude=('name'))
392                 print
393             else:
394                 oname = '%s%9s %s' % (prfx, size, oname)
395                 oname += '/' if isDir else ''
396                 print(oname)
397             if self['more']:
398                 page_hold(index, limit, len(object_list))
399
400     def print_containers(self, container_list):
401         if self['json_output']:
402             print_json(container_list)
403             return
404         limit = int(self['limit']) if self['limit'] > 0\
405             else len(container_list)
406         for index, container in enumerate(container_list):
407             if 'bytes' in container:
408                 size = format_size(container['bytes'])
409             prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
410             cname = '%s%s' % (prfx, bold(container['name']))
411             if self['detail']:
412                 print(cname)
413                 pretty_c = container.copy()
414                 if 'bytes' in container:
415                     pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
416                 print_dict(pretty_c, exclude=('name'))
417                 print
418             else:
419                 if 'count' in container and 'bytes' in container:
420                     print('%s (%s, %s objects)' % (
421                         cname,
422                         size,
423                         container['count']))
424                 else:
425                     print(cname)
426             if self['more']:
427                 page_hold(index + 1, limit, len(container_list))
428
429     @errors.generic.all
430     @errors.pithos.connection
431     @errors.pithos.object_path
432     @errors.pithos.container
433     def _run(self):
434         if self.container is None:
435             r = self.client.account_get(
436                 limit=False if self['more'] else self['limit'],
437                 marker=self['marker'],
438                 if_modified_since=self['if_modified_since'],
439                 if_unmodified_since=self['if_unmodified_since'],
440                 until=self['until'],
441                 show_only_shared=self['shared'])
442             self._print(r.json, self.print_containers)
443         else:
444             prefix = self.path or self['prefix']
445             r = self.client.container_get(
446                 limit=False if self['more'] else self['limit'],
447                 marker=self['marker'],
448                 prefix=prefix,
449                 delimiter=self['delimiter'],
450                 path=self['path'],
451                 if_modified_since=self['if_modified_since'],
452                 if_unmodified_since=self['if_unmodified_since'],
453                 until=self['until'],
454                 meta=self['meta'],
455                 show_only_shared=self['shared'])
456             self._print(r.json, self.print_objects)
457
458     def main(self, container____path__=None):
459         super(self.__class__, self)._run(container____path__)
460         self._run()
461
462
463 @command(pithos_cmds)
464 class file_mkdir(_file_container_command, _optional_output_cmd):
465     """Create a directory
466     Kamaki hanldes directories the same way as OOS Storage and Pithos+:
467     A directory  is   an  object  with  type  "application/directory"
468     An object with path  dir/name can exist even if  dir does not exist
469     or even if dir  is  a non  directory  object.  Users can modify dir '
470     without affecting the dir/name object in any way.
471     """
472
473     @errors.generic.all
474     @errors.pithos.connection
475     @errors.pithos.container
476     def _run(self):
477         self._optional_output(self.client.create_directory(self.path))
478
479     def main(self, container___directory):
480         super(self.__class__, self)._run(
481             container___directory,
482             path_is_optional=False)
483         self._run()
484
485
486 @command(pithos_cmds)
487 class file_touch(_file_container_command, _optional_output_cmd):
488     """Create an empty object (file)
489     If object exists, this command will reset it to 0 length
490     """
491
492     arguments = dict(
493         content_type=ValueArgument(
494             'Set content type (default: application/octet-stream)',
495             '--content-type',
496             default='application/octet-stream')
497     )
498
499     @errors.generic.all
500     @errors.pithos.connection
501     @errors.pithos.container
502     def _run(self):
503         self._optional_output(
504             self.client.create_object(self.path, self['content_type']))
505
506     def main(self, container___path):
507         super(file_touch, self)._run(
508             container___path,
509             path_is_optional=False)
510         self._run()
511
512
513 @command(pithos_cmds)
514 class file_create(_file_container_command, _optional_output_cmd):
515     """Create a container"""
516
517     arguments = dict(
518         versioning=ValueArgument(
519             'set container versioning (auto/none)', '--versioning'),
520         limit=IntArgument('set default container limit', '--limit'),
521         meta=KeyValueArgument(
522             'set container metadata (can be repeated)', '--meta')
523     )
524
525     @errors.generic.all
526     @errors.pithos.connection
527     @errors.pithos.container
528     def _run(self, container):
529         self._optional_output(self.client.create_container(
530             container=container,
531             sizelimit=self['limit'],
532             versioning=self['versioning'],
533             metadata=self['meta']))
534
535     def main(self, container=None):
536         super(self.__class__, self)._run(container)
537         if container and self.container != container:
538             raiseCLIError('Invalid container name %s' % container, details=[
539                 'Did you mean "%s" ?' % self.container,
540                 'Use --container for names containing :'])
541         self._run(container)
542
543
544 class _source_destination_command(_file_container_command):
545
546     arguments = dict(
547         destination_account=ValueArgument('', ('-a', '--dst-account')),
548         recursive=FlagArgument('', ('-R', '--recursive')),
549         prefix=FlagArgument('', '--with-prefix', default=''),
550         suffix=ValueArgument('', '--with-suffix', default=''),
551         add_prefix=ValueArgument('', '--add-prefix', default=''),
552         add_suffix=ValueArgument('', '--add-suffix', default=''),
553         prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
554         suffix_replace=ValueArgument('', '--suffix-to-replace', default=''),
555     )
556
557     def __init__(self, arguments={}, auth_base=None, cloud=None):
558         self.arguments.update(arguments)
559         super(_source_destination_command, self).__init__(
560             self.arguments, auth_base, cloud)
561
562     def _run(self, source_container___path, path_is_optional=False):
563         super(_source_destination_command, self)._run(
564             source_container___path,
565             path_is_optional)
566         self.dst_client = PithosClient(
567             base_url=self.client.base_url,
568             token=self.client.token,
569             account=self['destination_account'] or self.client.account)
570
571     @errors.generic.all
572     @errors.pithos.account
573     def _dest_container_path(self, dest_container_path):
574         if self['destination_container']:
575             self.dst_client.container = self['destination_container']
576             return (self['destination_container'], dest_container_path)
577         if dest_container_path:
578             dst = dest_container_path.split(':')
579             if len(dst) > 1:
580                 try:
581                     self.dst_client.container = dst[0]
582                     self.dst_client.get_container_info(dst[0])
583                 except ClientError as err:
584                     if err.status in (404, 204):
585                         raiseCLIError(
586                             'Destination container %s not found' % dst[0])
587                     raise
588                 else:
589                     self.dst_client.container = dst[0]
590                 return (dst[0], dst[1])
591             return(None, dst[0])
592         raiseCLIError('No destination container:path provided')
593
594     def _get_all(self, prefix):
595         return self.client.container_get(prefix=prefix).json
596
597     def _get_src_objects(self, src_path, source_version=None):
598         """Get a list of the source objects to be called
599
600         :param src_path: (str) source path
601
602         :returns: (method, params) a method that returns a list when called
603         or (object) if it is a single object
604         """
605         if src_path and src_path[-1] == '/':
606             src_path = src_path[:-1]
607
608         if self['prefix']:
609             return (self._get_all, dict(prefix=src_path))
610         try:
611             srcobj = self.client.get_object_info(
612                 src_path, version=source_version)
613         except ClientError as srcerr:
614             if srcerr.status == 404:
615                 raiseCLIError(
616                     'Source object %s not in source container %s' % (
617                         src_path, self.client.container),
618                     details=['Hint: --with-prefix to match multiple objects'])
619             elif srcerr.status not in (204,):
620                 raise
621             return (self.client.list_objects, {})
622
623         if self._is_dir(srcobj):
624             if not self['recursive']:
625                 raiseCLIError(
626                     'Object %s of cont. %s is a dir' % (
627                         src_path, self.client.container),
628                     details=['Use --recursive to access directories'])
629             return (self._get_all, dict(prefix=src_path))
630         srcobj['name'] = src_path
631         return srcobj
632
633     def src_dst_pairs(self, dst_path, source_version=None):
634         src_iter = self._get_src_objects(self.path, source_version)
635         src_N = isinstance(src_iter, tuple)
636         add_prefix = self['add_prefix'].strip('/')
637
638         if dst_path and dst_path.endswith('/'):
639             dst_path = dst_path[:-1]
640
641         try:
642             dstobj = self.dst_client.get_object_info(dst_path)
643         except ClientError as trgerr:
644             if trgerr.status in (404,):
645                 if src_N:
646                     raiseCLIError(
647                         'Cannot merge multiple paths to path %s' % dst_path,
648                         details=[
649                             'Try to use / or a directory as destination',
650                             'or create the destination dir (/file mkdir)',
651                             'or use a single object as source'])
652             elif trgerr.status not in (204,):
653                 raise
654         else:
655             if self._is_dir(dstobj):
656                 add_prefix = '%s/%s' % (dst_path.strip('/'), add_prefix)
657             elif src_N:
658                 raiseCLIError(
659                     'Cannot merge multiple paths to path' % dst_path,
660                     details=[
661                         'Try to use / or a directory as destination',
662                         'or create the destination dir (/file mkdir)',
663                         'or use a single object as source'])
664
665         if src_N:
666             (method, kwargs) = src_iter
667             for obj in method(**kwargs):
668                 name = obj['name']
669                 if name.endswith(self['suffix']):
670                     yield (name, self._get_new_object(name, add_prefix))
671         elif src_iter['name'].endswith(self['suffix']):
672             name = src_iter['name']
673             yield (name, self._get_new_object(dst_path or name, add_prefix))
674         else:
675             raiseCLIError('Source path %s conflicts with suffix %s' % (
676                 src_iter['name'], self['suffix']))
677
678     def _get_new_object(self, obj, add_prefix):
679         if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
680             obj = obj[len(self['prefix_replace']):]
681         if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
682             obj = obj[:-len(self['suffix_replace'])]
683         return add_prefix + obj + self['add_suffix']
684
685
686 @command(pithos_cmds)
687 class file_copy(_source_destination_command, _optional_output_cmd):
688     """Copy objects from container to (another) container
689     Semantics:
690     copy cont:path dir
691     .   transfer path as dir/path
692     copy cont:path cont2:
693     .   trasnfer all <obj> prefixed with path to container cont2
694     copy cont:path [cont2:]path2
695     .   transfer path to path2
696     Use options:
697     1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
698     destination is container1:path2
699     2. <container>:<path1> <path2> : make a copy in the same container
700     3. Can use --container= instead of <container1>
701     """
702
703     arguments = dict(
704         destination_account=ValueArgument(
705             'Account to copy to', ('-a', '--dst-account')),
706         destination_container=ValueArgument(
707             'use it if destination container name contains a : character',
708             ('-D', '--dst-container')),
709         public=ValueArgument('make object publicly accessible', '--public'),
710         content_type=ValueArgument(
711             'change object\'s content type', '--content-type'),
712         recursive=FlagArgument(
713             'copy directory and contents', ('-R', '--recursive')),
714         prefix=FlagArgument(
715             'Match objects prefixed with src path (feels like src_path*)',
716             '--with-prefix',
717             default=''),
718         suffix=ValueArgument(
719             'Suffix of source objects (feels like *suffix)', '--with-suffix',
720             default=''),
721         add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
722         add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
723         prefix_replace=ValueArgument(
724             'Prefix of src to replace with dst path + add_prefix, if matched',
725             '--prefix-to-replace',
726             default=''),
727         suffix_replace=ValueArgument(
728             'Suffix of src to replace with add_suffix, if matched',
729             '--suffix-to-replace',
730             default=''),
731         source_version=ValueArgument(
732             'copy specific version', ('-S', '--source-version'))
733     )
734
735     @errors.generic.all
736     @errors.pithos.connection
737     @errors.pithos.container
738     @errors.pithos.account
739     def _run(self, dst_path):
740         no_source_object = True
741         src_account = self.client.account if (
742             self['destination_account']) else None
743         for src_obj, dst_obj in self.src_dst_pairs(
744                 dst_path, self['source_version']):
745             no_source_object = False
746             r = self.dst_client.copy_object(
747                 src_container=self.client.container,
748                 src_object=src_obj,
749                 dst_container=self.dst_client.container,
750                 dst_object=dst_obj,
751                 source_account=src_account,
752                 source_version=self['source_version'],
753                 public=self['public'],
754                 content_type=self['content_type'])
755         if no_source_object:
756             raiseCLIError('No object %s in container %s' % (
757                 self.path, self.container))
758         self._optional_output(r)
759
760     def main(
761             self, source_container___path,
762             destination_container___path=None):
763         super(file_copy, self)._run(
764             source_container___path,
765             path_is_optional=False)
766         (dst_cont, dst_path) = self._dest_container_path(
767             destination_container___path)
768         self.dst_client.container = dst_cont or self.container
769         self._run(dst_path=dst_path or '')
770
771
772 @command(pithos_cmds)
773 class file_move(_source_destination_command, _optional_output_cmd):
774     """Move/rename objects from container to (another) container
775     Semantics:
776     move cont:path dir
777     .   rename path as dir/path
778     move cont:path cont2:
779     .   trasnfer all <obj> prefixed with path to container cont2
780     move cont:path [cont2:]path2
781     .   transfer path to path2
782     Use options:
783     1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
784     destination is container1:path2
785     2. <container>:<path1> <path2> : move in the same container
786     3. Can use --container= instead of <container1>
787     """
788
789     arguments = dict(
790         destination_account=ValueArgument(
791             'Account to move to', ('-a', '--dst-account')),
792         destination_container=ValueArgument(
793             'use it if destination container name contains a : character',
794             ('-D', '--dst-container')),
795         public=ValueArgument('make object publicly accessible', '--public'),
796         content_type=ValueArgument(
797             'change object\'s content type', '--content-type'),
798         recursive=FlagArgument(
799             'copy directory and contents', ('-R', '--recursive')),
800         prefix=FlagArgument(
801             'Match objects prefixed with src path (feels like src_path*)',
802             '--with-prefix',
803             default=''),
804         suffix=ValueArgument(
805             'Suffix of source objects (feels like *suffix)', '--with-suffix',
806             default=''),
807         add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
808         add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
809         prefix_replace=ValueArgument(
810             'Prefix of src to replace with dst path + add_prefix, if matched',
811             '--prefix-to-replace',
812             default=''),
813         suffix_replace=ValueArgument(
814             'Suffix of src to replace with add_suffix, if matched',
815             '--suffix-to-replace',
816             default='')
817     )
818
819     @errors.generic.all
820     @errors.pithos.connection
821     @errors.pithos.container
822     def _run(self, dst_path):
823         no_source_object = True
824         src_account = self.client.account if (
825             self['destination_account']) else None
826         for src_obj, dst_obj in self.src_dst_pairs(dst_path):
827             no_source_object = False
828             r = self.dst_client.move_object(
829                 src_container=self.container,
830                 src_object=src_obj,
831                 dst_container=self.dst_client.container,
832                 dst_object=dst_obj,
833                 source_account=src_account,
834                 public=self['public'],
835                 content_type=self['content_type'])
836         if no_source_object:
837             raiseCLIError('No object %s in container %s' % (
838                 self.path,
839                 self.container))
840         self._optional_output(r)
841
842     def main(
843             self, source_container___path,
844             destination_container___path=None):
845         super(self.__class__, self)._run(
846             source_container___path,
847             path_is_optional=False)
848         (dst_cont, dst_path) = self._dest_container_path(
849             destination_container___path)
850         (dst_cont, dst_path) = self._dest_container_path(
851             destination_container___path)
852         self.dst_client.container = dst_cont or self.container
853         self._run(dst_path=dst_path or '')
854
855
856 @command(pithos_cmds)
857 class file_append(_file_container_command, _optional_output_cmd):
858     """Append local file to (existing) remote object
859     The remote object should exist.
860     If the remote object is a directory, it is transformed into a file.
861     In the later case, objects under the directory remain intact.
862     """
863
864     arguments = dict(
865         progress_bar=ProgressBarArgument(
866             'do not show progress bar',
867             ('-N', '--no-progress-bar'),
868             default=False)
869     )
870
871     @errors.generic.all
872     @errors.pithos.connection
873     @errors.pithos.container
874     @errors.pithos.object_path
875     def _run(self, local_path):
876         (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
877         try:
878             f = open(local_path, 'rb')
879             self._optional_output(
880                 self.client.append_object(self.path, f, upload_cb))
881         except Exception:
882             self._safe_progress_bar_finish(progress_bar)
883             raise
884         finally:
885             self._safe_progress_bar_finish(progress_bar)
886
887     def main(self, local_path, container___path):
888         super(self.__class__, self)._run(
889             container___path, path_is_optional=False)
890         self._run(local_path)
891
892
893 @command(pithos_cmds)
894 class file_truncate(_file_container_command, _optional_output_cmd):
895     """Truncate remote file up to a size (default is 0)"""
896
897     @errors.generic.all
898     @errors.pithos.connection
899     @errors.pithos.container
900     @errors.pithos.object_path
901     @errors.pithos.object_size
902     def _run(self, size=0):
903         self._optional_output(self.client.truncate_object(self.path, size))
904
905     def main(self, container___path, size=0):
906         super(self.__class__, self)._run(container___path)
907         self._run(size=size)
908
909
910 @command(pithos_cmds)
911 class file_overwrite(_file_container_command, _optional_output_cmd):
912     """Overwrite part (from start to end) of a remote file
913     overwrite local-path container 10 20
914     .   will overwrite bytes from 10 to 20 of a remote file with the same name
915     .   as local-path basename
916     overwrite local-path container:path 10 20
917     .   will overwrite as above, but the remote file is named path
918     """
919
920     arguments = dict(
921         progress_bar=ProgressBarArgument(
922             'do not show progress bar',
923             ('-N', '--no-progress-bar'),
924             default=False)
925     )
926
927     def _open_file(self, local_path, start):
928         f = open(path.abspath(local_path), 'rb')
929         f.seek(0, 2)
930         f_size = f.tell()
931         f.seek(start, 0)
932         return (f, f_size)
933
934     @errors.generic.all
935     @errors.pithos.connection
936     @errors.pithos.container
937     @errors.pithos.object_path
938     @errors.pithos.object_size
939     def _run(self, local_path, start, end):
940         (start, end) = (int(start), int(end))
941         (f, f_size) = self._open_file(local_path, start)
942         (progress_bar, upload_cb) = self._safe_progress_bar(
943             'Overwrite %s bytes' % (end - start))
944         try:
945             self._optional_output(self.client.overwrite_object(
946                 obj=self.path,
947                 start=start,
948                 end=end,
949                 source_file=f,
950                 upload_cb=upload_cb))
951         finally:
952             self._safe_progress_bar_finish(progress_bar)
953
954     def main(self, local_path, container___path, start, end):
955         super(self.__class__, self)._run(
956             container___path, path_is_optional=None)
957         self.path = self.path or path.basename(local_path)
958         self._run(local_path=local_path, start=start, end=end)
959
960
961 @command(pithos_cmds)
962 class file_manifest(_file_container_command, _optional_output_cmd):
963     """Create a remote file of uploaded parts by manifestation
964     Remains functional for compatibility with OOS Storage. Users are advised
965     to use the upload command instead.
966     Manifestation is a compliant process for uploading large files. The files
967     have to be chunked in smalled files and uploaded as <prefix><increment>
968     where increment is 1, 2, ...
969     Finally, the manifest command glues partial files together in one file
970     named <prefix>
971     The upload command is faster, easier and more intuitive than manifest
972     """
973
974     arguments = dict(
975         etag=ValueArgument('check written data', '--etag'),
976         content_encoding=ValueArgument(
977             'set MIME content type', '--content-encoding'),
978         content_disposition=ValueArgument(
979             'the presentation style of the object', '--content-disposition'),
980         content_type=ValueArgument(
981             'specify content type', '--content-type',
982             default='application/octet-stream'),
983         sharing=SharingArgument(
984             '\n'.join([
985                 'define object sharing policy',
986                 '    ( "read=user1,grp1,user2,... write=user1,grp2,..." )']),
987             '--sharing'),
988         public=FlagArgument('make object publicly accessible', '--public')
989     )
990
991     @errors.generic.all
992     @errors.pithos.connection
993     @errors.pithos.container
994     @errors.pithos.object_path
995     def _run(self):
996         ctype, cenc = guess_mime_type(self.path)
997         self._optional_output(self.client.create_object_by_manifestation(
998             self.path,
999             content_encoding=self['content_encoding'] or cenc,
1000             content_disposition=self['content_disposition'],
1001             content_type=self['content_type'] or ctype,
1002             sharing=self['sharing'],
1003             public=self['public']))
1004
1005     def main(self, container___path):
1006         super(self.__class__, self)._run(
1007             container___path, path_is_optional=False)
1008         self.run()
1009
1010
1011 @command(pithos_cmds)
1012 class file_upload(_file_container_command, _optional_output_cmd):
1013     """Upload a file"""
1014
1015     arguments = dict(
1016         use_hashes=FlagArgument(
1017             'provide hashmap file instead of data', '--use-hashes'),
1018         etag=ValueArgument('check written data', '--etag'),
1019         unchunked=FlagArgument('avoid chunked transfer mode', '--unchunked'),
1020         content_encoding=ValueArgument(
1021             'set MIME content type', '--content-encoding'),
1022         content_disposition=ValueArgument(
1023             'specify objects presentation style', '--content-disposition'),
1024         content_type=ValueArgument('specify content type', '--content-type'),
1025         sharing=SharingArgument(
1026             help='\n'.join([
1027                 'define sharing object policy',
1028                 '( "read=user1,grp1,user2,... write=user1,grp2,... )']),
1029             parsed_name='--sharing'),
1030         public=FlagArgument('make object publicly accessible', '--public'),
1031         poolsize=IntArgument('set pool size', '--with-pool-size'),
1032         progress_bar=ProgressBarArgument(
1033             'do not show progress bar',
1034             ('-N', '--no-progress-bar'),
1035             default=False),
1036         overwrite=FlagArgument('Force (over)write', ('-f', '--force')),
1037         recursive=FlagArgument(
1038             'Recursively upload directory *contents* + subdirectories',
1039             ('-R', '--recursive'))
1040     )
1041
1042     def _check_container_limit(self, path):
1043         cl_dict = self.client.get_container_limit()
1044         container_limit = int(cl_dict['x-container-policy-quota'])
1045         r = self.client.container_get()
1046         used_bytes = sum(int(o['bytes']) for o in r.json)
1047         path_size = get_path_size(path)
1048         if container_limit and path_size > (container_limit - used_bytes):
1049             raiseCLIError(
1050                 'Container(%s) (limit(%s) - used(%s)) < size(%s) of %s' % (
1051                     self.client.container,
1052                     format_size(container_limit),
1053                     format_size(used_bytes),
1054                     format_size(path_size),
1055                     path),
1056                 importance=1, details=[
1057                     'Check accound limit: /file quota',
1058                     'Check container limit:',
1059                     '\t/file containerlimit get %s' % self.client.container,
1060                     'Increase container limit:',
1061                     '\t/file containerlimit set <new limit> %s' % (
1062                         self.client.container)])
1063
1064     def _path_pairs(self, local_path, remote_path):
1065         """Get pairs of local and remote paths"""
1066         lpath = path.abspath(local_path)
1067         short_path = lpath.split(path.sep)[-1]
1068         rpath = remote_path or short_path
1069         if path.isdir(lpath):
1070             if not self['recursive']:
1071                 raiseCLIError('%s is a directory' % lpath, details=[
1072                     'Use -R to upload directory contents'])
1073             robj = self.client.container_get(path=rpath)
1074             if robj.json and not self['overwrite']:
1075                 raiseCLIError(
1076                     'Objects prefixed with %s already exist' % rpath,
1077                     importance=1,
1078                     details=['Existing objects:'] + ['\t%s:\t%s' % (
1079                         o['content_type'][12:],
1080                         o['name']) for o in robj.json] + [
1081                         'Use -f to add, overwrite or resume'])
1082             if not self['overwrite']:
1083                 try:
1084                     topobj = self.client.get_object_info(rpath)
1085                     if not self._is_dir(topobj):
1086                         raiseCLIError(
1087                             'Object %s exists but it is not a dir' % rpath,
1088                             importance=1, details=['Use -f to overwrite'])
1089                 except ClientError as ce:
1090                     if ce.status != 404:
1091                         raise
1092             self._check_container_limit(lpath)
1093             prev = ''
1094             for top, subdirs, files in walk(lpath):
1095                 if top != prev:
1096                     prev = top
1097                     try:
1098                         rel_path = rpath + top.split(lpath)[1]
1099                     except IndexError:
1100                         rel_path = rpath
1101                     print('mkdir %s:%s' % (self.client.container, rel_path))
1102                     self.client.create_directory(rel_path)
1103                 for f in files:
1104                     fpath = path.join(top, f)
1105                     if path.isfile(fpath):
1106                         rel_path = rel_path.replace(path.sep, '/')
1107                         pathfix = f.replace(path.sep, '/')
1108                         yield open(fpath, 'rb'), '%s/%s' % (rel_path, pathfix)
1109                     else:
1110                         print('%s is not a regular file' % fpath)
1111         else:
1112             if not path.isfile(lpath):
1113                 raiseCLIError(('%s is not a regular file' % lpath) if (
1114                     path.exists(lpath)) else '%s does not exist' % lpath)
1115             try:
1116                 robj = self.client.get_object_info(rpath)
1117                 if remote_path and self._is_dir(robj):
1118                     rpath += '/%s' % (short_path.replace(path.sep, '/'))
1119                     self.client.get_object_info(rpath)
1120                 if not self['overwrite']:
1121                     raiseCLIError(
1122                         'Object %s already exists' % rpath,
1123                         importance=1,
1124                         details=['use -f to overwrite or resume'])
1125             except ClientError as ce:
1126                 if ce.status != 404:
1127                     raise
1128             self._check_container_limit(lpath)
1129             yield open(lpath, 'rb'), rpath
1130
1131     @errors.generic.all
1132     @errors.pithos.connection
1133     @errors.pithos.container
1134     @errors.pithos.object_path
1135     @errors.pithos.local_path
1136     def _run(self, local_path, remote_path):
1137         poolsize = self['poolsize']
1138         if poolsize > 0:
1139             self.client.MAX_THREADS = int(poolsize)
1140         params = dict(
1141             content_encoding=self['content_encoding'],
1142             content_type=self['content_type'],
1143             content_disposition=self['content_disposition'],
1144             sharing=self['sharing'],
1145             public=self['public'])
1146         uploaded = []
1147         container_info_cache = dict()
1148         for f, rpath in self._path_pairs(local_path, remote_path):
1149             print('%s --> %s:%s' % (f.name, self.client.container, rpath))
1150             if not (self['content_type'] and self['content_encoding']):
1151                 ctype, cenc = guess_mime_type(f.name)
1152                 params['content_type'] = self['content_type'] or ctype
1153                 params['content_encoding'] = self['content_encoding'] or cenc
1154             if self['unchunked']:
1155                 r = self.client.upload_object_unchunked(
1156                     rpath, f,
1157                     etag=self['etag'], withHashFile=self['use_hashes'],
1158                     **params)
1159                 if self['with_output'] or self['json_output']:
1160                     r['name'] = '%s: %s' % (self.client.container, rpath)
1161                     uploaded.append(r)
1162             else:
1163                 try:
1164                     (progress_bar, upload_cb) = self._safe_progress_bar(
1165                         'Uploading %s' % f.name.split(path.sep)[-1])
1166                     if progress_bar:
1167                         hash_bar = progress_bar.clone()
1168                         hash_cb = hash_bar.get_generator(
1169                             'Calculating block hashes')
1170                     else:
1171                         hash_cb = None
1172                     r = self.client.upload_object(
1173                         rpath, f,
1174                         hash_cb=hash_cb,
1175                         upload_cb=upload_cb,
1176                         container_info_cache=container_info_cache,
1177                         **params)
1178                     if self['with_output'] or self['json_output']:
1179                         r['name'] = '%s: %s' % (self.client.container, rpath)
1180                         uploaded.append(r)
1181                 except Exception:
1182                     self._safe_progress_bar_finish(progress_bar)
1183                     raise
1184                 finally:
1185                     self._safe_progress_bar_finish(progress_bar)
1186         self._optional_output(uploaded)
1187         print('Upload completed')
1188
1189     def main(self, local_path, container____path__=None):
1190         super(self.__class__, self)._run(container____path__)
1191         remote_path = self.path or path.basename(path.abspath(local_path))
1192         self._run(local_path=local_path, remote_path=remote_path)
1193
1194
1195 @command(pithos_cmds)
1196 class file_cat(_file_container_command):
1197     """Print remote file contents to console"""
1198
1199     arguments = dict(
1200         range=RangeArgument('show range of data', '--range'),
1201         if_match=ValueArgument('show output if ETags match', '--if-match'),
1202         if_none_match=ValueArgument(
1203             'show output if ETags match', '--if-none-match'),
1204         if_modified_since=DateArgument(
1205             'show output modified since then', '--if-modified-since'),
1206         if_unmodified_since=DateArgument(
1207             'show output unmodified since then', '--if-unmodified-since'),
1208         object_version=ValueArgument(
1209             'get the specific version', ('-O', '--object-version'))
1210     )
1211
1212     @errors.generic.all
1213     @errors.pithos.connection
1214     @errors.pithos.container
1215     @errors.pithos.object_path
1216     def _run(self):
1217         self.client.download_object(
1218             self.path,
1219             stdout,
1220             range_str=self['range'],
1221             version=self['object_version'],
1222             if_match=self['if_match'],
1223             if_none_match=self['if_none_match'],
1224             if_modified_since=self['if_modified_since'],
1225             if_unmodified_since=self['if_unmodified_since'])
1226
1227     def main(self, container___path):
1228         super(self.__class__, self)._run(
1229             container___path, path_is_optional=False)
1230         self._run()
1231
1232
1233 @command(pithos_cmds)
1234 class file_download(_file_container_command):
1235     """Download remote object as local file
1236     If local destination is a directory:
1237     *   download <container>:<path> <local dir> -R
1238     will download all files on <container> prefixed as <path>,
1239     to <local dir>/<full path> (or <local dir>\<full path> in windows)
1240     *   download <container>:<path> <local dir> --exact-match
1241     will download only one file, exactly matching <path>
1242     ATTENTION: to download cont:dir1/dir2/file there must exist objects
1243     cont:dir1 and cont:dir1/dir2 of type application/directory
1244     To create directory objects, use /file mkdir
1245     """
1246
1247     arguments = dict(
1248         resume=FlagArgument('Resume instead of overwrite', ('-r', '--resume')),
1249         range=RangeArgument('show range of data', '--range'),
1250         if_match=ValueArgument('show output if ETags match', '--if-match'),
1251         if_none_match=ValueArgument(
1252             'show output if ETags match', '--if-none-match'),
1253         if_modified_since=DateArgument(
1254             'show output modified since then', '--if-modified-since'),
1255         if_unmodified_since=DateArgument(
1256             'show output unmodified since then', '--if-unmodified-since'),
1257         object_version=ValueArgument(
1258             'get the specific version', ('-O', '--object-version')),
1259         poolsize=IntArgument('set pool size', '--with-pool-size'),
1260         progress_bar=ProgressBarArgument(
1261             'do not show progress bar',
1262             ('-N', '--no-progress-bar'),
1263             default=False),
1264         recursive=FlagArgument(
1265             'Download a remote path and all its contents',
1266             ('-R', '--recursive'))
1267     )
1268
1269     def _outputs(self, local_path):
1270         """:returns: (local_file, remote_path)"""
1271         remotes = []
1272         if self['recursive']:
1273             r = self.client.container_get(
1274                 prefix=self.path or '/',
1275                 if_modified_since=self['if_modified_since'],
1276                 if_unmodified_since=self['if_unmodified_since'])
1277             dirlist = dict()
1278             for remote in r.json:
1279                 rname = remote['name'].strip('/')
1280                 tmppath = ''
1281                 for newdir in rname.strip('/').split('/')[:-1]:
1282                     tmppath = '/'.join([tmppath, newdir])
1283                     dirlist.update({tmppath.strip('/'): True})
1284                 remotes.append((rname, file_download._is_dir(remote)))
1285             dir_remotes = [r[0] for r in remotes if r[1]]
1286             if not set(dirlist).issubset(dir_remotes):
1287                 badguys = [bg.strip('/') for bg in set(
1288                     dirlist).difference(dir_remotes)]
1289                 raiseCLIError(
1290                     'Some remote paths contain non existing directories',
1291                     details=['Missing remote directories:'] + badguys)
1292         elif self.path:
1293             r = self.client.get_object_info(
1294                 self.path,
1295                 version=self['object_version'])
1296             if file_download._is_dir(r):
1297                 raiseCLIError(
1298                     'Illegal download: Remote object %s is a directory' % (
1299                         self.path),
1300                     details=['To download a directory, try --recursive or -R'])
1301             if '/' in self.path.strip('/') and not local_path:
1302                 raiseCLIError(
1303                     'Illegal download: remote object %s contains "/"' % (
1304                         self.path),
1305                     details=[
1306                         'To download an object containing "/" characters',
1307                         'either create the remote directories or',
1308                         'specify a non-directory local path for this object'])
1309             remotes = [(self.path, False)]
1310         if not remotes:
1311             if self.path:
1312                 raiseCLIError(
1313                     'No matching path %s on container %s' % (
1314                         self.path, self.container),
1315                     details=[
1316                         'To list the contents of %s, try:' % self.container,
1317                         '   /file list %s' % self.container])
1318             raiseCLIError(
1319                 'Illegal download of container %s' % self.container,
1320                 details=[
1321                     'To download a whole container, try:',
1322                     '   /file download --recursive <container>'])
1323
1324         lprefix = path.abspath(local_path or path.curdir)
1325         if path.isdir(lprefix):
1326             for rpath, remote_is_dir in remotes:
1327                 lpath = path.sep.join([
1328                     lprefix[:-1] if lprefix.endswith(path.sep) else lprefix,
1329                     rpath.strip('/').replace('/', path.sep)])
1330                 if remote_is_dir:
1331                     if path.exists(lpath) and path.isdir(lpath):
1332                         continue
1333                     makedirs(lpath)
1334                 elif path.exists(lpath):
1335                     if not self['resume']:
1336                         print('File %s exists, aborting...' % lpath)
1337                         continue
1338                     with open(lpath, 'rwb+') as f:
1339                         yield (f, rpath)
1340                 else:
1341                     with open(lpath, 'wb+') as f:
1342                         yield (f, rpath)
1343         elif path.exists(lprefix):
1344             if len(remotes) > 1:
1345                 raiseCLIError(
1346                     '%s remote objects cannot be merged in local file %s' % (
1347                         len(remotes),
1348                         local_path),
1349                     details=[
1350                         'To download multiple objects, local path should be',
1351                         'a directory, or use download without a local path'])
1352             (rpath, remote_is_dir) = remotes[0]
1353             if remote_is_dir:
1354                 raiseCLIError(
1355                     'Remote directory %s should not replace local file %s' % (
1356                         rpath,
1357                         local_path))
1358             if self['resume']:
1359                 with open(lprefix, 'rwb+') as f:
1360                     yield (f, rpath)
1361             else:
1362                 raiseCLIError(
1363                     'Local file %s already exist' % local_path,
1364                     details=['Try --resume to overwrite it'])
1365         else:
1366             if len(remotes) > 1 or remotes[0][1]:
1367                 raiseCLIError(
1368                     'Local directory %s does not exist' % local_path)
1369             with open(lprefix, 'wb+') as f:
1370                 yield (f, remotes[0][0])
1371
1372     @errors.generic.all
1373     @errors.pithos.connection
1374     @errors.pithos.container
1375     @errors.pithos.object_path
1376     @errors.pithos.local_path
1377     def _run(self, local_path):
1378         poolsize = self['poolsize']
1379         if poolsize:
1380             self.client.MAX_THREADS = int(poolsize)
1381         progress_bar = None
1382         try:
1383             for f, rpath in self._outputs(local_path):
1384                 (
1385                     progress_bar,
1386                     download_cb) = self._safe_progress_bar(
1387                         'Download %s' % rpath)
1388                 self.client.download_object(
1389                     rpath, f,
1390                     download_cb=download_cb,
1391                     range_str=self['range'],
1392                     version=self['object_version'],
1393                     if_match=self['if_match'],
1394                     resume=self['resume'],
1395                     if_none_match=self['if_none_match'],
1396                     if_modified_since=self['if_modified_since'],
1397                     if_unmodified_since=self['if_unmodified_since'])
1398         except KeyboardInterrupt:
1399             from threading import activeCount, enumerate as activethreads
1400             timeout = 0.5
1401             while activeCount() > 1:
1402                 stdout.write('\nCancel %s threads: ' % (activeCount() - 1))
1403                 stdout.flush()
1404                 for thread in activethreads():
1405                     try:
1406                         thread.join(timeout)
1407                         stdout.write('.' if thread.isAlive() else '*')
1408                     except RuntimeError:
1409                         continue
1410                     finally:
1411                         stdout.flush()
1412                         timeout += 0.1
1413             print('\nDownload canceled by user')
1414             if local_path is not None:
1415                 print('to resume, re-run with --resume')
1416         except Exception:
1417             self._safe_progress_bar_finish(progress_bar)
1418             raise
1419         finally:
1420             self._safe_progress_bar_finish(progress_bar)
1421
1422     def main(self, container___path, local_path=None):
1423         super(self.__class__, self)._run(container___path)
1424         self._run(local_path=local_path)
1425
1426
1427 @command(pithos_cmds)
1428 class file_hashmap(_file_container_command, _optional_json):
1429     """Get the hash-map of an object"""
1430
1431     arguments = dict(
1432         if_match=ValueArgument('show output if ETags match', '--if-match'),
1433         if_none_match=ValueArgument(
1434             'show output if ETags match', '--if-none-match'),
1435         if_modified_since=DateArgument(
1436             'show output modified since then', '--if-modified-since'),
1437         if_unmodified_since=DateArgument(
1438             'show output unmodified since then', '--if-unmodified-since'),
1439         object_version=ValueArgument(
1440             'get the specific version', ('-O', '--object-version'))
1441     )
1442
1443     @errors.generic.all
1444     @errors.pithos.connection
1445     @errors.pithos.container
1446     @errors.pithos.object_path
1447     def _run(self):
1448         self._print(self.client.get_object_hashmap(
1449             self.path,
1450             version=self['object_version'],
1451             if_match=self['if_match'],
1452             if_none_match=self['if_none_match'],
1453             if_modified_since=self['if_modified_since'],
1454             if_unmodified_since=self['if_unmodified_since']), print_dict)
1455
1456     def main(self, container___path):
1457         super(self.__class__, self)._run(
1458             container___path,
1459             path_is_optional=False)
1460         self._run()
1461
1462
1463 @command(pithos_cmds)
1464 class file_delete(_file_container_command, _optional_output_cmd):
1465     """Delete a container [or an object]
1466     How to delete a non-empty container:
1467     - empty the container:  /file delete -R <container>
1468     - delete it:            /file delete <container>
1469     .
1470     Semantics of directory deletion:
1471     .a preserve the contents: /file delete <container>:<directory>
1472     .    objects of the form dir/filename can exist with a dir object
1473     .b delete contents:       /file delete -R <container>:<directory>
1474     .    all dir/* objects are affected, even if dir does not exist
1475     .
1476     To restore a deleted object OBJ in a container CONT:
1477     - get object versions: /file versions CONT:OBJ
1478     .   and choose the version to be restored
1479     - restore the object:  /file copy --source-version=<version> CONT:OBJ OBJ
1480     """
1481
1482     arguments = dict(
1483         until=DateArgument('remove history until that date', '--until'),
1484         yes=FlagArgument('Do not prompt for permission', '--yes'),
1485         recursive=FlagArgument(
1486             'empty dir or container and delete (if dir)',
1487             ('-R', '--recursive'))
1488     )
1489
1490     def __init__(self, arguments={}, auth_base=None, cloud=None):
1491         super(self.__class__, self).__init__(arguments,  auth_base, cloud)
1492         self['delimiter'] = DelimiterArgument(
1493             self,
1494             parsed_name='--delimiter',
1495             help='delete objects prefixed with <object><delimiter>')
1496
1497     @errors.generic.all
1498     @errors.pithos.connection
1499     @errors.pithos.container
1500     @errors.pithos.object_path
1501     def _run(self):
1502         if self.path:
1503             if self['yes'] or ask_user(
1504                     'Delete %s:%s ?' % (self.container, self.path)):
1505                 self._optional_output(self.client.del_object(
1506                     self.path,
1507                     until=self['until'], delimiter=self['delimiter']))
1508             else:
1509                 print('Aborted')
1510         else:
1511             if self['recursive']:
1512                 ask_msg = 'Delete container contents'
1513             else:
1514                 ask_msg = 'Delete container'
1515             if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1516                 self._optional_output(self.client.del_container(
1517                     until=self['until'], delimiter=self['delimiter']))
1518             else:
1519                 print('Aborted')
1520
1521     def main(self, container____path__=None):
1522         super(self.__class__, self)._run(container____path__)
1523         self._run()
1524
1525
1526 @command(pithos_cmds)
1527 class file_purge(_file_container_command, _optional_output_cmd):
1528     """Delete a container and release related data blocks
1529     Non-empty containers can not purged.
1530     To purge a container with content:
1531     .   /file delete -R <container>
1532     .      objects are deleted, but data blocks remain on server
1533     .   /file purge <container>
1534     .      container and data blocks are released and deleted
1535     """
1536
1537     arguments = dict(
1538         yes=FlagArgument('Do not prompt for permission', '--yes'),
1539         force=FlagArgument('purge even if not empty', ('-F', '--force'))
1540     )
1541
1542     @errors.generic.all
1543     @errors.pithos.connection
1544     @errors.pithos.container
1545     def _run(self):
1546         if self['yes'] or ask_user('Purge container %s?' % self.container):
1547             try:
1548                 r = self.client.purge_container()
1549             except ClientError as ce:
1550                 if ce.status in (409,):
1551                     if self['force']:
1552                         self.client.del_container(delimiter='/')
1553                         r = self.client.purge_container()
1554                     else:
1555                         raiseCLIError(ce, details=['Try -F to force-purge'])
1556                 else:
1557                     raise
1558             self._optional_output(r)
1559         else:
1560             print('Aborted')
1561
1562     def main(self, container=None):
1563         super(self.__class__, self)._run(container)
1564         if container and self.container != container:
1565             raiseCLIError('Invalid container name %s' % container, details=[
1566                 'Did you mean "%s" ?' % self.container,
1567                 'Use --container for names containing :'])
1568         self._run()
1569
1570
1571 @command(pithos_cmds)
1572 class file_publish(_file_container_command):
1573     """Publish the object and print the public url"""
1574
1575     @errors.generic.all
1576     @errors.pithos.connection
1577     @errors.pithos.container
1578     @errors.pithos.object_path
1579     def _run(self):
1580         print self.client.publish_object(self.path)
1581
1582     def main(self, container___path):
1583         super(self.__class__, self)._run(
1584             container___path, path_is_optional=False)
1585         self._run()
1586
1587
1588 @command(pithos_cmds)
1589 class file_unpublish(_file_container_command, _optional_output_cmd):
1590     """Unpublish an object"""
1591
1592     @errors.generic.all
1593     @errors.pithos.connection
1594     @errors.pithos.container
1595     @errors.pithos.object_path
1596     def _run(self):
1597             self._optional_output(self.client.unpublish_object(self.path))
1598
1599     def main(self, container___path):
1600         super(self.__class__, self)._run(
1601             container___path, path_is_optional=False)
1602         self._run()
1603
1604
1605 @command(pithos_cmds)
1606 class file_permissions(_pithos_init):
1607     """Manage user and group accessibility for objects
1608     Permissions are lists of users and user groups. There are read and write
1609     permissions. Users and groups with write permission have also read
1610     permission.
1611     """
1612
1613
1614 def print_permissions(permissions_dict):
1615     expected_keys = ('read', 'write')
1616     if set(permissions_dict).issubset(expected_keys):
1617         print_dict(permissions_dict)
1618     else:
1619         invalid_keys = set(permissions_dict.keys()).difference(expected_keys)
1620         raiseCLIError(
1621             'Illegal permission keys: %s' % ', '.join(invalid_keys),
1622             importance=1, details=[
1623                 'Valid permission types: %s' % ' '.join(expected_keys)])
1624
1625
1626 @command(pithos_cmds)
1627 class file_permissions_get(_file_container_command, _optional_json):
1628     """Get read and write permissions of an object"""
1629
1630     @errors.generic.all
1631     @errors.pithos.connection
1632     @errors.pithos.container
1633     @errors.pithos.object_path
1634     def _run(self):
1635         self._print(
1636             self.client.get_object_sharing(self.path), print_permissions)
1637
1638     def main(self, container___path):
1639         super(self.__class__, self)._run(
1640             container___path, path_is_optional=False)
1641         self._run()
1642
1643
1644 @command(pithos_cmds)
1645 class file_permissions_set(_file_container_command, _optional_output_cmd):
1646     """Set permissions for an object
1647     New permissions overwrite existing permissions.
1648     Permission format:
1649     -   read=<username>[,usergroup[,...]]
1650     -   write=<username>[,usegroup[,...]]
1651     E.g. to give read permissions for file F to users A and B and write for C:
1652     .       /file permissions set F read=A,B write=C
1653     """
1654
1655     @errors.generic.all
1656     def format_permission_dict(self, permissions):
1657         read = False
1658         write = False
1659         for perms in permissions:
1660             splstr = perms.split('=')
1661             if 'read' == splstr[0]:
1662                 read = [ug.strip() for ug in splstr[1].split(',')]
1663             elif 'write' == splstr[0]:
1664                 write = [ug.strip() for ug in splstr[1].split(',')]
1665             else:
1666                 msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1667                 raiseCLIError(None, msg)
1668         return (read, write)
1669
1670     @errors.generic.all
1671     @errors.pithos.connection
1672     @errors.pithos.container
1673     @errors.pithos.object_path
1674     def _run(self, read, write):
1675         self._optional_output(self.client.set_object_sharing(
1676             self.path, read_permission=read, write_permission=write))
1677
1678     def main(self, container___path, *permissions):
1679         super(self.__class__, self)._run(
1680             container___path, path_is_optional=False)
1681         read, write = self.format_permission_dict(permissions)
1682         self._run(read, write)
1683
1684
1685 @command(pithos_cmds)
1686 class file_permissions_delete(_file_container_command, _optional_output_cmd):
1687     """Delete all permissions set on object
1688     To modify permissions, use /file permissions set
1689     """
1690
1691     @errors.generic.all
1692     @errors.pithos.connection
1693     @errors.pithos.container
1694     @errors.pithos.object_path
1695     def _run(self):
1696         self._optional_output(self.client.del_object_sharing(self.path))
1697
1698     def main(self, container___path):
1699         super(self.__class__, self)._run(
1700             container___path, path_is_optional=False)
1701         self._run()
1702
1703
1704 @command(pithos_cmds)
1705 class file_info(_file_container_command, _optional_json):
1706     """Get detailed information for user account, containers or objects
1707     to get account info:    /file info
1708     to get container info:  /file info <container>
1709     to get object info:     /file info <container>:<path>
1710     """
1711
1712     arguments = dict(
1713         object_version=ValueArgument(
1714             'show specific version \ (applies only for objects)',
1715             ('-O', '--object-version'))
1716     )
1717
1718     @errors.generic.all
1719     @errors.pithos.connection
1720     @errors.pithos.container
1721     @errors.pithos.object_path
1722     def _run(self):
1723         if self.container is None:
1724             r = self.client.get_account_info()
1725         elif self.path is None:
1726             r = self.client.get_container_info(self.container)
1727         else:
1728             r = self.client.get_object_info(
1729                 self.path, version=self['object_version'])
1730         self._print(r, print_dict)
1731
1732     def main(self, container____path__=None):
1733         super(self.__class__, self)._run(container____path__)
1734         self._run()
1735
1736
1737 @command(pithos_cmds)
1738 class file_metadata(_pithos_init):
1739     """Metadata are attached on objects. They are formed as key:value pairs.
1740     They can have arbitary values.
1741     """
1742
1743
1744 @command(pithos_cmds)
1745 class file_metadata_get(_file_container_command, _optional_json):
1746     """Get metadata for account, containers or objects"""
1747
1748     arguments = dict(
1749         detail=FlagArgument('show detailed output', ('-l', '--details')),
1750         until=DateArgument('show metadata until then', '--until'),
1751         object_version=ValueArgument(
1752             'show specific version (applies only for objects)',
1753             ('-O', '--object-version'))
1754     )
1755
1756     @errors.generic.all
1757     @errors.pithos.connection
1758     @errors.pithos.container
1759     @errors.pithos.object_path
1760     def _run(self):
1761         until = self['until']
1762         r = None
1763         if self.container is None:
1764             r = self.client.get_account_info(until=until)
1765         elif self.path is None:
1766             if self['detail']:
1767                 r = self.client.get_container_info(until=until)
1768             else:
1769                 cmeta = self.client.get_container_meta(until=until)
1770                 ometa = self.client.get_container_object_meta(until=until)
1771                 r = {}
1772                 if cmeta:
1773                     r['container-meta'] = cmeta
1774                 if ometa:
1775                     r['object-meta'] = ometa
1776         else:
1777             if self['detail']:
1778                 r = self.client.get_object_info(
1779                     self.path,
1780                     version=self['object_version'])
1781             else:
1782                 r = self.client.get_object_meta(
1783                     self.path,
1784                     version=self['object_version'])
1785         if r:
1786             self._print(r, print_dict)
1787
1788     def main(self, container____path__=None):
1789         super(self.__class__, self)._run(container____path__)
1790         self._run()
1791
1792
1793 @command(pithos_cmds)
1794 class file_metadata_set(_file_container_command, _optional_output_cmd):
1795     """Set a piece of metadata for account, container or object"""
1796
1797     @errors.generic.all
1798     @errors.pithos.connection
1799     @errors.pithos.container
1800     @errors.pithos.object_path
1801     def _run(self, metakey, metaval):
1802         if not self.container:
1803             r = self.client.set_account_meta({metakey: metaval})
1804         elif not self.path:
1805             r = self.client.set_container_meta({metakey: metaval})
1806         else:
1807             r = self.client.set_object_meta(self.path, {metakey: metaval})
1808         self._optional_output(r)
1809
1810     def main(self, metakey, metaval, container____path__=None):
1811         super(self.__class__, self)._run(container____path__)
1812         self._run(metakey=metakey, metaval=metaval)
1813
1814
1815 @command(pithos_cmds)
1816 class file_metadata_delete(_file_container_command, _optional_output_cmd):
1817     """Delete metadata with given key from account, container or object
1818     - to get metadata of current account: /file metadata get
1819     - to get metadata of a container:     /file metadata get <container>
1820     - to get metadata of an object:       /file metadata get <container>:<path>
1821     """
1822
1823     @errors.generic.all
1824     @errors.pithos.connection
1825     @errors.pithos.container
1826     @errors.pithos.object_path
1827     def _run(self, metakey):
1828         if self.container is None:
1829             r = self.client.del_account_meta(metakey)
1830         elif self.path is None:
1831             r = self.client.del_container_meta(metakey)
1832         else:
1833             r = self.client.del_object_meta(self.path, metakey)
1834         self._optional_output(r)
1835
1836     def main(self, metakey, container____path__=None):
1837         super(self.__class__, self)._run(container____path__)
1838         self._run(metakey)
1839
1840
1841 @command(pithos_cmds)
1842 class file_quota(_file_account_command, _optional_json):
1843     """Get account quota"""
1844
1845     arguments = dict(
1846         in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1847     )
1848
1849     @errors.generic.all
1850     @errors.pithos.connection
1851     def _run(self):
1852
1853         def pretty_print(output):
1854             if not self['in_bytes']:
1855                 for k in output:
1856                     output[k] = format_size(output[k])
1857             print_dict(output, '-')
1858
1859         self._print(self.client.get_account_quota(), pretty_print)
1860
1861     def main(self, custom_uuid=None):
1862         super(self.__class__, self)._run(custom_account=custom_uuid)
1863         self._run()
1864
1865
1866 @command(pithos_cmds)
1867 class file_containerlimit(_pithos_init):
1868     """Container size limit commands"""
1869
1870
1871 @command(pithos_cmds)
1872 class file_containerlimit_get(_file_container_command, _optional_json):
1873     """Get container size limit"""
1874
1875     arguments = dict(
1876         in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1877     )
1878
1879     @errors.generic.all
1880     @errors.pithos.container
1881     def _run(self):
1882
1883         def pretty_print(output):
1884             if not self['in_bytes']:
1885                 for k, v in output.items():
1886                     output[k] = 'unlimited' if '0' == v else format_size(v)
1887             print_dict(output, '-')
1888
1889         self._print(
1890             self.client.get_container_limit(self.container), pretty_print)
1891
1892     def main(self, container=None):
1893         super(self.__class__, self)._run()
1894         self.container = container
1895         self._run()
1896
1897
1898 @command(pithos_cmds)
1899 class file_containerlimit_set(_file_account_command, _optional_output_cmd):
1900     """Set new storage limit for a container
1901     By default, the limit is set in bytes
1902     Users may specify a different unit, e.g:
1903     /file containerlimit set 2.3GB mycontainer
1904     Valid units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1905     To set container limit to "unlimited", use 0
1906     """
1907
1908     @errors.generic.all
1909     def _calculate_limit(self, user_input):
1910         limit = 0
1911         try:
1912             limit = int(user_input)
1913         except ValueError:
1914             index = 0
1915             digits = [str(num) for num in range(0, 10)] + ['.']
1916             while user_input[index] in digits:
1917                 index += 1
1918             limit = user_input[:index]
1919             format = user_input[index:]
1920             try:
1921                 return to_bytes(limit, format)
1922             except Exception as qe:
1923                 msg = 'Failed to convert %s to bytes' % user_input,
1924                 raiseCLIError(qe, msg, details=[
1925                     'Syntax: containerlimit set <limit>[format] [container]',
1926                     'e.g.: containerlimit set 2.3GB mycontainer',
1927                     'Valid formats:',
1928                     '(*1024): B, KiB, MiB, GiB, TiB',
1929                     '(*1000): B, KB, MB, GB, TB'])
1930         return limit
1931
1932     @errors.generic.all
1933     @errors.pithos.connection
1934     @errors.pithos.container
1935     def _run(self, limit):
1936         if self.container:
1937             self.client.container = self.container
1938         self._optional_output(self.client.set_container_limit(limit))
1939
1940     def main(self, limit, container=None):
1941         super(self.__class__, self)._run()
1942         limit = self._calculate_limit(limit)
1943         self.container = container
1944         self._run(limit)
1945
1946
1947 @command(pithos_cmds)
1948 class file_versioning(_pithos_init):
1949     """Manage the versioning scheme of current pithos user account"""
1950
1951
1952 @command(pithos_cmds)
1953 class file_versioning_get(_file_account_command, _optional_json):
1954     """Get  versioning for account or container"""
1955
1956     @errors.generic.all
1957     @errors.pithos.connection
1958     @errors.pithos.container
1959     def _run(self):
1960         self._print(
1961             self.client.get_container_versioning(self.container), print_dict)
1962
1963     def main(self, container):
1964         super(self.__class__, self)._run()
1965         self.container = container
1966         self._run()
1967
1968
1969 @command(pithos_cmds)
1970 class file_versioning_set(_file_account_command, _optional_output_cmd):
1971     """Set versioning mode (auto, none) for account or container"""
1972
1973     def _check_versioning(self, versioning):
1974         if versioning and versioning.lower() in ('auto', 'none'):
1975             return versioning.lower()
1976         raiseCLIError('Invalid versioning %s' % versioning, details=[
1977             'Versioning can be auto or none'])
1978
1979     @errors.generic.all
1980     @errors.pithos.connection
1981     @errors.pithos.container
1982     def _run(self, versioning):
1983         self.client.container = self.container
1984         r = self.client.set_container_versioning(versioning)
1985         self._optional_output(r)
1986
1987     def main(self, versioning, container):
1988         super(self.__class__, self)._run()
1989         self._run(self._check_versioning(versioning))
1990
1991
1992 @command(pithos_cmds)
1993 class file_group(_pithos_init):
1994     """Manage access groups and group members"""
1995
1996
1997 @command(pithos_cmds)
1998 class file_group_list(_file_account_command, _optional_json):
1999     """list all groups and group members"""
2000
2001     @errors.generic.all
2002     @errors.pithos.connection
2003     def _run(self):
2004         self._print(self.client.get_account_group(), print_dict, delim='-')
2005
2006     def main(self):
2007         super(self.__class__, self)._run()
2008         self._run()
2009
2010
2011 @command(pithos_cmds)
2012 class file_group_set(_file_account_command, _optional_output_cmd):
2013     """Set a user group"""
2014
2015     @errors.generic.all
2016     @errors.pithos.connection
2017     def _run(self, groupname, *users):
2018         self._optional_output(self.client.set_account_group(groupname, users))
2019
2020     def main(self, groupname, *users):
2021         super(self.__class__, self)._run()
2022         if users:
2023             self._run(groupname, *users)
2024         else:
2025             raiseCLIError('No users to add in group %s' % groupname)
2026
2027
2028 @command(pithos_cmds)
2029 class file_group_delete(_file_account_command, _optional_output_cmd):
2030     """Delete a user group"""
2031
2032     @errors.generic.all
2033     @errors.pithos.connection
2034     def _run(self, groupname):
2035         self._optional_output(self.client.del_account_group(groupname))
2036
2037     def main(self, groupname):
2038         super(self.__class__, self)._run()
2039         self._run(groupname)
2040
2041
2042 @command(pithos_cmds)
2043 class file_sharers(_file_account_command, _optional_json):
2044     """List the accounts that share objects with current user"""
2045
2046     arguments = dict(
2047         detail=FlagArgument('show detailed output', ('-l', '--details')),
2048         marker=ValueArgument('show output greater then marker', '--marker')
2049     )
2050
2051     @errors.generic.all
2052     @errors.pithos.connection
2053     def _run(self):
2054         accounts = self.client.get_sharing_accounts(marker=self['marker'])
2055         uuids = [acc['name'] for acc in accounts]
2056         try:
2057             astakos_responce = self.auth_base.post_user_catalogs(uuids)
2058             usernames = astakos_responce.json
2059             r = usernames['uuid_catalog']
2060         except Exception as e:
2061             print 'WARNING: failed to call user_catalogs, %s' % e
2062             r = dict(sharer_uuid=uuids)
2063             usernames = accounts
2064         if self['json_output'] or self['detail']:
2065             self._print(usernames)
2066         else:
2067             self._print(r, print_dict)
2068
2069     def main(self):
2070         super(self.__class__, self)._run()
2071         self._run()
2072
2073
2074 def version_print(versions):
2075     print_items([dict(id=vitem[0], created=strftime(
2076         '%d-%m-%Y %H:%M:%S',
2077         localtime(float(vitem[1])))) for vitem in versions])
2078
2079
2080 @command(pithos_cmds)
2081 class file_versions(_file_container_command, _optional_json):
2082     """Get the list of object versions
2083     Deleted objects may still have versions that can be used to restore it and
2084     get information about its previous state.
2085     The version number can be used in a number of other commands, like info,
2086     copy, move, meta. See these commands for more information, e.g.
2087     /file info -h
2088     """
2089
2090     @errors.generic.all
2091     @errors.pithos.connection
2092     @errors.pithos.container
2093     @errors.pithos.object_path
2094     def _run(self):
2095         self._print(
2096             self.client.get_object_versionlist(self.path), version_print)
2097
2098     def main(self, container___path):
2099         super(file_versions, self)._run(
2100             container___path,
2101             path_is_optional=False)
2102         self._run()