3 # Copyright 2011-2012 GRNET S.A. All rights reserved.
5 # Redistribution and use in source and binary forms, with or
6 # without modification, are permitted provided that the following
9 # 1. Redistributions of source code must retain the above
10 # copyright notice, this list of conditions and the following
13 # 2. Redistributions in binary form must reproduce the above
14 # copyright notice, this list of conditions and the following
15 # disclaimer in the documentation and/or other materials
16 # provided with the distribution.
18 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 # POSSIBILITY OF SUCH DAMAGE.
31 # The views and conclusions contained in the software and
32 # documentation are those of the authors and should not be
33 # interpreted as representing official policies, either expressed
34 # or implied, of GRNET S.A.
36 from getpass import getuser
37 from optparse import OptionParser
38 from os import environ
39 from sys import argv, exit, stdin, stdout
40 from datetime import datetime
42 from pithos.tools.lib.client import Pithos_Client, Fault
43 from pithos.tools.lib.util import get_user, get_auth, get_url
44 from pithos.tools.lib.transfer import upload, download
55 def cli_command(*args):
59 _cli_commands[name] = cls
63 def class_for_cli_command(name):
64 return _cli_commands[name]
66 class Command(object):
69 def __init__(self, name, argv):
70 parser = OptionParser('%%prog %s [options] %s' % (name, self.syntax))
71 parser.add_option('--url', dest='url', metavar='URL',
72 default=get_url(), help='server URL (currently: %s)' % get_url())
73 parser.add_option('--user', dest='user', metavar='USER',
75 help='account USER (currently: %s)' % get_user())
76 parser.add_option('--token', dest='token', metavar='TOKEN',
78 help='account TOKEN (currently: %s)' % get_auth())
79 parser.add_option('-v', action='store_true', dest='verbose',
80 default=False, help='verbose output')
81 parser.add_option('-d', action='store_true', dest='debug',
82 default=False, help='debug output')
83 self.add_options(parser)
84 options, args = parser.parse_args(argv)
87 for opt in parser.option_list:
90 val = getattr(options, key)
91 setattr(self, key, val)
93 self.client = Pithos_Client(self.url, self.token, self.user, self.verbose,
99 def _build_args(self, attrs):
101 for a in [a for a in attrs if getattr(self, a)]:
102 args[a] = getattr(self, a)
105 def add_options(self, parser):
108 def execute(self, *args):
111 @cli_command('list', 'ls')
113 syntax = '[<container>[/<object>]]'
114 description = 'list containers or objects'
116 def add_options(self, parser):
117 parser.add_option('-l', action='store_true', dest='detail',
118 default=False, help='show detailed output')
119 parser.add_option('-n', action='store', type='int', dest='limit',
120 default=10000, help='show limited output')
121 parser.add_option('--marker', action='store', type='str',
122 dest='marker', default=None,
123 help='show output greater then marker')
124 parser.add_option('--prefix', action='store', type='str',
125 dest='prefix', default=None,
126 help='show output starting with prefix')
127 parser.add_option('--delimiter', action='store', type='str',
128 dest='delimiter', default=None,
129 help='show output up to the delimiter')
130 parser.add_option('--path', action='store', type='str',
131 dest='path', default=None,
132 help='show output starting with prefix up to /')
133 parser.add_option('--meta', action='store', type='str',
134 dest='meta', default=None,
135 help='show output having the specified meta keys')
136 parser.add_option('--if-modified-since', action='store', type='str',
137 dest='if_modified_since', default=None,
138 help='show output if modified since then')
139 parser.add_option('--if-unmodified-since', action='store', type='str',
140 dest='if_unmodified_since', default=None,
141 help='show output if not modified since then')
142 parser.add_option('--until', action='store', dest='until',
143 default=None, help='show metadata until that date')
144 parser.add_option('--format', action='store', dest='format',
145 default='%d/%m/%Y', help='format to parse until date')
146 parser.add_option('--shared', action='store_true', dest='shared',
147 default=False, help='show only shared')
148 parser.add_option('--public', action='store_true', dest='public',
149 default=False, help='show only public')
152 def execute(self, container=None):
154 self.list_objects(container)
156 self.list_containers()
158 def list_containers(self):
159 attrs = ['limit', 'marker', 'if_modified_since',
160 'if_unmodified_since', 'shared', 'public']
161 args = self._build_args(attrs)
162 args['format'] = 'json' if self.detail else 'text'
164 if getattr(self, 'until'):
165 t = _time.strptime(self.until, self.format)
166 args['until'] = int(_time.mktime(t))
168 l = self.client.list_containers(**args)
171 def list_objects(self, container):
174 attrs = ['limit', 'marker', 'prefix', 'delimiter', 'path',
175 'meta', 'if_modified_since', 'if_unmodified_since',
177 args = self._build_args(attrs)
178 args['format'] = 'json' if self.detail else 'text'
181 t = _time.strptime(self.until, self.format)
182 args['until'] = int(_time.mktime(t))
184 container, sep, object = container.partition('/')
189 #if request with meta quering disable trash filtering
190 show_trashed = True if self.meta else False
191 l = self.client.list_objects(container, **args)
192 print_list(l, detail=self.detail)
196 syntax = '[<container>[/<object>]]'
197 description = 'get account/container/object metadata'
199 def add_options(self, parser):
200 parser.add_option('-r', action='store_true', dest='restricted',
201 default=False, help='show only user defined metadata')
202 parser.add_option('--until', action='store', dest='until',
203 default=None, help='show metadata until that date')
204 parser.add_option('--format', action='store', dest='format',
205 default='%d/%m/%Y', help='format to parse until date')
206 parser.add_option('--version', action='store', dest='version',
207 default=None, help='show specific version \
208 (applies only for objects)')
210 def execute(self, path=''):
211 container, sep, object = path.partition('/')
212 args = {'restricted': self.restricted}
213 if getattr(self, 'until'):
214 t = _time.strptime(self.until, self.format)
215 args['until'] = int(_time.mktime(t))
218 meta = self.client.retrieve_object_metadata(container, object,
222 meta = self.client.retrieve_container_metadata(container, **args)
224 meta = self.client.retrieve_account_metadata(**args)
226 print 'Entity does not exist'
228 print_dict(meta, header=None)
230 @cli_command('create')
231 class CreateContainer(Command):
232 syntax = '<container> [key=val] [...]'
233 description = 'create a container'
235 def add_options(self, parser):
236 parser.add_option('--versioning', action='store', dest='versioning',
237 default=None, help='set container versioning (auto/none)')
238 parser.add_option('--quota', action='store', dest='quota',
239 default=None, help='set default container quota')
241 def execute(self, container, *args):
244 key, sep, val = arg.partition('=')
247 if getattr(self, 'versioning'):
248 policy['versioning'] = self.versioning
249 if getattr(self, 'quota'):
250 policy['quota'] = self.quota
251 ret = self.client.create_container(container, meta=meta, policies=policy)
253 print 'Container already exists'
255 @cli_command('delete', 'rm')
256 class Delete(Command):
257 syntax = '<container>[/<object>]'
258 description = 'delete a container or an object'
260 def add_options(self, parser):
261 parser.add_option('--until', action='store', dest='until',
262 default=None, help='remove history until that date')
263 parser.add_option('--format', action='store', dest='format',
264 default='%d/%m/%Y', help='format to parse until date')
265 parser.add_option('--delimiter', action='store', type='str',
266 dest='delimiter', default=None,
267 help='mass delete objects with path staring with <src object> + delimiter')
268 parser.add_option('-r', action='store_true',
269 dest='recursive', default=False,
270 help='mass delimiter objects with delimiter /')
272 def execute(self, path):
273 container, sep, object = path.partition('/')
275 if getattr(self, 'until'):
276 t = _time.strptime(self.until, self.format)
277 until = int(_time.mktime(t))
282 kwargs['delimiter'] = self.delimiter
284 kwargs['delimiter'] = '/'
285 self.client.delete_object(container, object, until, **kwargs)
287 self.client.delete_container(container, until)
290 class GetObject(Command):
291 syntax = '<container>/<object>'
292 description = 'get the data of an object'
294 def add_options(self, parser):
295 parser.add_option('-l', action='store_true', dest='detail',
296 default=False, help='show detailed output')
297 parser.add_option('--range', action='store', dest='range',
298 default=None, help='show range of data')
299 parser.add_option('--if-range', action='store', dest='if_range',
300 default=None, help='show range of data')
301 parser.add_option('--if-match', action='store', dest='if_match',
302 default=None, help='show output if ETags match')
303 parser.add_option('--if-none-match', action='store',
304 dest='if_none_match', default=None,
305 help='show output if ETags don\'t match')
306 parser.add_option('--if-modified-since', action='store', type='str',
307 dest='if_modified_since', default=None,
308 help='show output if modified since then')
309 parser.add_option('--if-unmodified-since', action='store', type='str',
310 dest='if_unmodified_since', default=None,
311 help='show output if not modified since then')
312 parser.add_option('-o', action='store', type='str',
313 dest='file', default=None,
314 help='save output in file')
315 parser.add_option('--version', action='store', type='str',
316 dest='version', default=None,
317 help='get the specific \
319 parser.add_option('--versionlist', action='store_true',
320 dest='versionlist', default=False,
321 help='get the full object version list')
322 parser.add_option('--hashmap', action='store_true',
323 dest='hashmap', default=False,
324 help='get the object hashmap instead')
326 def execute(self, path):
327 attrs = ['if_match', 'if_none_match', 'if_modified_since',
328 'if_unmodified_since', 'hashmap']
329 args = self._build_args(attrs)
330 args['format'] = 'json' if self.detail else 'text'
332 args['range'] = 'bytes=%s' % self.range
333 if getattr(self, 'if_range'):
334 args['if-range'] = 'If-Range:%s' % getattr(self, 'if_range')
336 container, sep, object = path.partition('/')
339 if 'detail' in args.keys():
343 data = self.client.retrieve_object_versionlist(container, object, **args)
345 data = self.client.retrieve_object_version(container, object,
346 self.version, **args)
348 if 'detail' in args.keys():
352 data = self.client.retrieve_object_hashmap(container, object, **args)
354 data = self.client.retrieve_object(container, object, **args)
356 f = open(self.file, 'w') if self.file else stdout
357 if self.detail or type(data) == types.DictionaryType:
359 print_versions(data, f=f)
361 print_dict(data, f=f)
366 @cli_command('mkdir')
367 class PutMarker(Command):
368 syntax = '<container>/<directory marker>'
369 description = 'create a directory marker'
371 def execute(self, path):
372 container, sep, object = path.partition('/')
373 self.client.create_directory_marker(container, object)
376 class PutObject(Command):
377 syntax = '<container>/<object> [key=val] [...]'
378 description = 'create/override object'
380 def add_options(self, parser):
381 parser.add_option('--use_hashes', action='store_true', dest='use_hashes',
382 default=False, help='provide hashmap instead of data')
383 parser.add_option('--chunked', action='store_true', dest='chunked',
384 default=False, help='set chunked transfer mode')
385 parser.add_option('--etag', action='store', dest='etag',
386 default=None, help='check written data')
387 parser.add_option('--content-encoding', action='store',
388 dest='content_encoding', default=None,
389 help='provide the object MIME content type')
390 parser.add_option('--content-disposition', action='store', type='str',
391 dest='content_disposition', default=None,
392 help='provide the presentation style of the object')
393 #parser.add_option('-S', action='store',
394 # dest='segment_size', default=False,
395 # help='use for large file support')
396 parser.add_option('--manifest', action='store',
397 dest='x_object_manifest', default=None,
398 help='provide object parts prefix in <container>/<object> form')
399 parser.add_option('--content-type', action='store',
400 dest='content_type', default=None,
401 help='create object with specific content type')
402 parser.add_option('--sharing', action='store',
403 dest='x_object_sharing', default=None,
404 help='define sharing object policy')
405 parser.add_option('-f', action='store',
406 dest='srcpath', default=None,
407 help='file descriptor to read from (pass - for standard input)')
408 parser.add_option('--public', action='store_true',
409 dest='x_object_public', default=False,
410 help='make object publicly accessible')
412 def execute(self, path, *args):
413 if path.find('=') != -1:
414 raise Fault('Missing path argument')
416 #prepare user defined meta
419 key, sep, val = arg.partition('=')
422 attrs = ['etag', 'content_encoding', 'content_disposition',
423 'content_type', 'x_object_sharing', 'x_object_public']
424 args = self._build_args(attrs)
426 container, sep, object = path.partition('/')
430 f = open(self.srcpath) if self.srcpath != '-' else stdin
432 if self.use_hashes and not f:
433 raise Fault('Illegal option combination')
436 self.client.create_object_using_chunks(container, object, f,
438 elif self.use_hashes:
440 hashmap = json.loads(data)
441 self.client.create_object_by_hashmap(container, object, hashmap,
443 elif self.x_object_manifest:
444 self.client.create_manifestation(container, object, self.x_object_manifest)
446 self.client.create_zero_length_object(container, object, meta=meta, **args)
448 self.client.create_object(container, object, f, meta=meta, **args)
452 @cli_command('copy', 'cp')
453 class CopyObject(Command):
454 syntax = '<src container>/<src object> [<dst container>/]<dst object> [key=val] [...]'
455 description = 'copy an object to a different location'
457 def add_options(self, parser):
458 parser.add_option('--version', action='store',
459 dest='version', default=False,
460 help='copy specific version')
461 parser.add_option('--public', action='store_true',
462 dest='public', default=False,
463 help='make object publicly accessible')
464 parser.add_option('--content-type', action='store',
465 dest='content_type', default=None,
466 help='change object\'s content type')
467 parser.add_option('--delimiter', action='store', type='str',
468 dest='delimiter', default=None,
469 help='mass copy objects with path staring with <src object> + delimiter')
470 parser.add_option('-r', action='store_true',
471 dest='recursive', default=False,
472 help='mass copy with delimiter /')
474 def execute(self, src, dst, *args):
475 src_container, sep, src_object = src.partition('/')
476 dst_container, sep, dst_object = dst.partition('/')
478 #prepare user defined meta
481 key, sep, val = arg.partition('=')
485 dst_container = src_container
488 args = {'content_type':self.content_type} if self.content_type else {}
490 args['delimiter'] = self.delimiter
492 args['delimiter'] = '/'
493 self.client.copy_object(src_container, src_object, dst_container,
494 dst_object, meta, self.public, self.version,
498 class SetMeta(Command):
499 syntax = '[<container>[/<object>]] key=val [key=val] [...]'
500 description = 'set account/container/object metadata'
502 def execute(self, path, *args):
503 #in case of account fix the args
504 if path.find('=') != -1:
511 key, sep, val = arg.partition('=')
512 meta[key.strip()] = val.strip()
513 container, sep, object = path.partition('/')
515 self.client.update_object_metadata(container, object, **meta)
517 self.client.update_container_metadata(container, **meta)
519 self.client.update_account_metadata(**meta)
521 @cli_command('update')
522 class UpdateObject(Command):
523 syntax = '<container>/<object> path [key=val] [...]'
524 description = 'update object metadata/data (default mode: append)'
526 def add_options(self, parser):
527 parser.add_option('-a', action='store_true', dest='append',
528 default=True, help='append data')
529 parser.add_option('--offset', action='store',
531 default=None, help='starting offest to be updated')
532 parser.add_option('--range', action='store', dest='content_range',
533 default=None, help='range of data to be updated')
534 parser.add_option('--chunked', action='store_true', dest='chunked',
535 default=False, help='set chunked transfer mode')
536 parser.add_option('--content-encoding', action='store',
537 dest='content_encoding', default=None,
538 help='provide the object MIME content type')
539 parser.add_option('--content-disposition', action='store', type='str',
540 dest='content_disposition', default=None,
541 help='provide the presentation style of the object')
542 parser.add_option('--manifest', action='store', type='str',
543 dest='x_object_manifest', default=None,
544 help='use for large file support')
545 parser.add_option('--sharing', action='store',
546 dest='x_object_sharing', default=None,
547 help='define sharing object policy')
548 parser.add_option('--nosharing', action='store_true',
549 dest='no_sharing', default=None,
550 help='clear object sharing policy')
551 parser.add_option('-f', action='store',
552 dest='srcpath', default=None,
553 help='file descriptor to read from: pass - for standard input')
554 parser.add_option('--public', action='store_true',
555 dest='x_object_public', default=False,
556 help='make object publicly accessible')
557 parser.add_option('--replace', action='store_true',
558 dest='replace', default=False,
559 help='override metadata')
561 def execute(self, path, *args):
562 if path.find('=') != -1:
563 raise Fault('Missing path argument')
565 #prepare user defined meta
568 key, sep, val = arg.partition('=')
572 attrs = ['content_encoding', 'content_disposition', 'x_object_sharing',
573 'x_object_public', 'x_object_manifest', 'replace', 'offset',
575 args = self._build_args(attrs)
578 args['x_object_sharing'] = ''
580 container, sep, object = path.partition('/')
584 f = open(self.srcpath) if self.srcpath != '-' else stdin
587 self.client.update_object_using_chunks(container, object, f,
590 self.client.update_object(container, object, f, meta=meta, **args)
594 @cli_command('move', 'mv')
595 class MoveObject(Command):
596 syntax = '<src container>/<src object> [<dst container>/]<dst object>'
597 description = 'move an object to a different location'
599 def add_options(self, parser):
600 parser.add_option('--public', action='store_true',
601 dest='public', default=False,
602 help='make object publicly accessible')
603 parser.add_option('--content-type', action='store',
604 dest='content_type', default=None,
605 help='change object\'s content type')
606 parser.add_option('--delimiter', action='store', type='str',
607 dest='delimiter', default=None,
608 help='mass move objects with path staring with <src object> + delimiter')
609 parser.add_option('-r', action='store_true',
610 dest='recursive', default=False,
611 help='mass move objects with delimiter /')
613 def execute(self, src, dst, *args):
614 src_container, sep, src_object = src.partition('/')
615 dst_container, sep, dst_object = dst.partition('/')
617 dst_container = src_container
620 #prepare user defined meta
623 key, sep, val = arg.partition('=')
626 args = {'content_type':self.content_type} if self.content_type else {}
628 args['delimiter'] = self.delimiter
630 args['delimiter'] = '/'
631 self.client.move_object(src_container, src_object, dst_container,
632 dst_object, meta, self.public, **args)
634 @cli_command('unset')
635 class UnsetObject(Command):
636 syntax = '<container>/[<object>] key [key] [...]'
637 description = 'delete metadata info'
639 def execute(self, path, *args):
640 #in case of account fix the args
649 container, sep, object = path.partition('/')
651 self.client.delete_object_metadata(container, object, meta)
653 self.client.delete_container_metadata(container, meta)
655 self.client.delete_account_metadata(meta)
657 @cli_command('group')
658 class CreateGroup(Command):
659 syntax = 'key=val [key=val] [...]'
660 description = 'create account groups'
662 def execute(self, *args):
665 key, sep, val = arg.partition('=')
667 self.client.set_account_groups(**groups)
669 @cli_command('ungroup')
670 class DeleteGroup(Command):
671 syntax = 'key [key] [...]'
672 description = 'delete account groups'
674 def execute(self, *args):
678 self.client.unset_account_groups(groups)
680 @cli_command('policy')
681 class SetPolicy(Command):
682 syntax = 'container key=val [key=val] [...]'
683 description = 'set container policies'
685 def execute(self, path, *args):
686 if path.find('=') != -1:
687 raise Fault('Missing container argument')
689 container, sep, object = path.partition('/')
692 raise Fault('Only containers have policies')
696 key, sep, val = arg.partition('=')
699 self.client.set_container_policies(container, **policies)
701 @cli_command('publish')
702 class PublishObject(Command):
703 syntax = '<container>/<object>'
704 description = 'publish an object'
706 def execute(self, src):
707 src_container, sep, src_object = src.partition('/')
709 self.client.publish_object(src_container, src_object)
711 @cli_command('unpublish')
712 class UnpublishObject(Command):
713 syntax = '<container>/<object>'
714 description = 'unpublish an object'
716 def execute(self, src):
717 src_container, sep, src_object = src.partition('/')
719 self.client.unpublish_object(src_container, src_object)
721 @cli_command('sharing')
722 class SharingObject(Command):
723 syntax = 'list users sharing objects with the user'
724 description = 'list user accounts sharing objects with the user'
726 def add_options(self, parser):
727 parser.add_option('-l', action='store_true', dest='detail',
728 default=False, help='show detailed output')
729 parser.add_option('-n', action='store', type='int', dest='limit',
730 default=10000, help='show limited output')
731 parser.add_option('--marker', action='store', type='str',
732 dest='marker', default=None,
733 help='show output greater then marker')
737 attrs = ['limit', 'marker']
738 args = self._build_args(attrs)
739 args['format'] = 'json' if self.detail else 'text'
741 print_list(self.client.list_shared_by_others(**args))
745 syntax = '<file> <container>[/<prefix>]'
746 description = 'upload file to container (using prefix)'
748 def execute(self, file, path):
749 container, sep, prefix = path.partition('/')
750 upload(self.client, file, container, prefix)
752 @cli_command('receive')
753 class Receive(Command):
754 syntax = '<container>/<object> <file>'
755 description = 'download object to file'
757 def execute(self, path, file):
758 container, sep, object = path.partition('/')
759 download(self.client, container, object, file)
762 cmd = Command('', [])
764 parser.usage = '%prog <command> [options]'
768 for cls in set(_cli_commands.values()):
769 name = ', '.join(cls.commands)
770 description = getattr(cls, 'description', '')
771 commands.append(' %s %s' % (name.ljust(12), description))
772 print '\nCommands:\n' + '\n'.join(sorted(commands))
774 def print_dict(d, header='name', f=stdout, detail=True):
775 header = header if header in d else 'subdir'
776 if header and header in d:
777 f.write('%s\n' %d.pop(header).encode('utf8'))
779 patterns = ['^x_(account|container|object)_meta_(\w+)$']
780 patterns.append(patterns[0].replace('_', '-'))
781 for key, val in sorted(d.items()):
782 f.write('%s: %s\n' % (key.rjust(30), val))
784 def print_list(l, verbose=False, f=stdout, detail=True):
786 #if it's empty string continue
789 if type(elem) == types.DictionaryType:
790 print_dict(elem, f=f, detail=detail)
791 elif type(elem) == types.StringType:
793 elem = elem.split('Traceback')[0]
794 f.write('%s\n' % elem)
796 f.write('%s\n' % elem)
798 def print_versions(data, f=stdout):
799 if 'versions' not in data:
800 f.write('%s\n' %data)
802 f.write('versions:\n')
803 for id, t in data['versions']:
804 f.write('%s @ %s\n' % (str(id).rjust(30), datetime.fromtimestamp(float(t))))
810 cls = class_for_cli_command(name)
811 except (IndexError, KeyError):
815 cmd = cls(name, argv[2:])
818 cmd.execute(*cmd.args)
820 cmd.parser.print_help()
823 status = '%s ' % f.status if f.status else ''
824 print '%s%s' % (status, f.data)
827 if __name__ == '__main__':