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 sys import stdout
35 from time import localtime, strftime
36 from os import path, makedirs, walk
38 from kamaki.cli import command
39 from kamaki.cli.command_tree import CommandTree
40 from kamaki.cli.errors import raiseCLIError, CLISyntaxError, CLIBaseUrlError
41 from kamaki.cli.utils import (
42 format_size, to_bytes, print_dict, print_items, page_hold, bold, ask_user,
43 get_path_size, print_json, guess_mime_type)
44 from kamaki.cli.argument import FlagArgument, ValueArgument, IntArgument
45 from kamaki.cli.argument import KeyValueArgument, DateArgument
46 from kamaki.cli.argument import ProgressBarArgument
47 from kamaki.cli.commands import _command_init, errors
48 from kamaki.cli.commands import addLogSettings, DontRaiseKeyError
49 from kamaki.cli.commands import _optional_output_cmd, _optional_json
50 from kamaki.clients.pithos import PithosClient, ClientError
51 from kamaki.clients.astakos import AstakosClient
53 pithos_cmds = CommandTree('file', 'Pithos+/Storage API commands')
54 _commands = [pithos_cmds]
57 # Argument functionality
59 class DelimiterArgument(ValueArgument):
62 :value returns: given string or /
65 def __init__(self, caller_obj, help='', parsed_name=None, default=None):
66 super(DelimiterArgument, self).__init__(help, parsed_name, default)
67 self.caller_obj = caller_obj
71 if self.caller_obj['recursive']:
73 return getattr(self, '_value', self.default)
76 def value(self, newvalue):
77 self._value = newvalue
80 class SharingArgument(ValueArgument):
81 """Set sharing (read and/or write) groups
83 :value type: "read=term1,term2,... write=term1,term2,..."
85 :value returns: {'read':['term1', 'term2', ...],
86 . 'write':['term1', 'term2', ...]}
91 return getattr(self, '_value', self.default)
94 def value(self, newvalue):
97 permlist = newvalue.split(' ')
98 except AttributeError:
102 (key, val) = p.split('=')
103 except ValueError as err:
106 'Error in --sharing',
107 details='Incorrect format',
109 if key.lower() not in ('read', 'write'):
110 msg = 'Error in --sharing'
111 raiseCLIError(err, msg, importance=1, details=[
112 'Invalid permission key %s' % key])
113 val_list = val.split(',')
116 for item in val_list:
117 if item not in perms[key]:
118 perms[key].append(item)
122 class RangeArgument(ValueArgument):
124 :value type: string of the form <start>-<end> where <start> and <end> are
126 :value returns: the input string, after type checking <start> and <end>
131 return getattr(self, '_value', self.default)
134 def value(self, newvalues):
136 self._value = self.default
139 for newvalue in newvalues.split(','):
140 self._value = ('%s,' % self._value) if self._value else ''
141 start, sep, end = newvalue.partition('-')
144 start, end = (int(start), int(end))
145 assert start <= end, 'Invalid range value %s' % newvalue
146 self._value += '%s-%s' % (int(start), int(end))
148 self._value += '-%s' % int(end)
150 self._value += '%s' % int(start)
156 class _pithos_init(_command_init):
157 """Initialize a pithos+ kamaki client"""
160 def _is_dir(remote_dict):
161 return 'application/directory' == remote_dict.get(
162 'content_type', remote_dict.get('content-type', ''))
165 def _custom_container(self):
166 return self.config.get_cloud(self.cloud, 'pithos_container')
169 def _custom_uuid(self):
170 return self.config.get_cloud(self.cloud, 'pithos_uuid')
172 def _set_account(self):
173 self.account = self._custom_uuid()
176 if getattr(self, 'auth_base', False):
177 self.account = self.auth_base.user_term('id', self.token)
179 astakos_url = self._custom_url('astakos')
180 astakos_token = self._custom_token('astakos') or self.token
182 raise CLIBaseUrlError(service='astakos')
183 astakos = AstakosClient(astakos_url, astakos_token)
184 self.account = astakos.user_term('id')
190 if getattr(self, 'cloud', None):
191 self.base_url = self._custom_url('pithos')
193 self.cloud = 'default'
194 self.token = self._custom_token('pithos')
195 self.container = self._custom_container()
197 if getattr(self, 'auth_base', False):
198 self.token = self.token or self.auth_base.token
199 if not self.base_url:
200 pithos_endpoints = self.auth_base.get_service_endpoints(
201 self._custom_type('pithos') or 'object-store',
202 self._custom_version('pithos') or '')
203 self.base_url = pithos_endpoints['publicURL']
204 elif not self.base_url:
205 raise CLIBaseUrlError(service='pithos')
208 self.client = PithosClient(
209 base_url=self.base_url,
211 account=self.account,
212 container=self.container)
218 class _file_account_command(_pithos_init):
219 """Base class for account level storage commands"""
221 def __init__(self, arguments={}, auth_base=None, cloud=None):
222 super(_file_account_command, self).__init__(
223 arguments, auth_base, cloud)
224 self['account'] = ValueArgument(
225 'Set user account (not permanent)', ('-A', '--account'))
227 def _run(self, custom_account=None):
228 super(_file_account_command, self)._run()
230 self.client.account = custom_account
231 elif self['account']:
232 self.client.account = self['account']
239 class _file_container_command(_file_account_command):
240 """Base class for container level storage commands"""
245 def __init__(self, arguments={}, auth_base=None, cloud=None):
246 super(_file_container_command, self).__init__(
247 arguments, auth_base, cloud)
248 self['container'] = ValueArgument(
249 'Set container to work with (temporary)', ('-C', '--container'))
251 def extract_container_and_path(
254 path_is_optional=True):
255 """Contains all heuristics for deciding what should be used as
256 container or path. Options are:
257 * user string of the form container:path
258 * self.container, self.path variables set by super constructor, or
259 explicitly by the caller application
260 Error handling is explicit as these error cases happen only here
263 assert isinstance(container_with_path, str)
264 except AssertionError as err:
265 if self['container'] and path_is_optional:
266 self.container = self['container']
267 self.client.container = self['container']
271 user_cont, sep, userpath = container_with_path.partition(':')
275 raiseCLIError(CLISyntaxError(
276 'Container is missing\n',
277 details=errors.pithos.container_howto))
278 alt_cont = self['container']
279 if alt_cont and user_cont != alt_cont:
280 raiseCLIError(CLISyntaxError(
281 'Conflict: 2 containers (%s, %s)' % (user_cont, alt_cont),
282 details=errors.pithos.container_howto)
284 self.container = user_cont
286 raiseCLIError(CLISyntaxError(
287 'Path is missing for object in container %s' % user_cont,
288 details=errors.pithos.container_howto)
292 alt_cont = self['container'] or self.client.container
294 self.container = alt_cont
295 self.path = user_cont
296 elif path_is_optional:
297 self.container = user_cont
300 self.container = user_cont
301 raiseCLIError(CLISyntaxError(
302 'Both container and path are required',
303 details=errors.pithos.container_howto)
307 def _run(self, container_with_path=None, path_is_optional=True):
308 super(_file_container_command, self)._run()
309 if self['container']:
310 self.client.container = self['container']
311 if container_with_path:
312 self.path = container_with_path
313 elif not path_is_optional:
314 raise CLISyntaxError(
315 'Both container and path are required',
316 details=errors.pithos.container_howto)
317 elif container_with_path:
318 self.extract_container_and_path(
321 self.client.container = self.container
322 self.container = self.client.container
324 def main(self, container_with_path=None, path_is_optional=True):
325 self._run(container_with_path, path_is_optional)
328 @command(pithos_cmds)
329 class file_list(_file_container_command, _optional_json):
330 """List containers, object trees or objects in a directory
332 1 no parameters : containers in current account
333 2. one parameter (container) or --container : contents of container
334 3. <container>:<prefix> or --container=<container> <prefix>: objects in
335 . container starting with prefix
339 detail=FlagArgument('detailed output', ('-l', '--list')),
340 limit=IntArgument('limit number of listed items', ('-n', '--number')),
341 marker=ValueArgument('output greater that marker', '--marker'),
342 prefix=ValueArgument('output starting with prefix', '--prefix'),
343 delimiter=ValueArgument('show output up to delimiter', '--delimiter'),
345 'show output starting with prefix up to /', '--path'),
347 'show output with specified meta keys', '--meta',
349 if_modified_since=ValueArgument(
350 'show output modified since then', '--if-modified-since'),
351 if_unmodified_since=ValueArgument(
352 'show output not modified since then', '--if-unmodified-since'),
353 until=DateArgument('show metadata until then', '--until'),
354 format=ValueArgument(
355 'format to parse until data (default: d/m/Y H:M:S )', '--format'),
356 shared=FlagArgument('show only shared', '--shared'),
358 'output results in pages (-n to set items per page, default 10)',
360 exact_match=FlagArgument(
361 'Show only objects that match exactly with path',
363 enum=FlagArgument('Enumerate results', '--enumerate')
366 def print_objects(self, object_list):
367 if self['json_output']:
368 print_json(object_list)
370 limit = int(self['limit']) if self['limit'] > 0 else len(object_list)
371 for index, obj in enumerate(object_list):
372 if self['exact_match'] and self.path and not (
373 obj['name'] == self.path or 'content_type' in obj):
375 pretty_obj = obj.copy()
377 empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
380 if obj['content_type'] == 'application/directory':
385 size = format_size(obj['bytes'])
386 pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
387 oname = bold(obj['name'])
388 prfx = ('%s%s. ' % (empty_space, index)) if self['enum'] else ''
390 print('%s%s' % (prfx, oname))
391 print_dict(pretty_obj, exclude=('name'))
394 oname = '%s%9s %s' % (prfx, size, oname)
395 oname += '/' if isDir else ''
398 page_hold(index, limit, len(object_list))
400 def print_containers(self, container_list):
401 if self['json_output']:
402 print_json(container_list)
404 limit = int(self['limit']) if self['limit'] > 0\
405 else len(container_list)
406 for index, container in enumerate(container_list):
407 if 'bytes' in container:
408 size = format_size(container['bytes'])
409 prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
410 cname = '%s%s' % (prfx, bold(container['name']))
413 pretty_c = container.copy()
414 if 'bytes' in container:
415 pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
416 print_dict(pretty_c, exclude=('name'))
419 if 'count' in container and 'bytes' in container:
420 print('%s (%s, %s objects)' % (
427 page_hold(index + 1, limit, len(container_list))
430 @errors.pithos.connection
431 @errors.pithos.object_path
432 @errors.pithos.container
434 if self.container is None:
435 r = self.client.account_get(
436 limit=False if self['more'] else self['limit'],
437 marker=self['marker'],
438 if_modified_since=self['if_modified_since'],
439 if_unmodified_since=self['if_unmodified_since'],
441 show_only_shared=self['shared'])
442 self._print(r.json, self.print_containers)
444 prefix = self.path or self['prefix']
445 r = self.client.container_get(
446 limit=False if self['more'] else self['limit'],
447 marker=self['marker'],
449 delimiter=self['delimiter'],
451 if_modified_since=self['if_modified_since'],
452 if_unmodified_since=self['if_unmodified_since'],
455 show_only_shared=self['shared'])
456 self._print(r.json, self.print_objects)
458 def main(self, container____path__=None):
459 super(self.__class__, self)._run(container____path__)
463 @command(pithos_cmds)
464 class file_mkdir(_file_container_command, _optional_output_cmd):
465 """Create a directory
466 Kamaki hanldes directories the same way as OOS Storage and Pithos+:
467 A directory is an object with type "application/directory"
468 An object with path dir/name can exist even if dir does not exist
469 or even if dir is a non directory object. Users can modify dir '
470 without affecting the dir/name object in any way.
474 @errors.pithos.connection
475 @errors.pithos.container
477 self._optional_output(self.client.create_directory(self.path))
479 def main(self, container___directory):
480 super(self.__class__, self)._run(
481 container___directory,
482 path_is_optional=False)
486 @command(pithos_cmds)
487 class file_touch(_file_container_command, _optional_output_cmd):
488 """Create an empty object (file)
489 If object exists, this command will reset it to 0 length
493 content_type=ValueArgument(
494 'Set content type (default: application/octet-stream)',
496 default='application/octet-stream')
500 @errors.pithos.connection
501 @errors.pithos.container
503 self._optional_output(
504 self.client.create_object(self.path, self['content_type']))
506 def main(self, container___path):
507 super(file_touch, self)._run(
509 path_is_optional=False)
513 @command(pithos_cmds)
514 class file_create(_file_container_command, _optional_output_cmd):
515 """Create a container"""
518 versioning=ValueArgument(
519 'set container versioning (auto/none)', '--versioning'),
520 limit=IntArgument('set default container limit', '--limit'),
521 meta=KeyValueArgument(
522 'set container metadata (can be repeated)', '--meta')
526 @errors.pithos.connection
527 @errors.pithos.container
528 def _run(self, container):
529 self._optional_output(self.client.create_container(
531 sizelimit=self['limit'],
532 versioning=self['versioning'],
533 metadata=self['meta']))
535 def main(self, container=None):
536 super(self.__class__, self)._run(container)
537 if container and self.container != container:
538 raiseCLIError('Invalid container name %s' % container, details=[
539 'Did you mean "%s" ?' % self.container,
540 'Use --container for names containing :'])
544 class _source_destination_command(_file_container_command):
547 destination_account=ValueArgument('', ('-a', '--dst-account')),
548 recursive=FlagArgument('', ('-R', '--recursive')),
549 prefix=FlagArgument('', '--with-prefix', default=''),
550 suffix=ValueArgument('', '--with-suffix', default=''),
551 add_prefix=ValueArgument('', '--add-prefix', default=''),
552 add_suffix=ValueArgument('', '--add-suffix', default=''),
553 prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
554 suffix_replace=ValueArgument('', '--suffix-to-replace', default=''),
557 def __init__(self, arguments={}, auth_base=None, cloud=None):
558 self.arguments.update(arguments)
559 super(_source_destination_command, self).__init__(
560 self.arguments, auth_base, cloud)
562 def _run(self, source_container___path, path_is_optional=False):
563 super(_source_destination_command, self)._run(
564 source_container___path,
566 self.dst_client = PithosClient(
567 base_url=self.client.base_url,
568 token=self.client.token,
569 account=self['destination_account'] or self.client.account)
572 @errors.pithos.account
573 def _dest_container_path(self, dest_container_path):
574 if self['destination_container']:
575 self.dst_client.container = self['destination_container']
576 return (self['destination_container'], dest_container_path)
577 if dest_container_path:
578 dst = dest_container_path.split(':')
581 self.dst_client.container = dst[0]
582 self.dst_client.get_container_info(dst[0])
583 except ClientError as err:
584 if err.status in (404, 204):
586 'Destination container %s not found' % dst[0])
589 self.dst_client.container = dst[0]
590 return (dst[0], dst[1])
592 raiseCLIError('No destination container:path provided')
594 def _get_all(self, prefix):
595 return self.client.container_get(prefix=prefix).json
597 def _get_src_objects(self, src_path, source_version=None):
598 """Get a list of the source objects to be called
600 :param src_path: (str) source path
602 :returns: (method, params) a method that returns a list when called
603 or (object) if it is a single object
605 if src_path and src_path[-1] == '/':
606 src_path = src_path[:-1]
609 return (self._get_all, dict(prefix=src_path))
611 srcobj = self.client.get_object_info(
612 src_path, version=source_version)
613 except ClientError as srcerr:
614 if srcerr.status == 404:
616 'Source object %s not in source container %s' % (
617 src_path, self.client.container),
618 details=['Hint: --with-prefix to match multiple objects'])
619 elif srcerr.status not in (204,):
621 return (self.client.list_objects, {})
623 if self._is_dir(srcobj):
624 if not self['recursive']:
626 'Object %s of cont. %s is a dir' % (
627 src_path, self.client.container),
628 details=['Use --recursive to access directories'])
629 return (self._get_all, dict(prefix=src_path))
630 srcobj['name'] = src_path
633 def src_dst_pairs(self, dst_path, source_version=None):
634 src_iter = self._get_src_objects(self.path, source_version)
635 src_N = isinstance(src_iter, tuple)
636 add_prefix = self['add_prefix'].strip('/')
638 if dst_path and dst_path.endswith('/'):
639 dst_path = dst_path[:-1]
642 dstobj = self.dst_client.get_object_info(dst_path)
643 except ClientError as trgerr:
644 if trgerr.status in (404,):
647 'Cannot merge multiple paths to path %s' % dst_path,
649 'Try to use / or a directory as destination',
650 'or create the destination dir (/file mkdir)',
651 'or use a single object as source'])
652 elif trgerr.status not in (204,):
655 if self._is_dir(dstobj):
656 add_prefix = '%s/%s' % (dst_path.strip('/'), add_prefix)
659 'Cannot merge multiple paths to path' % dst_path,
661 'Try to use / or a directory as destination',
662 'or create the destination dir (/file mkdir)',
663 'or use a single object as source'])
666 (method, kwargs) = src_iter
667 for obj in method(**kwargs):
669 if name.endswith(self['suffix']):
670 yield (name, self._get_new_object(name, add_prefix))
671 elif src_iter['name'].endswith(self['suffix']):
672 name = src_iter['name']
673 yield (name, self._get_new_object(dst_path or name, add_prefix))
675 raiseCLIError('Source path %s conflicts with suffix %s' % (
676 src_iter['name'], self['suffix']))
678 def _get_new_object(self, obj, add_prefix):
679 if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
680 obj = obj[len(self['prefix_replace']):]
681 if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
682 obj = obj[:-len(self['suffix_replace'])]
683 return add_prefix + obj + self['add_suffix']
686 @command(pithos_cmds)
687 class file_copy(_source_destination_command, _optional_output_cmd):
688 """Copy objects from container to (another) container
691 . transfer path as dir/path
692 copy cont:path cont2:
693 . trasnfer all <obj> prefixed with path to container cont2
694 copy cont:path [cont2:]path2
695 . transfer path to path2
697 1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
698 destination is container1:path2
699 2. <container>:<path1> <path2> : make a copy in the same container
700 3. Can use --container= instead of <container1>
704 destination_account=ValueArgument(
705 'Account to copy to', ('-a', '--dst-account')),
706 destination_container=ValueArgument(
707 'use it if destination container name contains a : character',
708 ('-D', '--dst-container')),
709 public=ValueArgument('make object publicly accessible', '--public'),
710 content_type=ValueArgument(
711 'change object\'s content type', '--content-type'),
712 recursive=FlagArgument(
713 'copy directory and contents', ('-R', '--recursive')),
715 'Match objects prefixed with src path (feels like src_path*)',
718 suffix=ValueArgument(
719 'Suffix of source objects (feels like *suffix)', '--with-suffix',
721 add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
722 add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
723 prefix_replace=ValueArgument(
724 'Prefix of src to replace with dst path + add_prefix, if matched',
725 '--prefix-to-replace',
727 suffix_replace=ValueArgument(
728 'Suffix of src to replace with add_suffix, if matched',
729 '--suffix-to-replace',
731 source_version=ValueArgument(
732 'copy specific version', ('-S', '--source-version'))
736 @errors.pithos.connection
737 @errors.pithos.container
738 @errors.pithos.account
739 def _run(self, dst_path):
740 no_source_object = True
741 src_account = self.client.account if (
742 self['destination_account']) else None
743 for src_obj, dst_obj in self.src_dst_pairs(
744 dst_path, self['source_version']):
745 no_source_object = False
746 r = self.dst_client.copy_object(
747 src_container=self.client.container,
749 dst_container=self.dst_client.container,
751 source_account=src_account,
752 source_version=self['source_version'],
753 public=self['public'],
754 content_type=self['content_type'])
756 raiseCLIError('No object %s in container %s' % (
757 self.path, self.container))
758 self._optional_output(r)
761 self, source_container___path,
762 destination_container___path=None):
763 super(file_copy, self)._run(
764 source_container___path,
765 path_is_optional=False)
766 (dst_cont, dst_path) = self._dest_container_path(
767 destination_container___path)
768 self.dst_client.container = dst_cont or self.container
769 self._run(dst_path=dst_path or '')
772 @command(pithos_cmds)
773 class file_move(_source_destination_command, _optional_output_cmd):
774 """Move/rename objects from container to (another) container
777 . rename path as dir/path
778 move cont:path cont2:
779 . trasnfer all <obj> prefixed with path to container cont2
780 move cont:path [cont2:]path2
781 . transfer path to path2
783 1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
784 destination is container1:path2
785 2. <container>:<path1> <path2> : move in the same container
786 3. Can use --container= instead of <container1>
790 destination_account=ValueArgument(
791 'Account to move to', ('-a', '--dst-account')),
792 destination_container=ValueArgument(
793 'use it if destination container name contains a : character',
794 ('-D', '--dst-container')),
795 public=ValueArgument('make object publicly accessible', '--public'),
796 content_type=ValueArgument(
797 'change object\'s content type', '--content-type'),
798 recursive=FlagArgument(
799 'copy directory and contents', ('-R', '--recursive')),
801 'Match objects prefixed with src path (feels like src_path*)',
804 suffix=ValueArgument(
805 'Suffix of source objects (feels like *suffix)', '--with-suffix',
807 add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
808 add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
809 prefix_replace=ValueArgument(
810 'Prefix of src to replace with dst path + add_prefix, if matched',
811 '--prefix-to-replace',
813 suffix_replace=ValueArgument(
814 'Suffix of src to replace with add_suffix, if matched',
815 '--suffix-to-replace',
820 @errors.pithos.connection
821 @errors.pithos.container
822 def _run(self, dst_path):
823 no_source_object = True
824 src_account = self.client.account if (
825 self['destination_account']) else None
826 for src_obj, dst_obj in self.src_dst_pairs(dst_path):
827 no_source_object = False
828 r = self.dst_client.move_object(
829 src_container=self.container,
831 dst_container=self.dst_client.container,
833 source_account=src_account,
834 public=self['public'],
835 content_type=self['content_type'])
837 raiseCLIError('No object %s in container %s' % (
840 self._optional_output(r)
843 self, source_container___path,
844 destination_container___path=None):
845 super(self.__class__, self)._run(
846 source_container___path,
847 path_is_optional=False)
848 (dst_cont, dst_path) = self._dest_container_path(
849 destination_container___path)
850 (dst_cont, dst_path) = self._dest_container_path(
851 destination_container___path)
852 self.dst_client.container = dst_cont or self.container
853 self._run(dst_path=dst_path or '')
856 @command(pithos_cmds)
857 class file_append(_file_container_command, _optional_output_cmd):
858 """Append local file to (existing) remote object
859 The remote object should exist.
860 If the remote object is a directory, it is transformed into a file.
861 In the later case, objects under the directory remain intact.
865 progress_bar=ProgressBarArgument(
866 'do not show progress bar',
867 ('-N', '--no-progress-bar'),
872 @errors.pithos.connection
873 @errors.pithos.container
874 @errors.pithos.object_path
875 def _run(self, local_path):
876 (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
878 f = open(local_path, 'rb')
879 self._optional_output(
880 self.client.append_object(self.path, f, upload_cb))
882 self._safe_progress_bar_finish(progress_bar)
885 self._safe_progress_bar_finish(progress_bar)
887 def main(self, local_path, container___path):
888 super(self.__class__, self)._run(
889 container___path, path_is_optional=False)
890 self._run(local_path)
893 @command(pithos_cmds)
894 class file_truncate(_file_container_command, _optional_output_cmd):
895 """Truncate remote file up to a size (default is 0)"""
898 @errors.pithos.connection
899 @errors.pithos.container
900 @errors.pithos.object_path
901 @errors.pithos.object_size
902 def _run(self, size=0):
903 self._optional_output(self.client.truncate_object(self.path, size))
905 def main(self, container___path, size=0):
906 super(self.__class__, self)._run(container___path)
910 @command(pithos_cmds)
911 class file_overwrite(_file_container_command, _optional_output_cmd):
912 """Overwrite part (from start to end) of a remote file
913 overwrite local-path container 10 20
914 . will overwrite bytes from 10 to 20 of a remote file with the same name
915 . as local-path basename
916 overwrite local-path container:path 10 20
917 . will overwrite as above, but the remote file is named path
921 progress_bar=ProgressBarArgument(
922 'do not show progress bar',
923 ('-N', '--no-progress-bar'),
927 def _open_file(self, local_path, start):
928 f = open(path.abspath(local_path), 'rb')
935 @errors.pithos.connection
936 @errors.pithos.container
937 @errors.pithos.object_path
938 @errors.pithos.object_size
939 def _run(self, local_path, start, end):
940 (start, end) = (int(start), int(end))
941 (f, f_size) = self._open_file(local_path, start)
942 (progress_bar, upload_cb) = self._safe_progress_bar(
943 'Overwrite %s bytes' % (end - start))
945 self._optional_output(self.client.overwrite_object(
950 upload_cb=upload_cb))
952 self._safe_progress_bar_finish(progress_bar)
954 def main(self, local_path, container___path, start, end):
955 super(self.__class__, self)._run(
956 container___path, path_is_optional=None)
957 self.path = self.path or path.basename(local_path)
958 self._run(local_path=local_path, start=start, end=end)
961 @command(pithos_cmds)
962 class file_manifest(_file_container_command, _optional_output_cmd):
963 """Create a remote file of uploaded parts by manifestation
964 Remains functional for compatibility with OOS Storage. Users are advised
965 to use the upload command instead.
966 Manifestation is a compliant process for uploading large files. The files
967 have to be chunked in smalled files and uploaded as <prefix><increment>
968 where increment is 1, 2, ...
969 Finally, the manifest command glues partial files together in one file
971 The upload command is faster, easier and more intuitive than manifest
975 etag=ValueArgument('check written data', '--etag'),
976 content_encoding=ValueArgument(
977 'set MIME content type', '--content-encoding'),
978 content_disposition=ValueArgument(
979 'the presentation style of the object', '--content-disposition'),
980 content_type=ValueArgument(
981 'specify content type', '--content-type',
982 default='application/octet-stream'),
983 sharing=SharingArgument(
985 'define object sharing policy',
986 ' ( "read=user1,grp1,user2,... write=user1,grp2,..." )']),
988 public=FlagArgument('make object publicly accessible', '--public')
992 @errors.pithos.connection
993 @errors.pithos.container
994 @errors.pithos.object_path
996 ctype, cenc = guess_mime_type(self.path)
997 self._optional_output(self.client.create_object_by_manifestation(
999 content_encoding=self['content_encoding'] or cenc,
1000 content_disposition=self['content_disposition'],
1001 content_type=self['content_type'] or ctype,
1002 sharing=self['sharing'],
1003 public=self['public']))
1005 def main(self, container___path):
1006 super(self.__class__, self)._run(
1007 container___path, path_is_optional=False)
1011 @command(pithos_cmds)
1012 class file_upload(_file_container_command, _optional_output_cmd):
1016 use_hashes=FlagArgument(
1017 'provide hashmap file instead of data', '--use-hashes'),
1018 etag=ValueArgument('check written data', '--etag'),
1019 unchunked=FlagArgument('avoid chunked transfer mode', '--unchunked'),
1020 content_encoding=ValueArgument(
1021 'set MIME content type', '--content-encoding'),
1022 content_disposition=ValueArgument(
1023 'specify objects presentation style', '--content-disposition'),
1024 content_type=ValueArgument('specify content type', '--content-type'),
1025 sharing=SharingArgument(
1027 'define sharing object policy',
1028 '( "read=user1,grp1,user2,... write=user1,grp2,... )']),
1029 parsed_name='--sharing'),
1030 public=FlagArgument('make object publicly accessible', '--public'),
1031 poolsize=IntArgument('set pool size', '--with-pool-size'),
1032 progress_bar=ProgressBarArgument(
1033 'do not show progress bar',
1034 ('-N', '--no-progress-bar'),
1036 overwrite=FlagArgument('Force (over)write', ('-f', '--force')),
1037 recursive=FlagArgument(
1038 'Recursively upload directory *contents* + subdirectories',
1039 ('-R', '--recursive'))
1042 def _check_container_limit(self, path):
1043 cl_dict = self.client.get_container_limit()
1044 container_limit = int(cl_dict['x-container-policy-quota'])
1045 r = self.client.container_get()
1046 used_bytes = sum(int(o['bytes']) for o in r.json)
1047 path_size = get_path_size(path)
1048 if container_limit and path_size > (container_limit - used_bytes):
1050 'Container(%s) (limit(%s) - used(%s)) < size(%s) of %s' % (
1051 self.client.container,
1052 format_size(container_limit),
1053 format_size(used_bytes),
1054 format_size(path_size),
1056 importance=1, details=[
1057 'Check accound limit: /file quota',
1058 'Check container limit:',
1059 '\t/file containerlimit get %s' % self.client.container,
1060 'Increase container limit:',
1061 '\t/file containerlimit set <new limit> %s' % (
1062 self.client.container)])
1064 def _path_pairs(self, local_path, remote_path):
1065 """Get pairs of local and remote paths"""
1066 lpath = path.abspath(local_path)
1067 short_path = lpath.split(path.sep)[-1]
1068 rpath = remote_path or short_path
1069 if path.isdir(lpath):
1070 if not self['recursive']:
1071 raiseCLIError('%s is a directory' % lpath, details=[
1072 'Use -R to upload directory contents'])
1073 robj = self.client.container_get(path=rpath)
1074 if robj.json and not self['overwrite']:
1076 'Objects prefixed with %s already exist' % rpath,
1078 details=['Existing objects:'] + ['\t%s:\t%s' % (
1079 o['content_type'][12:],
1080 o['name']) for o in robj.json] + [
1081 'Use -f to add, overwrite or resume'])
1082 if not self['overwrite']:
1084 topobj = self.client.get_object_info(rpath)
1085 if not self._is_dir(topobj):
1087 'Object %s exists but it is not a dir' % rpath,
1088 importance=1, details=['Use -f to overwrite'])
1089 except ClientError as ce:
1090 if ce.status != 404:
1092 self._check_container_limit(lpath)
1094 for top, subdirs, files in walk(lpath):
1098 rel_path = rpath + top.split(lpath)[1]
1101 print('mkdir %s:%s' % (self.client.container, rel_path))
1102 self.client.create_directory(rel_path)
1104 fpath = path.join(top, f)
1105 if path.isfile(fpath):
1106 rel_path = rel_path.replace(path.sep, '/')
1107 pathfix = f.replace(path.sep, '/')
1108 yield open(fpath, 'rb'), '%s/%s' % (rel_path, pathfix)
1110 print('%s is not a regular file' % fpath)
1112 if not path.isfile(lpath):
1113 raiseCLIError(('%s is not a regular file' % lpath) if (
1114 path.exists(lpath)) else '%s does not exist' % lpath)
1116 robj = self.client.get_object_info(rpath)
1117 if remote_path and self._is_dir(robj):
1118 rpath += '/%s' % (short_path.replace(path.sep, '/'))
1119 self.client.get_object_info(rpath)
1120 if not self['overwrite']:
1122 'Object %s already exists' % rpath,
1124 details=['use -f to overwrite or resume'])
1125 except ClientError as ce:
1126 if ce.status != 404:
1128 self._check_container_limit(lpath)
1129 yield open(lpath, 'rb'), rpath
1132 @errors.pithos.connection
1133 @errors.pithos.container
1134 @errors.pithos.object_path
1135 @errors.pithos.local_path
1136 def _run(self, local_path, remote_path):
1137 poolsize = self['poolsize']
1139 self.client.MAX_THREADS = int(poolsize)
1141 content_encoding=self['content_encoding'],
1142 content_type=self['content_type'],
1143 content_disposition=self['content_disposition'],
1144 sharing=self['sharing'],
1145 public=self['public'])
1147 container_info_cache = dict()
1148 for f, rpath in self._path_pairs(local_path, remote_path):
1149 print('%s --> %s:%s' % (f.name, self.client.container, rpath))
1150 if not (self['content_type'] and self['content_encoding']):
1151 ctype, cenc = guess_mime_type(f.name)
1152 params['content_type'] = self['content_type'] or ctype
1153 params['content_encoding'] = self['content_encoding'] or cenc
1154 if self['unchunked']:
1155 r = self.client.upload_object_unchunked(
1157 etag=self['etag'], withHashFile=self['use_hashes'],
1159 if self['with_output'] or self['json_output']:
1160 r['name'] = '%s: %s' % (self.client.container, rpath)
1164 (progress_bar, upload_cb) = self._safe_progress_bar(
1165 'Uploading %s' % f.name.split(path.sep)[-1])
1167 hash_bar = progress_bar.clone()
1168 hash_cb = hash_bar.get_generator(
1169 'Calculating block hashes')
1172 r = self.client.upload_object(
1175 upload_cb=upload_cb,
1176 container_info_cache=container_info_cache,
1178 if self['with_output'] or self['json_output']:
1179 r['name'] = '%s: %s' % (self.client.container, rpath)
1182 self._safe_progress_bar_finish(progress_bar)
1185 self._safe_progress_bar_finish(progress_bar)
1186 self._optional_output(uploaded)
1187 print('Upload completed')
1189 def main(self, local_path, container____path__=None):
1190 super(self.__class__, self)._run(container____path__)
1191 remote_path = self.path or path.basename(path.abspath(local_path))
1192 self._run(local_path=local_path, remote_path=remote_path)
1195 @command(pithos_cmds)
1196 class file_cat(_file_container_command):
1197 """Print remote file contents to console"""
1200 range=RangeArgument('show range of data', '--range'),
1201 if_match=ValueArgument('show output if ETags match', '--if-match'),
1202 if_none_match=ValueArgument(
1203 'show output if ETags match', '--if-none-match'),
1204 if_modified_since=DateArgument(
1205 'show output modified since then', '--if-modified-since'),
1206 if_unmodified_since=DateArgument(
1207 'show output unmodified since then', '--if-unmodified-since'),
1208 object_version=ValueArgument(
1209 'get the specific version', ('-O', '--object-version'))
1213 @errors.pithos.connection
1214 @errors.pithos.container
1215 @errors.pithos.object_path
1217 self.client.download_object(
1220 range_str=self['range'],
1221 version=self['object_version'],
1222 if_match=self['if_match'],
1223 if_none_match=self['if_none_match'],
1224 if_modified_since=self['if_modified_since'],
1225 if_unmodified_since=self['if_unmodified_since'])
1227 def main(self, container___path):
1228 super(self.__class__, self)._run(
1229 container___path, path_is_optional=False)
1233 @command(pithos_cmds)
1234 class file_download(_file_container_command):
1235 """Download remote object as local file
1236 If local destination is a directory:
1237 * download <container>:<path> <local dir> -R
1238 will download all files on <container> prefixed as <path>,
1239 to <local dir>/<full path> (or <local dir>\<full path> in windows)
1240 * download <container>:<path> <local dir> --exact-match
1241 will download only one file, exactly matching <path>
1242 ATTENTION: to download cont:dir1/dir2/file there must exist objects
1243 cont:dir1 and cont:dir1/dir2 of type application/directory
1244 To create directory objects, use /file mkdir
1248 resume=FlagArgument('Resume instead of overwrite', ('-r', '--resume')),
1249 range=RangeArgument('show range of data', '--range'),
1250 if_match=ValueArgument('show output if ETags match', '--if-match'),
1251 if_none_match=ValueArgument(
1252 'show output if ETags match', '--if-none-match'),
1253 if_modified_since=DateArgument(
1254 'show output modified since then', '--if-modified-since'),
1255 if_unmodified_since=DateArgument(
1256 'show output unmodified since then', '--if-unmodified-since'),
1257 object_version=ValueArgument(
1258 'get the specific version', ('-O', '--object-version')),
1259 poolsize=IntArgument('set pool size', '--with-pool-size'),
1260 progress_bar=ProgressBarArgument(
1261 'do not show progress bar',
1262 ('-N', '--no-progress-bar'),
1264 recursive=FlagArgument(
1265 'Download a remote path and all its contents',
1266 ('-R', '--recursive'))
1269 def _outputs(self, local_path):
1270 """:returns: (local_file, remote_path)"""
1272 if self['recursive']:
1273 r = self.client.container_get(
1274 prefix=self.path or '/',
1275 if_modified_since=self['if_modified_since'],
1276 if_unmodified_since=self['if_unmodified_since'])
1278 for remote in r.json:
1279 rname = remote['name'].strip('/')
1281 for newdir in rname.strip('/').split('/')[:-1]:
1282 tmppath = '/'.join([tmppath, newdir])
1283 dirlist.update({tmppath.strip('/'): True})
1284 remotes.append((rname, file_download._is_dir(remote)))
1285 dir_remotes = [r[0] for r in remotes if r[1]]
1286 if not set(dirlist).issubset(dir_remotes):
1287 badguys = [bg.strip('/') for bg in set(
1288 dirlist).difference(dir_remotes)]
1290 'Some remote paths contain non existing directories',
1291 details=['Missing remote directories:'] + badguys)
1293 r = self.client.get_object_info(
1295 version=self['object_version'])
1296 if file_download._is_dir(r):
1298 'Illegal download: Remote object %s is a directory' % (
1300 details=['To download a directory, try --recursive or -R'])
1301 if '/' in self.path.strip('/') and not local_path:
1303 'Illegal download: remote object %s contains "/"' % (
1306 'To download an object containing "/" characters',
1307 'either create the remote directories or',
1308 'specify a non-directory local path for this object'])
1309 remotes = [(self.path, False)]
1313 'No matching path %s on container %s' % (
1314 self.path, self.container),
1316 'To list the contents of %s, try:' % self.container,
1317 ' /file list %s' % self.container])
1319 'Illegal download of container %s' % self.container,
1321 'To download a whole container, try:',
1322 ' /file download --recursive <container>'])
1324 lprefix = path.abspath(local_path or path.curdir)
1325 if path.isdir(lprefix):
1326 for rpath, remote_is_dir in remotes:
1327 lpath = path.sep.join([
1328 lprefix[:-1] if lprefix.endswith(path.sep) else lprefix,
1329 rpath.strip('/').replace('/', path.sep)])
1331 if path.exists(lpath) and path.isdir(lpath):
1334 elif path.exists(lpath):
1335 if not self['resume']:
1336 print('File %s exists, aborting...' % lpath)
1338 with open(lpath, 'rwb+') as f:
1341 with open(lpath, 'wb+') as f:
1343 elif path.exists(lprefix):
1344 if len(remotes) > 1:
1346 '%s remote objects cannot be merged in local file %s' % (
1350 'To download multiple objects, local path should be',
1351 'a directory, or use download without a local path'])
1352 (rpath, remote_is_dir) = remotes[0]
1355 'Remote directory %s should not replace local file %s' % (
1359 with open(lprefix, 'rwb+') as f:
1363 'Local file %s already exist' % local_path,
1364 details=['Try --resume to overwrite it'])
1366 if len(remotes) > 1 or remotes[0][1]:
1368 'Local directory %s does not exist' % local_path)
1369 with open(lprefix, 'wb+') as f:
1370 yield (f, remotes[0][0])
1373 @errors.pithos.connection
1374 @errors.pithos.container
1375 @errors.pithos.object_path
1376 @errors.pithos.local_path
1377 def _run(self, local_path):
1378 poolsize = self['poolsize']
1380 self.client.MAX_THREADS = int(poolsize)
1383 for f, rpath in self._outputs(local_path):
1386 download_cb) = self._safe_progress_bar(
1387 'Download %s' % rpath)
1388 self.client.download_object(
1390 download_cb=download_cb,
1391 range_str=self['range'],
1392 version=self['object_version'],
1393 if_match=self['if_match'],
1394 resume=self['resume'],
1395 if_none_match=self['if_none_match'],
1396 if_modified_since=self['if_modified_since'],
1397 if_unmodified_since=self['if_unmodified_since'])
1398 except KeyboardInterrupt:
1399 from threading import activeCount, enumerate as activethreads
1401 while activeCount() > 1:
1402 stdout.write('\nCancel %s threads: ' % (activeCount() - 1))
1404 for thread in activethreads():
1406 thread.join(timeout)
1407 stdout.write('.' if thread.isAlive() else '*')
1408 except RuntimeError:
1413 print('\nDownload canceled by user')
1414 if local_path is not None:
1415 print('to resume, re-run with --resume')
1417 self._safe_progress_bar_finish(progress_bar)
1420 self._safe_progress_bar_finish(progress_bar)
1422 def main(self, container___path, local_path=None):
1423 super(self.__class__, self)._run(container___path)
1424 self._run(local_path=local_path)
1427 @command(pithos_cmds)
1428 class file_hashmap(_file_container_command, _optional_json):
1429 """Get the hash-map of an object"""
1432 if_match=ValueArgument('show output if ETags match', '--if-match'),
1433 if_none_match=ValueArgument(
1434 'show output if ETags match', '--if-none-match'),
1435 if_modified_since=DateArgument(
1436 'show output modified since then', '--if-modified-since'),
1437 if_unmodified_since=DateArgument(
1438 'show output unmodified since then', '--if-unmodified-since'),
1439 object_version=ValueArgument(
1440 'get the specific version', ('-O', '--object-version'))
1444 @errors.pithos.connection
1445 @errors.pithos.container
1446 @errors.pithos.object_path
1448 self._print(self.client.get_object_hashmap(
1450 version=self['object_version'],
1451 if_match=self['if_match'],
1452 if_none_match=self['if_none_match'],
1453 if_modified_since=self['if_modified_since'],
1454 if_unmodified_since=self['if_unmodified_since']), print_dict)
1456 def main(self, container___path):
1457 super(self.__class__, self)._run(
1459 path_is_optional=False)
1463 @command(pithos_cmds)
1464 class file_delete(_file_container_command, _optional_output_cmd):
1465 """Delete a container [or an object]
1466 How to delete a non-empty container:
1467 - empty the container: /file delete -R <container>
1468 - delete it: /file delete <container>
1470 Semantics of directory deletion:
1471 .a preserve the contents: /file delete <container>:<directory>
1472 . objects of the form dir/filename can exist with a dir object
1473 .b delete contents: /file delete -R <container>:<directory>
1474 . all dir/* objects are affected, even if dir does not exist
1476 To restore a deleted object OBJ in a container CONT:
1477 - get object versions: /file versions CONT:OBJ
1478 . and choose the version to be restored
1479 - restore the object: /file copy --source-version=<version> CONT:OBJ OBJ
1483 until=DateArgument('remove history until that date', '--until'),
1484 yes=FlagArgument('Do not prompt for permission', '--yes'),
1485 recursive=FlagArgument(
1486 'empty dir or container and delete (if dir)',
1487 ('-R', '--recursive'))
1490 def __init__(self, arguments={}, auth_base=None, cloud=None):
1491 super(self.__class__, self).__init__(arguments, auth_base, cloud)
1492 self['delimiter'] = DelimiterArgument(
1494 parsed_name='--delimiter',
1495 help='delete objects prefixed with <object><delimiter>')
1498 @errors.pithos.connection
1499 @errors.pithos.container
1500 @errors.pithos.object_path
1503 if self['yes'] or ask_user(
1504 'Delete %s:%s ?' % (self.container, self.path)):
1505 self._optional_output(self.client.del_object(
1507 until=self['until'], delimiter=self['delimiter']))
1511 if self['recursive']:
1512 ask_msg = 'Delete container contents'
1514 ask_msg = 'Delete container'
1515 if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1516 self._optional_output(self.client.del_container(
1517 until=self['until'], delimiter=self['delimiter']))
1521 def main(self, container____path__=None):
1522 super(self.__class__, self)._run(container____path__)
1526 @command(pithos_cmds)
1527 class file_purge(_file_container_command, _optional_output_cmd):
1528 """Delete a container and release related data blocks
1529 Non-empty containers can not purged.
1530 To purge a container with content:
1531 . /file delete -R <container>
1532 . objects are deleted, but data blocks remain on server
1533 . /file purge <container>
1534 . container and data blocks are released and deleted
1538 yes=FlagArgument('Do not prompt for permission', '--yes'),
1539 force=FlagArgument('purge even if not empty', ('-F', '--force'))
1543 @errors.pithos.connection
1544 @errors.pithos.container
1546 if self['yes'] or ask_user('Purge container %s?' % self.container):
1548 r = self.client.purge_container()
1549 except ClientError as ce:
1550 if ce.status in (409,):
1552 self.client.del_container(delimiter='/')
1553 r = self.client.purge_container()
1555 raiseCLIError(ce, details=['Try -F to force-purge'])
1558 self._optional_output(r)
1562 def main(self, container=None):
1563 super(self.__class__, self)._run(container)
1564 if container and self.container != container:
1565 raiseCLIError('Invalid container name %s' % container, details=[
1566 'Did you mean "%s" ?' % self.container,
1567 'Use --container for names containing :'])
1571 @command(pithos_cmds)
1572 class file_publish(_file_container_command):
1573 """Publish the object and print the public url"""
1576 @errors.pithos.connection
1577 @errors.pithos.container
1578 @errors.pithos.object_path
1580 print self.client.publish_object(self.path)
1582 def main(self, container___path):
1583 super(self.__class__, self)._run(
1584 container___path, path_is_optional=False)
1588 @command(pithos_cmds)
1589 class file_unpublish(_file_container_command, _optional_output_cmd):
1590 """Unpublish an object"""
1593 @errors.pithos.connection
1594 @errors.pithos.container
1595 @errors.pithos.object_path
1597 self._optional_output(self.client.unpublish_object(self.path))
1599 def main(self, container___path):
1600 super(self.__class__, self)._run(
1601 container___path, path_is_optional=False)
1605 @command(pithos_cmds)
1606 class file_permissions(_pithos_init):
1607 """Manage user and group accessibility for objects
1608 Permissions are lists of users and user groups. There are read and write
1609 permissions. Users and groups with write permission have also read
1614 def print_permissions(permissions_dict):
1615 expected_keys = ('read', 'write')
1616 if set(permissions_dict).issubset(expected_keys):
1617 print_dict(permissions_dict)
1619 invalid_keys = set(permissions_dict.keys()).difference(expected_keys)
1621 'Illegal permission keys: %s' % ', '.join(invalid_keys),
1622 importance=1, details=[
1623 'Valid permission types: %s' % ' '.join(expected_keys)])
1626 @command(pithos_cmds)
1627 class file_permissions_get(_file_container_command, _optional_json):
1628 """Get read and write permissions of an object"""
1631 @errors.pithos.connection
1632 @errors.pithos.container
1633 @errors.pithos.object_path
1636 self.client.get_object_sharing(self.path), print_permissions)
1638 def main(self, container___path):
1639 super(self.__class__, self)._run(
1640 container___path, path_is_optional=False)
1644 @command(pithos_cmds)
1645 class file_permissions_set(_file_container_command, _optional_output_cmd):
1646 """Set permissions for an object
1647 New permissions overwrite existing permissions.
1649 - read=<username>[,usergroup[,...]]
1650 - write=<username>[,usegroup[,...]]
1651 E.g. to give read permissions for file F to users A and B and write for C:
1652 . /file permissions set F read=A,B write=C
1656 def format_permission_dict(self, permissions):
1659 for perms in permissions:
1660 splstr = perms.split('=')
1661 if 'read' == splstr[0]:
1662 read = [ug.strip() for ug in splstr[1].split(',')]
1663 elif 'write' == splstr[0]:
1664 write = [ug.strip() for ug in splstr[1].split(',')]
1666 msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1667 raiseCLIError(None, msg)
1668 return (read, write)
1671 @errors.pithos.connection
1672 @errors.pithos.container
1673 @errors.pithos.object_path
1674 def _run(self, read, write):
1675 self._optional_output(self.client.set_object_sharing(
1676 self.path, read_permission=read, write_permission=write))
1678 def main(self, container___path, *permissions):
1679 super(self.__class__, self)._run(
1680 container___path, path_is_optional=False)
1681 read, write = self.format_permission_dict(permissions)
1682 self._run(read, write)
1685 @command(pithos_cmds)
1686 class file_permissions_delete(_file_container_command, _optional_output_cmd):
1687 """Delete all permissions set on object
1688 To modify permissions, use /file permissions set
1692 @errors.pithos.connection
1693 @errors.pithos.container
1694 @errors.pithos.object_path
1696 self._optional_output(self.client.del_object_sharing(self.path))
1698 def main(self, container___path):
1699 super(self.__class__, self)._run(
1700 container___path, path_is_optional=False)
1704 @command(pithos_cmds)
1705 class file_info(_file_container_command, _optional_json):
1706 """Get detailed information for user account, containers or objects
1707 to get account info: /file info
1708 to get container info: /file info <container>
1709 to get object info: /file info <container>:<path>
1713 object_version=ValueArgument(
1714 'show specific version \ (applies only for objects)',
1715 ('-O', '--object-version'))
1719 @errors.pithos.connection
1720 @errors.pithos.container
1721 @errors.pithos.object_path
1723 if self.container is None:
1724 r = self.client.get_account_info()
1725 elif self.path is None:
1726 r = self.client.get_container_info(self.container)
1728 r = self.client.get_object_info(
1729 self.path, version=self['object_version'])
1730 self._print(r, print_dict)
1732 def main(self, container____path__=None):
1733 super(self.__class__, self)._run(container____path__)
1737 @command(pithos_cmds)
1738 class file_metadata(_pithos_init):
1739 """Metadata are attached on objects. They are formed as key:value pairs.
1740 They can have arbitary values.
1744 @command(pithos_cmds)
1745 class file_metadata_get(_file_container_command, _optional_json):
1746 """Get metadata for account, containers or objects"""
1749 detail=FlagArgument('show detailed output', ('-l', '--details')),
1750 until=DateArgument('show metadata until then', '--until'),
1751 object_version=ValueArgument(
1752 'show specific version (applies only for objects)',
1753 ('-O', '--object-version'))
1757 @errors.pithos.connection
1758 @errors.pithos.container
1759 @errors.pithos.object_path
1761 until = self['until']
1763 if self.container is None:
1764 r = self.client.get_account_info(until=until)
1765 elif self.path is None:
1767 r = self.client.get_container_info(until=until)
1769 cmeta = self.client.get_container_meta(until=until)
1770 ometa = self.client.get_container_object_meta(until=until)
1773 r['container-meta'] = cmeta
1775 r['object-meta'] = ometa
1778 r = self.client.get_object_info(
1780 version=self['object_version'])
1782 r = self.client.get_object_meta(
1784 version=self['object_version'])
1786 self._print(r, print_dict)
1788 def main(self, container____path__=None):
1789 super(self.__class__, self)._run(container____path__)
1793 @command(pithos_cmds)
1794 class file_metadata_set(_file_container_command, _optional_output_cmd):
1795 """Set a piece of metadata for account, container or object"""
1798 @errors.pithos.connection
1799 @errors.pithos.container
1800 @errors.pithos.object_path
1801 def _run(self, metakey, metaval):
1802 if not self.container:
1803 r = self.client.set_account_meta({metakey: metaval})
1805 r = self.client.set_container_meta({metakey: metaval})
1807 r = self.client.set_object_meta(self.path, {metakey: metaval})
1808 self._optional_output(r)
1810 def main(self, metakey, metaval, container____path__=None):
1811 super(self.__class__, self)._run(container____path__)
1812 self._run(metakey=metakey, metaval=metaval)
1815 @command(pithos_cmds)
1816 class file_metadata_delete(_file_container_command, _optional_output_cmd):
1817 """Delete metadata with given key from account, container or object
1818 - to get metadata of current account: /file metadata get
1819 - to get metadata of a container: /file metadata get <container>
1820 - to get metadata of an object: /file metadata get <container>:<path>
1824 @errors.pithos.connection
1825 @errors.pithos.container
1826 @errors.pithos.object_path
1827 def _run(self, metakey):
1828 if self.container is None:
1829 r = self.client.del_account_meta(metakey)
1830 elif self.path is None:
1831 r = self.client.del_container_meta(metakey)
1833 r = self.client.del_object_meta(self.path, metakey)
1834 self._optional_output(r)
1836 def main(self, metakey, container____path__=None):
1837 super(self.__class__, self)._run(container____path__)
1841 @command(pithos_cmds)
1842 class file_quota(_file_account_command, _optional_json):
1843 """Get account quota"""
1846 in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1850 @errors.pithos.connection
1853 def pretty_print(output):
1854 if not self['in_bytes']:
1856 output[k] = format_size(output[k])
1857 print_dict(output, '-')
1859 self._print(self.client.get_account_quota(), pretty_print)
1861 def main(self, custom_uuid=None):
1862 super(self.__class__, self)._run(custom_account=custom_uuid)
1866 @command(pithos_cmds)
1867 class file_containerlimit(_pithos_init):
1868 """Container size limit commands"""
1871 @command(pithos_cmds)
1872 class file_containerlimit_get(_file_container_command, _optional_json):
1873 """Get container size limit"""
1876 in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1880 @errors.pithos.container
1883 def pretty_print(output):
1884 if not self['in_bytes']:
1885 for k, v in output.items():
1886 output[k] = 'unlimited' if '0' == v else format_size(v)
1887 print_dict(output, '-')
1890 self.client.get_container_limit(self.container), pretty_print)
1892 def main(self, container=None):
1893 super(self.__class__, self)._run()
1894 self.container = container
1898 @command(pithos_cmds)
1899 class file_containerlimit_set(_file_account_command, _optional_output_cmd):
1900 """Set new storage limit for a container
1901 By default, the limit is set in bytes
1902 Users may specify a different unit, e.g:
1903 /file containerlimit set 2.3GB mycontainer
1904 Valid units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1905 To set container limit to "unlimited", use 0
1909 def _calculate_limit(self, user_input):
1912 limit = int(user_input)
1915 digits = [str(num) for num in range(0, 10)] + ['.']
1916 while user_input[index] in digits:
1918 limit = user_input[:index]
1919 format = user_input[index:]
1921 return to_bytes(limit, format)
1922 except Exception as qe:
1923 msg = 'Failed to convert %s to bytes' % user_input,
1924 raiseCLIError(qe, msg, details=[
1925 'Syntax: containerlimit set <limit>[format] [container]',
1926 'e.g.: containerlimit set 2.3GB mycontainer',
1928 '(*1024): B, KiB, MiB, GiB, TiB',
1929 '(*1000): B, KB, MB, GB, TB'])
1933 @errors.pithos.connection
1934 @errors.pithos.container
1935 def _run(self, limit):
1937 self.client.container = self.container
1938 self._optional_output(self.client.set_container_limit(limit))
1940 def main(self, limit, container=None):
1941 super(self.__class__, self)._run()
1942 limit = self._calculate_limit(limit)
1943 self.container = container
1947 @command(pithos_cmds)
1948 class file_versioning(_pithos_init):
1949 """Manage the versioning scheme of current pithos user account"""
1952 @command(pithos_cmds)
1953 class file_versioning_get(_file_account_command, _optional_json):
1954 """Get versioning for account or container"""
1957 @errors.pithos.connection
1958 @errors.pithos.container
1961 self.client.get_container_versioning(self.container), print_dict)
1963 def main(self, container):
1964 super(self.__class__, self)._run()
1965 self.container = container
1969 @command(pithos_cmds)
1970 class file_versioning_set(_file_account_command, _optional_output_cmd):
1971 """Set versioning mode (auto, none) for account or container"""
1973 def _check_versioning(self, versioning):
1974 if versioning and versioning.lower() in ('auto', 'none'):
1975 return versioning.lower()
1976 raiseCLIError('Invalid versioning %s' % versioning, details=[
1977 'Versioning can be auto or none'])
1980 @errors.pithos.connection
1981 @errors.pithos.container
1982 def _run(self, versioning):
1983 self.client.container = self.container
1984 r = self.client.set_container_versioning(versioning)
1985 self._optional_output(r)
1987 def main(self, versioning, container):
1988 super(self.__class__, self)._run()
1989 self._run(self._check_versioning(versioning))
1992 @command(pithos_cmds)
1993 class file_group(_pithos_init):
1994 """Manage access groups and group members"""
1997 @command(pithos_cmds)
1998 class file_group_list(_file_account_command, _optional_json):
1999 """list all groups and group members"""
2002 @errors.pithos.connection
2004 self._print(self.client.get_account_group(), print_dict, delim='-')
2007 super(self.__class__, self)._run()
2011 @command(pithos_cmds)
2012 class file_group_set(_file_account_command, _optional_output_cmd):
2013 """Set a user group"""
2016 @errors.pithos.connection
2017 def _run(self, groupname, *users):
2018 self._optional_output(self.client.set_account_group(groupname, users))
2020 def main(self, groupname, *users):
2021 super(self.__class__, self)._run()
2023 self._run(groupname, *users)
2025 raiseCLIError('No users to add in group %s' % groupname)
2028 @command(pithos_cmds)
2029 class file_group_delete(_file_account_command, _optional_output_cmd):
2030 """Delete a user group"""
2033 @errors.pithos.connection
2034 def _run(self, groupname):
2035 self._optional_output(self.client.del_account_group(groupname))
2037 def main(self, groupname):
2038 super(self.__class__, self)._run()
2039 self._run(groupname)
2042 @command(pithos_cmds)
2043 class file_sharers(_file_account_command, _optional_json):
2044 """List the accounts that share objects with current user"""
2047 detail=FlagArgument('show detailed output', ('-l', '--details')),
2048 marker=ValueArgument('show output greater then marker', '--marker')
2052 @errors.pithos.connection
2054 accounts = self.client.get_sharing_accounts(marker=self['marker'])
2055 uuids = [acc['name'] for acc in accounts]
2057 astakos_responce = self.auth_base.post_user_catalogs(uuids)
2058 usernames = astakos_responce.json
2059 r = usernames['uuid_catalog']
2060 except Exception as e:
2061 print 'WARNING: failed to call user_catalogs, %s' % e
2062 r = dict(sharer_uuid=uuids)
2063 usernames = accounts
2064 if self['json_output'] or self['detail']:
2065 self._print(usernames)
2067 self._print(r, print_dict)
2070 super(self.__class__, self)._run()
2074 def version_print(versions):
2075 print_items([dict(id=vitem[0], created=strftime(
2076 '%d-%m-%Y %H:%M:%S',
2077 localtime(float(vitem[1])))) for vitem in versions])
2080 @command(pithos_cmds)
2081 class file_versions(_file_container_command, _optional_json):
2082 """Get the list of object versions
2083 Deleted objects may still have versions that can be used to restore it and
2084 get information about its previous state.
2085 The version number can be used in a number of other commands, like info,
2086 copy, move, meta. See these commands for more information, e.g.
2091 @errors.pithos.connection
2092 @errors.pithos.container
2093 @errors.pithos.object_path
2096 self.client.get_object_versionlist(self.path), version_print)
2098 def main(self, container___path):
2099 super(file_versions, self)._run(
2101 path_is_optional=False)