1 # Copyright 2011-2013 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
11 # 2. Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following
13 # disclaimer in the documentation and/or other materials
14 # provided with the distribution.
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.command
34 from time import localtime, strftime
35 from os import path, makedirs, walk
36 from io import StringIO
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
54 pithos_cmds = CommandTree('file', 'Pithos+/Storage API commands')
55 _commands = [pithos_cmds]
58 # Argument functionality
61 class SharingArgument(ValueArgument):
62 """Set sharing (read and/or write) groups
64 :value type: "read=term1,term2,... write=term1,term2,..."
66 :value returns: {'read':['term1', 'term2', ...],
67 . 'write':['term1', 'term2', ...]}
72 return getattr(self, '_value', self.default)
75 def value(self, newvalue):
78 permlist = newvalue.split(' ')
79 except AttributeError:
83 (key, val) = p.split('=')
84 except ValueError as err:
88 details='Incorrect format',
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(',')
98 if item not in perms[key]:
99 perms[key].append(item)
103 class RangeArgument(ValueArgument):
105 :value type: string of the form <start>-<end> where <start> and <end> are
107 :value returns: the input string, after type checking <start> and <end>
112 return getattr(self, '_value', self.default)
115 def value(self, newvalues):
117 self._value = self.default
120 for newvalue in newvalues.split(','):
121 self._value = ('%s,' % self._value) if self._value else ''
122 start, sep, end = newvalue.partition('-')
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))
129 self._value += '-%s' % int(end)
131 self._value += '%s' % int(start)
137 class _pithos_init(_command_init):
138 """Initialize a pithos+ kamaki client"""
141 def _is_dir(remote_dict):
142 return 'application/directory' == remote_dict.get(
143 'content_type', remote_dict.get('content-type', ''))
146 def _custom_container(self):
147 return self.config.get_cloud(self.cloud, 'pithos_container')
150 def _custom_uuid(self):
151 return self.config.get_cloud(self.cloud, 'pithos_uuid')
153 def _set_account(self):
154 self.account = self._custom_uuid()
157 if getattr(self, 'auth_base', False):
158 self.account = self.auth_base.user_term('id', self.token)
160 astakos_url = self._custom_url('astakos')
161 astakos_token = self._custom_token('astakos') or self.token
163 raise CLIBaseUrlError(service='astakos')
164 astakos = AstakosClient(astakos_url, astakos_token)
165 self.account = astakos.user_term('id')
171 if getattr(self, 'cloud', None):
172 self.base_url = self._custom_url('pithos')
174 self.cloud = 'default'
175 self.token = self._custom_token('pithos')
176 self.container = self._custom_container()
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')
189 self.client = PithosClient(
190 base_url=self.base_url,
192 account=self.account,
193 container=self.container)
199 class _file_account_command(_pithos_init):
200 """Base class for account level storage commands"""
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'))
208 def _run(self, custom_account=None):
209 super(_file_account_command, self)._run()
211 self.client.account = custom_account
212 elif self['account']:
213 self.client.account = self['account']
220 class _file_container_command(_file_account_command):
221 """Base class for container level storage commands"""
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'))
232 def extract_container_and_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
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']
252 user_cont, sep, userpath = container_with_path.partition(':')
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)
265 self.container = user_cont
267 raiseCLIError(CLISyntaxError(
268 'Path is missing for object in container %s' % user_cont,
269 details=errors.pithos.container_howto)
273 alt_cont = self['container'] or self.client.container
275 self.container = alt_cont
276 self.path = user_cont
277 elif path_is_optional:
278 self.container = user_cont
281 self.container = user_cont
282 raiseCLIError(CLISyntaxError(
283 'Both container and path are required',
284 details=errors.pithos.container_howto)
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(
302 self.client.container = self.container
303 self.container = self.client.container
305 def main(self, container_with_path=None, path_is_optional=True):
306 self._run(container_with_path, path_is_optional)
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
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
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'),
325 'show output starting with prefix up to /', '--path'),
327 'show output with specified meta keys', '--meta',
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',
341 enum=FlagArgument('Enumerate results', '--enumerate')
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):
349 pretty_obj = obj.copy()
351 empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
354 if obj['content_type'] == 'application/directory':
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'])
363 '%s%s. ' % (empty_space, index)) if self['enum'] else ''
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')
369 oname = u'%s%9s %s' % (prfx, size, oname)
370 oname += u'/' if isDir else u''
371 self._out.writelines(oname + u'\n')
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)
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')
389 if 'count' in container and 'bytes' in container:
390 self._out.writelines(u'%s (%s, %s objects)\n' % (
391 cname, size, container['count']))
393 self._out.writelines(cname + '\n')
396 @errors.pithos.connection
397 @errors.pithos.object_path
398 @errors.pithos.container
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'],
408 show_only_shared=self['shared'])
409 files, prnt = self._filter_by_name(r.json), self.print_containers
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'],
416 delimiter=self['delimiter'],
418 if_modified_since=self['if_modified_since'],
419 if_unmodified_since=self['if_unmodified_since'],
422 show_only_shared=self['shared'])
423 files, prnt = self._filter_by_name(r.json), self.print_objects
425 outbu, self._out = self._out, StringIO()
427 if self['json_output']:
433 pager(self._out.getvalue())
436 def main(self, container____path__=None):
437 super(self.__class__, self)._run(container____path__)
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.
452 @errors.pithos.connection
453 @errors.pithos.container
455 self._optional_output(self.client.create_directory(self.path))
457 def main(self, container___directory):
458 super(self.__class__, self)._run(
459 container___directory,
460 path_is_optional=False)
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
471 content_type=ValueArgument(
472 'Set content type (default: application/octet-stream)',
474 default='application/octet-stream')
478 @errors.pithos.connection
479 @errors.pithos.container
481 self._optional_output(
482 self.client.create_object(self.path, self['content_type']))
484 def main(self, container___path):
485 super(file_touch, self)._run(
487 path_is_optional=False)
491 @command(pithos_cmds)
492 class file_create(_file_container_command, _optional_output_cmd):
493 """Create a container"""
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')
504 @errors.pithos.connection
505 @errors.pithos.container
506 def _run(self, container):
507 self._optional_output(self.client.create_container(
509 sizelimit=self['limit'],
510 versioning=self['versioning'],
511 metadata=self['meta']))
513 def main(self, container=None):
514 super(self.__class__, self)._run(container)
515 if container and self.container != container:
516 raiseCLIError('Invalid container name %s' % container, details=[
517 'Did you mean "%s" ?' % self.container,
518 'Use --container for names containing :'])
522 class _source_destination_command(_file_container_command):
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=''),
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)
540 def _run(self, source_container___path, path_is_optional=False):
541 super(_source_destination_command, self)._run(
542 source_container___path,
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)
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(':')
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):
564 'Destination container %s not found' % dst[0])
567 self.dst_client.container = dst[0]
568 return (dst[0], dst[1])
570 raiseCLIError('No destination container:path provided')
572 def _get_all(self, prefix):
573 return self.client.container_get(prefix=prefix).json
575 def _get_src_objects(self, src_path, source_version=None):
576 """Get a list of the source objects to be called
578 :param src_path: (str) source path
580 :returns: (method, params) a method that returns a list when called
581 or (object) if it is a single object
583 if src_path and src_path[-1] == '/':
584 src_path = src_path[:-1]
587 return (self._get_all, dict(prefix=src_path))
589 srcobj = self.client.get_object_info(
590 src_path, version=source_version)
591 except ClientError as srcerr:
592 if srcerr.status == 404:
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,):
599 return (self.client.list_objects, {})
601 if self._is_dir(srcobj):
602 if not self['recursive']:
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
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('/')
616 if dst_path and dst_path.endswith('/'):
617 dst_path = dst_path[:-1]
620 dstobj = self.dst_client.get_object_info(dst_path)
621 except ClientError as trgerr:
622 if trgerr.status in (404,):
625 'Cannot merge multiple paths to path %s' % dst_path,
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,):
633 if self._is_dir(dstobj):
634 add_prefix = '%s/%s' % (dst_path.strip('/'), add_prefix)
637 'Cannot merge multiple paths to path' % dst_path,
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'])
644 (method, kwargs) = src_iter
645 for obj in method(**kwargs):
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))
653 raiseCLIError('Source path %s conflicts with suffix %s' % (
654 src_iter['name'], self['suffix']))
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']
664 @command(pithos_cmds)
665 class file_copy(_source_destination_command, _optional_output_cmd):
666 """Copy objects from container to (another) container
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
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>
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')),
693 'Match objects prefixed with src path (feels like src_path*)',
696 suffix=ValueArgument(
697 'Suffix of source objects (feels like *suffix)', '--with-suffix',
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',
705 suffix_replace=ValueArgument(
706 'Suffix of src to replace with add_suffix, if matched',
707 '--suffix-to-replace',
709 source_version=ValueArgument(
710 'copy specific version', ('-S', '--source-version'))
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,
727 dst_container=self.dst_client.container,
729 source_account=src_account,
730 source_version=self['source_version'],
731 public=self['public'],
732 content_type=self['content_type'])
734 raiseCLIError('No object %s in container %s' % (
735 self.path, self.container))
736 self._optional_output(r)
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 '')
750 @command(pithos_cmds)
751 class file_move(_source_destination_command, _optional_output_cmd):
752 """Move/rename objects from container to (another) container
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
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>
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')),
779 'Match objects prefixed with src path (feels like src_path*)',
782 suffix=ValueArgument(
783 'Suffix of source objects (feels like *suffix)', '--with-suffix',
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',
791 suffix_replace=ValueArgument(
792 'Suffix of src to replace with add_suffix, if matched',
793 '--suffix-to-replace',
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,
809 dst_container=self.dst_client.container,
811 source_account=src_account,
812 public=self['public'],
813 content_type=self['content_type'])
815 raiseCLIError('No object %s in container %s' % (
818 self._optional_output(r)
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 '')
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.
843 progress_bar=ProgressBarArgument(
844 'do not show progress bar',
845 ('-N', '--no-progress-bar'),
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')
856 f = open(local_path, 'rb')
857 self._optional_output(
858 self.client.append_object(self.path, f, upload_cb))
860 self._safe_progress_bar_finish(progress_bar)
863 self._safe_progress_bar_finish(progress_bar)
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)
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)"""
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))
883 def main(self, container___path, size=0):
884 super(self.__class__, self)._run(container___path)
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
899 progress_bar=ProgressBarArgument(
900 'do not show progress bar',
901 ('-N', '--no-progress-bar'),
905 def _open_file(self, local_path, start):
906 f = open(path.abspath(local_path), 'rb')
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))
923 self._optional_output(self.client.overwrite_object(
928 upload_cb=upload_cb))
930 self._safe_progress_bar_finish(progress_bar)
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)
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
949 The upload command is faster, easier and more intuitive than manifest
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(
963 'define object sharing policy',
964 ' ( "read=user1,grp1,user2,... write=user1,grp2,..." )']),
966 public=FlagArgument('make object publicly accessible', '--public')
970 @errors.pithos.connection
971 @errors.pithos.container
972 @errors.pithos.object_path
974 ctype, cenc = guess_mime_type(self.path)
975 self._optional_output(self.client.create_object_by_manifestation(
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']))
983 def main(self, container___path):
984 super(self.__class__, self)._run(
985 container___path, path_is_optional=False)
989 @command(pithos_cmds)
990 class file_upload(_file_container_command, _optional_output_cmd):
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(
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'),
1014 overwrite=FlagArgument('Force (over)write', ('-f', '--force')),
1015 recursive=FlagArgument(
1016 'Recursively upload directory *contents* + subdirectories',
1017 ('-R', '--recursive'))
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):
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),
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)])
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']:
1054 'Objects prefixed with %s already exist' % rpath,
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']:
1062 topobj = self.client.get_object_info(rpath)
1063 if not self._is_dir(topobj):
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:
1070 self._check_container_limit(lpath)
1072 for top, subdirs, files in walk(lpath):
1076 rel_path = rpath + top.split(lpath)[1]
1079 print('mkdir %s:%s' % (self.client.container, rel_path))
1080 self.client.create_directory(rel_path)
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)
1088 print('%s is not a regular file' % fpath)
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)
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']:
1100 'Object %s already exists' % rpath,
1102 details=['use -f to overwrite or resume'])
1103 except ClientError as ce:
1104 if ce.status != 404:
1106 self._check_container_limit(lpath)
1107 yield open(lpath, 'rb'), rpath
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']
1117 self.client.MAX_THREADS = int(poolsize)
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'])
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(
1135 etag=self['etag'], withHashFile=self['use_hashes'],
1137 if self['with_output'] or self['json_output']:
1138 r['name'] = '%s: %s' % (self.client.container, rpath)
1142 (progress_bar, upload_cb) = self._safe_progress_bar(
1143 'Uploading %s' % f.name.split(path.sep)[-1])
1145 hash_bar = progress_bar.clone()
1146 hash_cb = hash_bar.get_generator(
1147 'Calculating block hashes')
1150 r = self.client.upload_object(
1153 upload_cb=upload_cb,
1154 container_info_cache=container_info_cache,
1156 if self['with_output'] or self['json_output']:
1157 r['name'] = '%s: %s' % (self.client.container, rpath)
1160 self._safe_progress_bar_finish(progress_bar)
1163 self._safe_progress_bar_finish(progress_bar)
1164 self._optional_output(uploaded)
1165 print('Upload completed')
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)
1173 @command(pithos_cmds)
1174 class file_cat(_file_container_command):
1175 """Print remote file contents to console"""
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'))
1191 @errors.pithos.connection
1192 @errors.pithos.container
1193 @errors.pithos.object_path
1195 self.client.download_object(
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'])
1205 def main(self, container___path):
1206 super(self.__class__, self)._run(
1207 container___path, path_is_optional=False)
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
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'),
1242 recursive=FlagArgument(
1243 'Download a remote path and all its contents',
1244 ('-R', '--recursive'))
1247 def _outputs(self, local_path):
1248 """:returns: (local_file, remote_path)"""
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'])
1256 for remote in r.json:
1257 rname = remote['name'].strip('/')
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)]
1268 'Some remote paths contain non existing directories',
1269 details=['Missing remote directories:'] + badguys)
1271 r = self.client.get_object_info(
1273 version=self['object_version'])
1274 if file_download._is_dir(r):
1276 'Illegal download: Remote object %s is a directory' % (
1278 details=['To download a directory, try --recursive or -R'])
1279 if '/' in self.path.strip('/') and not local_path:
1281 'Illegal download: remote object %s contains "/"' % (
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)]
1291 'No matching path %s on container %s' % (
1292 self.path, self.container),
1294 'To list the contents of %s, try:' % self.container,
1295 ' /file list %s' % self.container])
1297 'Illegal download of container %s' % self.container,
1299 'To download a whole container, try:',
1300 ' /file download --recursive <container>'])
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)])
1309 if path.exists(lpath) and path.isdir(lpath):
1312 elif path.exists(lpath):
1313 if not self['resume']:
1314 print('File %s exists, aborting...' % lpath)
1316 with open(lpath, 'rwb+') as f:
1319 with open(lpath, 'wb+') as f:
1321 elif path.exists(lprefix):
1322 if len(remotes) > 1:
1324 '%s remote objects cannot be merged in local file %s' % (
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]
1333 'Remote directory %s should not replace local file %s' % (
1337 with open(lprefix, 'rwb+') as f:
1341 'Local file %s already exist' % local_path,
1342 details=['Try --resume to overwrite it'])
1344 if len(remotes) > 1 or remotes[0][1]:
1346 'Local directory %s does not exist' % local_path)
1347 with open(lprefix, 'wb+') as f:
1348 yield (f, remotes[0][0])
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']
1358 self.client.MAX_THREADS = int(poolsize)
1361 for f, rpath in self._outputs(local_path):
1364 download_cb) = self._safe_progress_bar(
1365 'Download %s' % rpath)
1366 self.client.download_object(
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
1379 while activeCount() > 1:
1380 self._out.write('\nCancel %s threads: ' % (activeCount() - 1))
1382 for thread in activethreads():
1384 thread.join(timeout)
1385 self._out.write('.' if thread.isAlive() else '*')
1386 except RuntimeError:
1391 print('\nDownload canceled by user')
1392 if local_path is not None:
1393 print('to resume, re-run with --resume')
1395 self._safe_progress_bar_finish(progress_bar)
1398 self._safe_progress_bar_finish(progress_bar)
1400 def main(self, container___path, local_path=None):
1401 super(self.__class__, self)._run(container___path)
1402 self._run(local_path=local_path)
1405 @command(pithos_cmds)
1406 class file_hashmap(_file_container_command, _optional_json):
1407 """Get the hash-map of an object"""
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'))
1422 @errors.pithos.connection
1423 @errors.pithos.container
1424 @errors.pithos.object_path
1426 self._print(self.client.get_object_hashmap(
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)
1434 def main(self, container___path):
1435 super(self.__class__, self)._run(
1437 path_is_optional=False)
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>
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
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
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')
1471 @errors.pithos.connection
1472 @errors.pithos.container
1473 @errors.pithos.object_path
1476 if self['yes'] or ask_user(
1477 'Delete %s:%s ?' % (self.container, self.path)):
1478 self._optional_output(self.client.del_object(
1480 until=self['until'],
1481 delimiter='/' if self['recursive'] else self['delimiter']))
1484 elif self.container:
1485 if self['recursive']:
1486 ask_msg = 'Delete container contents'
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']))
1496 raiseCLIError('Nothing to delete, please provide container[:path]')
1498 def main(self, container____path__=None):
1499 super(self.__class__, self)._run(container____path__)
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
1515 yes=FlagArgument('Do not prompt for permission', '--yes'),
1516 force=FlagArgument('purge even if not empty', ('-F', '--force'))
1520 @errors.pithos.connection
1521 @errors.pithos.container
1523 if self['yes'] or ask_user('Purge container %s?' % self.container):
1525 r = self.client.purge_container()
1526 except ClientError as ce:
1527 if ce.status in (409,):
1529 self.client.del_container(delimiter='/')
1530 r = self.client.purge_container()
1532 raiseCLIError(ce, details=['Try -F to force-purge'])
1535 self._optional_output(r)
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 :'])
1548 @command(pithos_cmds)
1549 class file_publish(_file_container_command):
1550 """Publish the object and print the public url"""
1553 @errors.pithos.connection
1554 @errors.pithos.container
1555 @errors.pithos.object_path
1557 print self.client.publish_object(self.path)
1559 def main(self, container___path):
1560 super(self.__class__, self)._run(
1561 container___path, path_is_optional=False)
1565 @command(pithos_cmds)
1566 class file_unpublish(_file_container_command, _optional_output_cmd):
1567 """Unpublish an object"""
1570 @errors.pithos.connection
1571 @errors.pithos.container
1572 @errors.pithos.object_path
1574 self._optional_output(self.client.unpublish_object(self.path))
1576 def main(self, container___path):
1577 super(self.__class__, self)._run(
1578 container___path, path_is_optional=False)
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
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)
1596 invalid_keys = set(permissions_dict.keys()).difference(expected_keys)
1598 'Illegal permission keys: %s' % ', '.join(invalid_keys),
1599 importance=1, details=[
1600 'Valid permission types: %s' % ' '.join(expected_keys)])
1603 @command(pithos_cmds)
1604 class file_permissions_get(_file_container_command, _optional_json):
1605 """Get read and write permissions of an object"""
1608 @errors.pithos.connection
1609 @errors.pithos.container
1610 @errors.pithos.object_path
1613 self.client.get_object_sharing(self.path), print_permissions)
1615 def main(self, container___path):
1616 super(self.__class__, self)._run(
1617 container___path, path_is_optional=False)
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.
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
1633 def format_permission_dict(self, permissions):
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(',')]
1643 msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1644 raiseCLIError(None, msg)
1645 return (read, write)
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))
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)
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
1669 @errors.pithos.connection
1670 @errors.pithos.container
1671 @errors.pithos.object_path
1673 self._optional_output(self.client.del_object_sharing(self.path))
1675 def main(self, container___path):
1676 super(self.__class__, self)._run(
1677 container___path, path_is_optional=False)
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>
1690 object_version=ValueArgument(
1691 'show specific version \ (applies only for objects)',
1692 ('-O', '--object-version'))
1696 @errors.pithos.connection
1697 @errors.pithos.container
1698 @errors.pithos.object_path
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)
1705 r = self.client.get_object_info(
1706 self.path, version=self['object_version'])
1707 self._print(r, print_dict)
1709 def main(self, container____path__=None):
1710 super(self.__class__, self)._run(container____path__)
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.
1721 @command(pithos_cmds)
1722 class file_metadata_get(_file_container_command, _optional_json):
1723 """Get metadata for account, containers or objects"""
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'))
1734 @errors.pithos.connection
1735 @errors.pithos.container
1736 @errors.pithos.object_path
1738 until = self['until']
1740 if self.container is None:
1741 r = self.client.get_account_info(until=until)
1742 elif self.path is None:
1744 r = self.client.get_container_info(until=until)
1746 cmeta = self.client.get_container_meta(until=until)
1747 ometa = self.client.get_container_object_meta(until=until)
1750 r['container-meta'] = cmeta
1752 r['object-meta'] = ometa
1755 r = self.client.get_object_info(
1757 version=self['object_version'])
1759 r = self.client.get_object_meta(
1761 version=self['object_version'])
1763 self._print(r, print_dict)
1765 def main(self, container____path__=None):
1766 super(self.__class__, self)._run(container____path__)
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"""
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})
1782 r = self.client.set_container_meta({metakey: metaval})
1784 r = self.client.set_object_meta(self.path, {metakey: metaval})
1785 self._optional_output(r)
1787 def main(self, metakey, metaval, container____path__=None):
1788 super(self.__class__, self)._run(container____path__)
1789 self._run(metakey=metakey, metaval=metaval)
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>
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)
1810 r = self.client.del_object_meta(self.path, metakey)
1811 self._optional_output(r)
1813 def main(self, metakey, container____path__=None):
1814 super(self.__class__, self)._run(container____path__)
1818 @command(pithos_cmds)
1819 class file_quota(_file_account_command, _optional_json):
1820 """Get account quota"""
1823 in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1827 @errors.pithos.connection
1830 def pretty_print(output):
1831 if not self['in_bytes']:
1833 output[k] = format_size(output[k])
1834 print_dict(output, '-')
1836 self._print(self.client.get_account_quota(), pretty_print)
1838 def main(self, custom_uuid=None):
1839 super(self.__class__, self)._run(custom_account=custom_uuid)
1843 @command(pithos_cmds)
1844 class file_containerlimit(_pithos_init):
1845 """Container size limit commands"""
1848 @command(pithos_cmds)
1849 class file_containerlimit_get(_file_container_command, _optional_json):
1850 """Get container size limit"""
1853 in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1857 @errors.pithos.container
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, '-')
1867 self.client.get_container_limit(self.container), pretty_print)
1869 def main(self, container=None):
1870 super(self.__class__, self)._run()
1871 self.container = container
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
1886 def _calculate_limit(self, user_input):
1889 limit = int(user_input)
1892 digits = [str(num) for num in range(0, 10)] + ['.']
1893 while user_input[index] in digits:
1895 limit = user_input[:index]
1896 format = user_input[index:]
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',
1905 '(*1024): B, KiB, MiB, GiB, TiB',
1906 '(*1000): B, KB, MB, GB, TB'])
1910 @errors.pithos.connection
1911 @errors.pithos.container
1912 def _run(self, limit):
1914 self.client.container = self.container
1915 self._optional_output(self.client.set_container_limit(limit))
1917 def main(self, limit, container=None):
1918 super(self.__class__, self)._run()
1919 limit = self._calculate_limit(limit)
1920 self.container = container
1924 @command(pithos_cmds)
1925 class file_versioning(_pithos_init):
1926 """Manage the versioning scheme of current pithos user account"""
1929 @command(pithos_cmds)
1930 class file_versioning_get(_file_account_command, _optional_json):
1931 """Get versioning for account or container"""
1934 @errors.pithos.connection
1935 @errors.pithos.container
1938 self.client.get_container_versioning(self.container), print_dict)
1940 def main(self, container):
1941 super(self.__class__, self)._run()
1942 self.container = container
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"""
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'])
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)
1964 def main(self, versioning, container):
1965 super(self.__class__, self)._run()
1966 self._run(self._check_versioning(versioning))
1969 @command(pithos_cmds)
1970 class file_group(_pithos_init):
1971 """Manage access groups and group members"""
1974 @command(pithos_cmds)
1975 class file_group_list(_file_account_command, _optional_json):
1976 """list all groups and group members"""
1979 @errors.pithos.connection
1981 self._print(self.client.get_account_group(), print_dict, delim='-')
1984 super(self.__class__, self)._run()
1988 @command(pithos_cmds)
1989 class file_group_set(_file_account_command, _optional_output_cmd):
1990 """Set a user group"""
1993 @errors.pithos.connection
1994 def _run(self, groupname, *users):
1995 self._optional_output(self.client.set_account_group(groupname, users))
1997 def main(self, groupname, *users):
1998 super(self.__class__, self)._run()
2000 self._run(groupname, *users)
2002 raiseCLIError('No users to add in group %s' % groupname)
2005 @command(pithos_cmds)
2006 class file_group_delete(_file_account_command, _optional_output_cmd):
2007 """Delete a user group"""
2010 @errors.pithos.connection
2011 def _run(self, groupname):
2012 self._optional_output(self.client.del_account_group(groupname))
2014 def main(self, groupname):
2015 super(self.__class__, self)._run()
2016 self._run(groupname)
2019 @command(pithos_cmds)
2020 class file_sharers(_file_account_command, _optional_json):
2021 """List the accounts that share objects with current user"""
2024 detail=FlagArgument('show detailed output', ('-l', '--details')),
2025 marker=ValueArgument('show output greater then marker', '--marker')
2029 @errors.pithos.connection
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:
2037 item['id'], item['name'] = uuid, usernames[uuid]
2038 if not self['detail']:
2039 item.pop('last_modified')
2040 self._print(accounts)
2043 super(self.__class__, self)._run()
2047 def version_print(versions, out):
2049 [dict(id=vitem[0], created=strftime(
2050 '%d-%m-%Y %H:%M:%S',
2051 localtime(float(vitem[1])))) for vitem in versions],
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.
2066 @errors.pithos.connection
2067 @errors.pithos.container
2068 @errors.pithos.object_path
2071 self.client.get_object_versionlist(self.path), version_print)
2073 def main(self, container___path):
2074 super(file_versions, self)._run(
2076 path_is_optional=False)