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.path import basename
39 from sys import argv, exit, stdin, stdout
40 from pithos.lib.client import Client, Fault
41 from datetime import datetime
49 DEFAULT_HOST = 'pithos.dev.grnet.gr'
54 def cli_command(*args):
58 _cli_commands[name] = cls
62 def class_for_cli_command(name):
63 return _cli_commands[name]
65 class Command(object):
66 def __init__(self, argv):
67 parser = OptionParser()
68 parser.add_option('--host', dest='host', metavar='HOST',
69 default=DEFAULT_HOST, help='use server HOST')
70 parser.add_option('--user', dest='user', metavar='USERNAME',
71 default=getuser(), help='use account USERNAME')
72 parser.add_option('--api', dest='api', metavar='API',
73 default=DEFAULT_API, help='use api API')
74 parser.add_option('-v', action='store_true', dest='verbose',
75 default=False, help='use verbose output')
76 parser.add_option('-d', action='store_true', dest='debug',
77 default=False, help='use debug output')
78 self.add_options(parser)
79 options, args = parser.parse_args(argv)
82 for opt in parser.option_list:
85 val = getattr(options, key)
86 setattr(self, key, val)
88 self.client = Client(self.host, self.user, self.api, self.verbose,
94 def add_options(self, parser):
97 def execute(self, *args):
100 @cli_command('list', 'ls')
102 syntax = '[<container>[/<object>]]'
103 description = 'list containers or objects'
105 def add_options(self, parser):
106 parser.add_option('-l', action='store_true', dest='detail',
107 default=False, help='show detailed output')
108 parser.add_option('-n', action='store', type='int', dest='limit',
109 default=1000, help='show limited output')
110 parser.add_option('--marker', action='store', type='str',
111 dest='marker', default=None,
112 help='show output greater then marker')
113 parser.add_option('--prefix', action='store', type='str',
114 dest='prefix', default=None,
115 help='show output starting with prefix')
116 parser.add_option('--delimiter', action='store', type='str',
117 dest='delimiter', default=None,
118 help='show output up to the delimiter')
119 parser.add_option('--path', action='store', type='str',
120 dest='path', default=None,
121 help='show output starting with prefix up to /')
122 parser.add_option('--meta', action='store', type='str',
123 dest='meta', default=None,
124 help='show output having the specified meta keys')
125 parser.add_option('--if-modified-since', action='store', type='str',
126 dest='if_modified_since', default=None,
127 help='show output if modified since then')
128 parser.add_option('--if-unmodified-since', action='store', type='str',
129 dest='if_unmodified_since', default=None,
130 help='show output if not modified since then')
131 parser.add_option('--until', action='store', dest='until',
132 default=False, help='show metadata until that date')
133 parser.add_option('--format', action='store', dest='format',
134 default='%d/%m/%Y', help='format to parse until date')
136 def execute(self, container=None):
138 self.list_objects(container)
140 self.list_containers()
142 def list_containers(self):
143 params = {'limit':self.limit, 'marker':self.marker}
144 headers = {'IF_MODIFIED_SINCE':self.if_modified_since,
145 'IF_UNMODIFIED_SINCE':self.if_unmodified_since}
148 t = _time.strptime(self.until, self.format)
149 params['until'] = int(_time.mktime(t))
151 l = self.client.list_containers(self.detail, params, headers)
154 def list_objects(self, container):
155 params = {'limit':self.limit, 'marker':self.marker,
156 'prefix':self.prefix, 'delimiter':self.delimiter,
157 'path':self.path, 'meta':self.meta}
158 headers = {'IF_MODIFIED_SINCE':self.if_modified_since,
159 'IF_UNMODIFIED_SINCE':self.if_unmodified_since}
160 container, sep, object = container.partition('/')
165 t = _time.strptime(self.until, self.format)
166 params['until'] = int(_time.mktime(t))
169 l = self.client.list_objects(container, detail, params, headers)
170 print_list(l, detail=self.detail)
174 syntax = '[<container>[/<object>]]'
175 description = 'get the metadata of an account, a container or an object'
177 def add_options(self, parser):
178 parser.add_option('-r', action='store_true', dest='restricted',
179 default=False, help='show only user defined metadata')
180 parser.add_option('--until', action='store', dest='until',
181 default=False, help='show metadata until that date')
182 parser.add_option('--format', action='store', dest='format',
183 default='%d/%m/%Y', help='format to parse until date')
184 parser.add_option('--version', action='store', dest='version',
185 default=None, help='show specific version \
186 (applies only for objects)')
188 def execute(self, path=''):
189 container, sep, object = path.partition('/')
191 t = _time.strptime(self.until, self.format)
192 self.until = int(_time.mktime(t))
194 meta = self.client.retrieve_object_metadata(container, object,
198 meta = self.client.retrieve_container_metadata(container,
202 meta = self.client.account_metadata(self.restricted, self.until)
204 print 'Entity does not exist'
206 print_dict(meta, header=None)
208 @cli_command('create')
209 class CreateContainer(Command):
210 syntax = '<container> [key=val] [...]'
211 description = 'create a container'
213 def execute(self, container, *args):
216 key, sep, val = arg.partition('=')
217 headers['X_CONTAINER_META_%s' %key.strip().upper()] = val.strip()
218 ret = self.client.create_container(container, headers)
220 print 'Container already exists'
222 @cli_command('delete', 'rm')
223 class Delete(Command):
224 syntax = '<container>[/<object>]'
225 description = 'delete a container or an object'
227 def execute(self, path):
228 container, sep, object = path.partition('/')
230 self.client.delete_object(container, object)
232 self.client.delete_container(container)
235 class GetObject(Command):
236 syntax = '<container>/<object>'
237 description = 'get the data of an object'
239 def add_options(self, parser):
240 parser.add_option('-l', action='store_true', dest='detail',
241 default=False, help='show detailed output')
242 parser.add_option('--range', action='store', dest='range',
243 default=None, help='show range of data')
244 parser.add_option('--if-match', action='store', dest='if-match',
245 default=None, help='show output if ETags match')
246 parser.add_option('--if-none-match', action='store',
247 dest='if-none-match', default=None,
248 help='show output if ETags don\'t match')
249 parser.add_option('--if-modified-since', action='store', type='str',
250 dest='if-modified-since', default=None,
251 help='show output if modified since then')
252 parser.add_option('--if-unmodified-since', action='store', type='str',
253 dest='if-unmodified-since', default=None,
254 help='show output if not modified since then')
255 parser.add_option('-f', action='store', type='str',
256 dest='file', default=None,
257 help='save output in file')
258 parser.add_option('--version', action='store', type='str',
259 dest='version', default=None,
260 help='get the specific \
262 parser.add_option('--versionlist', action='store_true',
263 dest='versionlist', default=False,
264 help='get the full object version list')
266 def execute(self, path):
269 headers['RANGE'] = 'bytes=%s' %self.range
270 attrs = ['if-match', 'if-none-match', 'if-modified-since',
271 'if-unmodified-since']
272 attrs = [a for a in attrs if getattr(self, a)]
274 headers[a.replace('-', '_').upper()] = getattr(self, a)
275 container, sep, object = path.partition('/')
277 self.version = 'list'
279 data = self.client.retrieve_object(container, object, self.detail,
280 headers, self.version)
281 f = self.file and open(self.file, 'w') or stdout
283 data = json.loads(data)
285 print_versions(data, f=f)
287 print_dict(data, f=f)
292 @cli_command('mkdir')
293 class PutMarker(Command):
294 syntax = '<container>/<directory marker>'
295 description = 'create a directory marker'
297 def execute(self, path):
298 container, sep, object = path.partition('/')
299 self.client.create_directory_marker(container, object)
302 class PutObject(Command):
303 syntax = '<container>/<object> <path> [key=val] [...]'
304 description = 'create/override object with path contents or standard input'
306 def add_options(self, parser):
307 parser.add_option('--chunked', action='store_true', dest='chunked',
308 default=False, help='set chunked transfer mode')
309 parser.add_option('--etag', action='store', dest='etag',
310 default=None, help='check written data')
311 parser.add_option('--content-encoding', action='store',
312 dest='content-encoding', default=None,
313 help='provide the object MIME content type')
314 parser.add_option('--content-disposition', action='store', type='str',
315 dest='content-disposition', default=None,
316 help='provide the presentation style of the object')
317 parser.add_option('-S', action='store',
318 dest='segment-size', default=False,
319 help='use for large file support')
320 parser.add_option('--manifest', action='store_true',
321 dest='manifest', default=None,
322 help='upload a manifestation file')
323 parser.add_option('--type', action='store',
324 dest='content-type', default=False,
325 help='create object with specific content type')
326 parser.add_option('--touch', action='store_true',
327 dest='touch', default=False,
328 help='create object with zero data')
330 def execute(self, path, srcpath='-', *args):
333 headers['X_OBJECT_MANIFEST'] = self.manifest
335 attrs = ['etag', 'content-encoding', 'content-disposition',
337 attrs = [a for a in attrs if getattr(self, a)]
339 headers[a.replace('-', '_').upper()] = getattr(self, a)
341 #prepare user defined meta
343 key, sep, val = arg.partition('=')
344 headers['X_OBJECT_META_%s' %key.strip().upper()] = val.strip()
346 container, sep, object = path.partition('/')
351 f = srcpath != '-' and open(srcpath) or stdin
352 chunked = (self.chunked or f == stdin) and True or False
353 self.client.create_object(container, object, f, chunked=chunked,
358 @cli_command('copy', 'cp')
359 class CopyObject(Command):
360 syntax = '<src container>/<src object> [<dst container>/]<dst object>'
361 description = 'copies an object to a different location'
363 def add_options(self, parser):
364 parser.add_option('--version', action='store',
365 dest='version', default=False,
366 help='copy specific version')
368 def execute(self, src, dst):
369 src_container, sep, src_object = src.partition('/')
370 dst_container, sep, dst_object = dst.partition('/')
372 dst_container = src_container
374 version = getattr(self, 'version')
377 headers['X_SOURCE_VERSION'] = version
378 self.client.copy_object(src_container, src_object, dst_container,
382 class SetMeta(Command):
383 syntax = '[<container>[/<object>]] key=val [key=val] [...]'
384 description = 'set metadata'
386 def execute(self, path, *args):
387 #in case of account fix the args
388 if path.find('=') != -1:
395 key, sep, val = arg.partition('=')
396 meta[key.strip()] = val.strip()
397 container, sep, object = path.partition('/')
399 self.client.update_object_metadata(container, object, **meta)
401 self.client.update_container_metadata(container, **meta)
403 self.client.update_account_metadata(**meta)
405 @cli_command('update')
406 class UpdateObject(Command):
407 syntax = '<container>/<object> path [key=val] [...]'
408 description = 'update object metadata/data (default mode: append)'
410 def add_options(self, parser):
411 parser.add_option('-a', action='store_true', dest='append',
412 default=True, help='append data')
413 parser.add_option('--start', action='store',
415 default=None, help='range of data to be updated')
416 parser.add_option('--range', action='store', dest='content-range',
417 default=None, help='range of data to be updated')
418 parser.add_option('--chunked', action='store_true', dest='chunked',
419 default=False, help='set chunked transfer mode')
420 parser.add_option('--content-encoding', action='store',
421 dest='content-encoding', default=None,
422 help='provide the object MIME content type')
423 parser.add_option('--content-disposition', action='store', type='str',
424 dest='content-disposition', default=None,
425 help='provide the presentation style of the object')
426 parser.add_option('--manifest', action='store', type='str',
427 dest='manifest', default=None,
428 help='use for large file support')
430 def execute(self, path, srcpath='-', *args):
433 headers['X_OBJECT_MANIFEST'] = self.manifest
435 if getattr(self, 'start'):
436 headers['CONTENT_RANGE'] = 'bytes %s-/*' % getattr(self, 'start')
438 headers['CONTENT_RANGE'] = 'bytes */*'
440 attrs = ['content-encoding', 'content-disposition']
441 attrs = [a for a in attrs if getattr(self, a)]
443 headers[a.replace('-', '_').upper()] = getattr(self, a)
445 #prepare user defined meta
447 key, sep, val = arg.partition('=')
448 headers['X_OBJECT_META_%s' %key.strip().upper()] = val.strip()
450 container, sep, object = path.partition('/')
452 f = srcpath != '-' and open(srcpath) or stdin
453 chunked = (self.chunked or f == stdin) and True or False
454 self.client.update_object(container, object, f, chunked=chunked,
458 @cli_command('move', 'mv')
459 class MoveObject(Command):
460 syntax = '<src container>/<src object> [<dst container>/]<dst object>'
461 description = 'moves an object to a different location'
463 def execute(self, src, dst):
464 src_container, sep, src_object = src.partition('/')
465 dst_container, sep, dst_object = dst.partition('/')
467 dst_container = src_container
470 self.client.move_object(src_container, src_object, dst_container,
473 @cli_command('remove', 'rm')
474 class TrashObject(Command):
475 syntax = '<container>/<object>'
476 description = 'trashes an object'
478 def execute(self, src):
479 src_container, sep, src_object = src.partition('/')
481 self.client.trash_object(src_container, src_object)
483 @cli_command('restore')
484 class TrashObject(Command):
485 syntax = '<container>/<object>'
486 description = 'trashes an object'
488 def execute(self, src):
489 src_container, sep, src_object = src.partition('/')
491 self.client.restore_object(src_container, src_object)
493 @cli_command('unset')
494 class TrashObject(Command):
495 syntax = '<container>/[<object>] key [key] [...]'
496 description = 'deletes metadata info'
498 def execute(self, path, *args):
499 #in case of account fix the args
508 container, sep, object = path.partition('/')
510 self.client.delete_object_metadata(container, object, meta)
512 self.client.delete_container_metadata(container, meta)
514 self.client.delete_account_metadata(meta)
519 parser.usage = '%prog <command> [options]'
523 for cls in set(_cli_commands.values()):
524 name = ', '.join(cls.commands)
525 description = getattr(cls, 'description', '')
526 commands.append(' %s %s' % (name.ljust(12), description))
527 print '\nCommands:\n' + '\n'.join(sorted(commands))
529 def print_dict(d, header='name', f=stdout, detail=True):
530 header = header in d and header or 'subdir'
531 if header and header in d:
532 f.write('%s\n' %d.pop(header))
534 patterns = ['^x_(account|container|object)_meta_(\w+)$']
535 patterns.append(patterns[0].replace('_', '-'))
536 for key, val in sorted(d.items()):
542 f.write('%s: %s\n' % (key.rjust(30), val))
544 def print_list(l, verbose=False, f=stdout, detail=True):
546 #if it's empty string continue
549 if type(elem) == types.DictionaryType:
550 print_dict(elem, f=f, detail=detail)
551 elif type(elem) == types.StringType:
553 elem = elem.split('Traceback')[0]
554 f.write('%s\n' % elem)
556 f.write('%s\n' % elem)
558 def print_versions(data, f=stdout):
559 if 'versions' not in data:
560 f.write('%s\n' %data)
562 f.write('versions:\n')
563 for id, t in data['versions']:
564 f.write('%s @ %s\n' % (str(id).rjust(30), datetime.fromtimestamp(t)))
569 cls = class_for_cli_command(name)
570 except (IndexError, KeyError):
577 cmd.execute(*cmd.args)
580 cmd.parser.usage = '%%prog %s [options] %s' % (name, cmd.syntax)
581 cmd.parser.print_help()
584 print f.status, f.data
585 status = f.status and '%s ' % f.status or ''
586 print '%s%s' % (status, f.data)
588 if __name__ == '__main__':