3 # Copyright 2011 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
41 from lib.client import Pithos_Client, Fault
42 from lib.util import get_user, get_auth, get_server, get_api
53 def cli_command(*args):
57 _cli_commands[name] = cls
61 def class_for_cli_command(name):
62 return _cli_commands[name]
64 class Command(object):
67 def __init__(self, name, argv):
68 parser = OptionParser('%%prog %s [options] %s' % (name, self.syntax))
69 parser.add_option('--host', dest='host', metavar='HOST',
70 default=get_server(), help='use server HOST')
71 parser.add_option('--user', dest='user', metavar='USERNAME',
73 help='use account USERNAME')
74 parser.add_option('--token', dest='token', metavar='AUTH',
76 help='use account AUTH')
77 parser.add_option('--api', dest='api', metavar='API',
78 default=get_api(), help='use api API')
79 parser.add_option('-v', action='store_true', dest='verbose',
80 default=False, help='use verbose output')
81 parser.add_option('-d', action='store_true', dest='debug',
82 default=False, help='use 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.host, self.token, self.user, self.api, 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')
147 def execute(self, container=None):
149 self.list_objects(container)
151 self.list_containers()
153 def list_containers(self):
154 attrs = ['limit', 'marker', 'if_modified_since',
155 'if_unmodified_since']
156 args = self._build_args(attrs)
157 args['format'] = 'json' if self.detail else 'text'
159 if getattr(self, 'until'):
160 t = _time.strptime(self.until, self.format)
161 args['until'] = int(_time.mktime(t))
163 l = self.client.list_containers(**args)
166 def list_objects(self, container):
169 attrs = ['limit', 'marker', 'prefix', 'delimiter', 'path',
170 'meta', 'if_modified_since', 'if_unmodified_since']
171 args = self._build_args(attrs)
172 args['format'] = 'json' if self.detail else 'text'
175 t = _time.strptime(self.until, self.format)
176 args['until'] = int(_time.mktime(t))
178 container, sep, object = container.partition('/')
183 #if request with meta quering disable trash filtering
184 show_trashed = True if self.meta else False
185 l = self.client.list_objects(container, **args)
186 print_list(l, detail=self.detail)
190 syntax = '[<container>[/<object>]]'
191 description = 'get account/container/object metadata'
193 def add_options(self, parser):
194 parser.add_option('-r', action='store_true', dest='restricted',
195 default=False, help='show only user defined metadata')
196 parser.add_option('--until', action='store', dest='until',
197 default=None, help='show metadata until that date')
198 parser.add_option('--format', action='store', dest='format',
199 default='%d/%m/%Y', help='format to parse until date')
200 parser.add_option('--version', action='store', dest='version',
201 default=None, help='show specific version \
202 (applies only for objects)')
204 def execute(self, path=''):
205 container, sep, object = path.partition('/')
206 args = {'restricted':self.restricted}
207 if getattr(self, 'until'):
208 t = _time.strptime(self.until, self.format)
209 args['until'] = int(_time.mktime(t))
212 meta = self.client.retrieve_object_metadata(container, object,
216 meta = self.client.retrieve_container_metadata(container, **args)
218 meta = self.client.retrieve_account_metadata(**args)
220 print 'Entity does not exist'
222 print_dict(meta, header=None)
224 @cli_command('create')
225 class CreateContainer(Command):
226 syntax = '<container> [key=val] [...]'
227 description = 'create a container'
229 def execute(self, container, *args):
232 key, sep, val = arg.partition('=')
234 ret = self.client.create_container(container, **meta)
236 print 'Container already exists'
238 @cli_command('delete', 'rm')
239 class Delete(Command):
240 syntax = '<container>[/<object>]'
241 description = 'delete a container or an object'
243 def add_options(self, parser):
244 parser.add_option('--until', action='store', dest='until',
245 default=None, help='remove history until that date')
246 parser.add_option('--format', action='store', dest='format',
247 default='%d/%m/%Y', help='format to parse until date')
249 def execute(self, path):
250 container, sep, object = path.partition('/')
252 if getattr(self, 'until'):
253 t = _time.strptime(self.until, self.format)
254 until = int(_time.mktime(t))
257 self.client.delete_object(container, object, until)
259 self.client.delete_container(container, until)
262 class GetObject(Command):
263 syntax = '<container>/<object>'
264 description = 'get the data of an object'
266 def add_options(self, parser):
267 parser.add_option('-l', action='store_true', dest='detail',
268 default=False, help='show detailed output')
269 parser.add_option('--range', action='store', dest='range',
270 default=None, help='show range of data')
271 parser.add_option('--if-range', action='store', dest='if_range',
272 default=None, help='show range of data')
273 parser.add_option('--if-match', action='store', dest='if_match',
274 default=None, help='show output if ETags match')
275 parser.add_option('--if-none-match', action='store',
276 dest='if_none_match', default=None,
277 help='show output if ETags don\'t match')
278 parser.add_option('--if-modified-since', action='store', type='str',
279 dest='if_modified_since', default=None,
280 help='show output if modified since then')
281 parser.add_option('--if-unmodified-since', action='store', type='str',
282 dest='if_unmodified_since', default=None,
283 help='show output if not modified since then')
284 parser.add_option('-o', action='store', type='str',
285 dest='file', default=None,
286 help='save output in file')
287 parser.add_option('--version', action='store', type='str',
288 dest='version', default=None,
289 help='get the specific \
291 parser.add_option('--versionlist', action='store_true',
292 dest='versionlist', default=False,
293 help='get the full object version list')
295 def execute(self, path):
296 attrs = ['if_match', 'if_none_match', 'if_modified_since',
297 'if_unmodified_since']
298 args = self._build_args(attrs)
299 args['format'] = 'json' if self.detail else 'text'
301 args['range'] = 'bytes=%s' %self.range
302 if getattr(self, 'if_range'):
303 args['if-range'] = 'If-Range:%s' % getattr(self, 'if_range')
305 container, sep, object = path.partition('/')
308 if 'detail' in args.keys():
310 data = self.client.retrieve_object_versionlist(container, object, **args)
312 data = self.client.retrieve_object_version(container, object,
313 self.version, **args)
315 data = self.client.retrieve_object(container, object, **args)
317 f = self.file and open(self.file, 'w') or stdout
319 data = json.loads(data)
321 print_versions(data, f=f)
323 print_dict(data, f=f)
328 @cli_command('mkdir')
329 class PutMarker(Command):
330 syntax = '<container>/<directory marker>'
331 description = 'create a directory marker'
333 def execute(self, path):
334 container, sep, object = path.partition('/')
335 self.client.create_directory_marker(container, object)
338 class PutObject(Command):
339 syntax = '<container>/<object> [key=val] [...]'
340 description = 'create/override object'
342 def add_options(self, parser):
343 parser.add_option('--use_hashes', action='store_true', dest='use_hashes',
344 default=False, help='provide hashmap instead of data')
345 parser.add_option('--chunked', action='store_true', dest='chunked',
346 default=False, help='set chunked transfer mode')
347 parser.add_option('--etag', action='store', dest='etag',
348 default=None, help='check written data')
349 parser.add_option('--content-encoding', action='store',
350 dest='content_encoding', default=None,
351 help='provide the object MIME content type')
352 parser.add_option('--content-disposition', action='store', type='str',
353 dest='content_disposition', default=None,
354 help='provide the presentation style of the object')
355 #parser.add_option('-S', action='store',
356 # dest='segment_size', default=False,
357 # help='use for large file support')
358 parser.add_option('--manifest', action='store',
359 dest='x_object_manifest', default=None,
360 help='upload a manifestation file')
361 parser.add_option('--content-type', action='store',
362 dest='content_type', default=None,
363 help='create object with specific content type')
364 parser.add_option('--sharing', action='store',
365 dest='x_object_sharing', default=None,
366 help='define sharing object policy')
367 parser.add_option('-f', action='store',
368 dest='srcpath', default=None,
369 help='file descriptor to read from (pass - for standard input)')
370 parser.add_option('--public', action='store_true',
371 dest='x_object_public', default=False,
372 help='make object publicly accessible')
374 def execute(self, path, *args):
375 if path.find('=') != -1:
376 raise Fault('Missing path argument')
378 #prepare user defined meta
381 key, sep, val = arg.partition('=')
384 attrs = ['etag', 'content_encoding', 'content_disposition',
385 'content_type', 'x_object_sharing', 'x_object_public']
386 args = self._build_args(attrs)
388 container, sep, object = path.partition('/')
392 f = open(self.srcpath) if self.srcpath != '-' else stdin
394 if self.use_hashes and not f:
395 raise Fault('Illegal option combination')
398 self.client.create_object_using_chunks(container, object, f,
400 elif self.use_hashes:
401 format = 'json' if detail else 'text'
402 self.client.create_object_by_hashmap(container, object, f, format,
404 elif self.x_object_manifest:
405 self.client.create_manifestation(container, object, self.x_object_manifest)
407 self.client.create_zero_length_object(container, object, meta=meta, **args)
409 self.client.create_object(container, object, f, meta=meta, **args)
413 @cli_command('copy', 'cp')
414 class CopyObject(Command):
415 syntax = '<src container>/<src object> [<dst container>/]<dst object> [key=val] [...]'
416 description = 'copy an object to a different location'
418 def add_options(self, parser):
419 parser.add_option('--version', action='store',
420 dest='version', default=False,
421 help='copy specific version')
422 parser.add_option('--public', action='store_true',
423 dest='public', default=False,
424 help='make object publicly accessible')
426 def execute(self, src, dst, *args):
427 src_container, sep, src_object = src.partition('/')
428 dst_container, sep, dst_object = dst.partition('/')
430 #prepare user defined meta
433 key, sep, val = arg.partition('=')
437 dst_container = src_container
440 self.client.copy_object(src_container, src_object, dst_container,
441 dst_object, meta, self.public, self.version, **meta)
444 class SetMeta(Command):
445 syntax = '[<container>[/<object>]] key=val [key=val] [...]'
446 description = 'set account/container/object metadata'
448 def execute(self, path, *args):
449 #in case of account fix the args
450 if path.find('=') != -1:
457 key, sep, val = arg.partition('=')
458 meta[key.strip()] = val.strip()
459 container, sep, object = path.partition('/')
461 self.client.update_object_metadata(container, object, **meta)
463 self.client.update_container_metadata(container, **meta)
465 self.client.update_account_metadata(**meta)
467 @cli_command('update')
468 class UpdateObject(Command):
469 syntax = '<container>/<object> path [key=val] [...]'
470 description = 'update object metadata/data (default mode: append)'
472 def add_options(self, parser):
473 parser.add_option('-a', action='store_true', dest='append',
474 default=True, help='append data')
475 parser.add_option('--offset', action='store',
477 default=None, help='starting offest to be updated')
478 parser.add_option('--range', action='store', dest='content-range',
479 default=None, help='range of data to be updated')
480 parser.add_option('--chunked', action='store_true', dest='chunked',
481 default=False, help='set chunked transfer mode')
482 parser.add_option('--content-encoding', action='store',
483 dest='content_encoding', default=None,
484 help='provide the object MIME content type')
485 parser.add_option('--content-disposition', action='store', type='str',
486 dest='content_disposition', default=None,
487 help='provide the presentation style of the object')
488 parser.add_option('--manifest', action='store', type='str',
489 dest='x_object_manifest', default=None,
490 help='use for large file support')
491 parser.add_option('--sharing', action='store',
492 dest='x_object_sharing', default=None,
493 help='define sharing object policy')
494 parser.add_option('--nosharing', action='store_true',
495 dest='no_sharing', default=None,
496 help='clear object sharing policy')
497 parser.add_option('-f', action='store',
498 dest='srcpath', default=None,
499 help='file descriptor to read from: pass - for standard input')
500 parser.add_option('--public', action='store_true',
501 dest='x_object_public', default=False,
502 help='make object publicly accessible')
504 def execute(self, path, *args):
505 if path.find('=') != -1:
506 raise Fault('Missing path argument')
508 #prepare user defined meta
511 key, sep, val = arg.partition('=')
515 self.x_object_sharing = ''
517 attrs = ['content_encoding', 'content_disposition', 'x_object_sharing',
519 args = self._build_args(attrs)
521 container, sep, object = path.partition('/')
525 f = open(self.srcpath) if self.srcpath != '-' else stdin
528 self.client.update_object_using_chunks(container, object, f,
531 self.client.update_object(container, object, f, meta=meta, **args)
535 @cli_command('move', 'mv')
536 class MoveObject(Command):
537 syntax = '<src container>/<src object> [<dst container>/]<dst object>'
538 description = 'move an object to a different location'
540 def add_options(self, parser):
541 parser.add_option('--version', action='store',
542 dest='version', default=None,
543 help='move a specific object version')
544 parser.add_option('--public', action='store_true',
545 dest='public', default=False,
546 help='make object publicly accessible')
548 def execute(self, src, dst, *args):
549 src_container, sep, src_object = src.partition('/')
550 dst_container, sep, dst_object = dst.partition('/')
552 dst_container = src_container
555 #prepare user defined meta
558 key, sep, val = arg.partition('=')
561 self.client.move_object(src_container, src_object, dst_container,
562 dst_object, meta, self.public, self.version)
564 @cli_command('remove')
565 class TrashObject(Command):
566 syntax = '<container>/<object>'
567 description = 'trash an object'
569 def execute(self, src):
570 src_container, sep, src_object = src.partition('/')
572 self.client.trash_object(src_container, src_object)
574 @cli_command('restore')
575 class RestoreObject(Command):
576 syntax = '<container>/<object>'
577 description = 'restore a trashed object'
579 def execute(self, src):
580 src_container, sep, src_object = src.partition('/')
582 self.client.restore_object(src_container, src_object)
584 @cli_command('unset')
585 class UnsetObject(Command):
586 syntax = '<container>/[<object>] key [key] [...]'
587 description = 'delete metadata info'
589 def execute(self, path, *args):
590 #in case of account fix the args
599 container, sep, object = path.partition('/')
601 self.client.delete_object_metadata(container, object, meta)
603 self.client.delete_container_metadata(container, meta)
605 self.client.delete_account_metadata(meta)
607 @cli_command('group')
608 class CreateGroup(Command):
609 syntax = 'key=val [key=val] [...]'
610 description = 'create account groups'
612 def execute(self, *args):
615 key, sep, val = arg.partition('=')
617 self.client.set_account_groups(**groups)
619 @cli_command('ungroup')
620 class DeleteGroup(Command):
621 syntax = 'key [key] [...]'
622 description = 'delete account groups'
624 def execute(self, *args):
628 self.client.unset_account_groups(groups)
630 @cli_command('policy')
631 class SetPolicy(Command):
632 syntax = 'container key=val [key=val] [...]'
633 description = 'set container policies'
635 def execute(self, path, *args):
636 if path.find('=') != -1:
637 raise Fault('Missing container argument')
639 container, sep, object = path.partition('/')
642 raise Fault('Only containers have policies')
646 key, sep, val = arg.partition('=')
649 self.client.set_container_policies(container, **policies)
651 @cli_command('publish')
652 class PublishObject(Command):
653 syntax = '<container>/<object>'
654 description = 'publish an object'
656 def execute(self, src):
657 src_container, sep, src_object = src.partition('/')
659 self.client.publish_object(src_container, src_object)
661 @cli_command('unpublish')
662 class UnpublishObject(Command):
663 syntax = '<container>/<object>'
664 description = 'unpublish an object'
666 def execute(self, src):
667 src_container, sep, src_object = src.partition('/')
669 self.client.unpublish_object(src_container, src_object)
672 cmd = Command('', [])
674 parser.usage = '%prog <command> [options]'
678 for cls in set(_cli_commands.values()):
679 name = ', '.join(cls.commands)
680 description = getattr(cls, 'description', '')
681 commands.append(' %s %s' % (name.ljust(12), description))
682 print '\nCommands:\n' + '\n'.join(sorted(commands))
684 def print_dict(d, header='name', f=stdout, detail=True):
685 header = header if header in d else 'subdir'
686 if header and header in d:
687 f.write('%s\n' %d.pop(header).encode('utf8'))
689 patterns = ['^x_(account|container|object)_meta_(\w+)$']
690 patterns.append(patterns[0].replace('_', '-'))
691 for key, val in sorted(d.items()):
692 f.write('%s: %s\n' % (key.rjust(30), val))
694 def print_list(l, verbose=False, f=stdout, detail=True):
696 #if it's empty string continue
699 if type(elem) == types.DictionaryType:
700 print_dict(elem, f=f, detail=detail)
701 elif type(elem) == types.StringType:
703 elem = elem.split('Traceback')[0]
704 f.write('%s\n' % elem)
706 f.write('%s\n' % elem)
708 def print_versions(data, f=stdout):
709 if 'versions' not in data:
710 f.write('%s\n' %data)
712 f.write('versions:\n')
713 for id, t in data['versions']:
714 f.write('%s @ %s\n' % (str(id).rjust(30), datetime.fromtimestamp(t)))
719 cls = class_for_cli_command(name)
720 except (IndexError, KeyError):
724 cmd = cls(name, argv[2:])
727 cmd.execute(*cmd.args)
729 cmd.parser.print_help()
732 status = f.status and '%s ' % f.status or ''
733 print '%s%s' % (status, f.data)
735 if __name__ == '__main__':