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