Statistics
| Branch: | Tag: | Revision:

root / tools / store @ d3fd269f

History | View | Annotate | Download (24.2 kB)

1
#!/usr/bin/env python
2

    
3
# Copyright 2011 GRNET S.A. All rights reserved.
4
# 
5
# Redistribution and use in source and binary forms, with or
6
# without modification, are permitted provided that the following
7
# conditions are met:
8
# 
9
#   1. Redistributions of source code must retain the above
10
#      copyright notice, this list of conditions and the following
11
#      disclaimer.
12
# 
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.
17
# 
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.
30
# 
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.
35

    
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
42

    
43
import json
44
import logging
45
import types
46
import re
47
import time as _time
48

    
49
DEFAULT_HOST = 'pithos.dev.grnet.gr'
50
DEFAULT_API = 'v1'
51

    
52
_cli_commands = {}
53

    
54
def cli_command(*args):
55
    def decorator(cls):
56
        cls.commands = args
57
        for name in args:
58
            _cli_commands[name] = cls
59
        return cls
60
    return decorator
61

    
62
def class_for_cli_command(name):
63
    return _cli_commands[name]
64

    
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)
80
        
81
        # Add options to self
82
        for opt in parser.option_list:
83
            key = opt.dest
84
            if key:
85
                val = getattr(options, key)
86
                setattr(self, key, val)
87
        
88
        self.client = Client(self.host, self.user, self.api, self.verbose,
89
                             self.debug)
90
        
91
        self.parser = parser
92
        self.args = args
93
        
94
    def add_options(self, parser):
95
        pass
96
    
97
    def execute(self, *args):
98
        pass
99

    
100
@cli_command('list', 'ls')
101
class List(Command):
102
    syntax = '[<container>[/<object>]]'
103
    description = 'list containers or objects'
104
    
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')
135
    
136
    def execute(self, container=None):
137
        if container:
138
            self.list_objects(container)
139
        else:
140
            self.list_containers()
141
    
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}
146
        
147
        if self.until:
148
            t = _time.strptime(self.until, self.format)
149
            params['until'] = int(_time.mktime(t))
150
        
151
        l = self.client.list_containers(self.detail, params, headers)
152
        print_list(l)
153
    
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('/')
161
        if object:
162
            return
163
        
164
        if self.until:
165
            t = _time.strptime(self.until, self.format)
166
            params['until'] = int(_time.mktime(t))
167
        
168
        detail = 'json'
169
        l = self.client.list_objects(container, detail, params, headers)
170
        print_list(l, detail=self.detail)
171

    
172
@cli_command('meta')
173
class Meta(Command):
174
    syntax = '[<container>[/<object>]]'
175
    description = 'get the metadata of an account, a container or an object'
176
    
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)')
187
    
188
    def execute(self, path=''):
189
        container, sep, object = path.partition('/')
190
        if self.until:
191
            t = _time.strptime(self.until, self.format)
192
            self.until = int(_time.mktime(t))
193
        if object:
194
            meta = self.client.retrieve_object_metadata(container, object,
195
                                                        self.restricted,
196
                                                        self.version)
197
        elif container:
198
            meta = self.client.retrieve_container_metadata(container,
199
                                                           self.restricted,
200
                                                           self.until)
201
        else:
202
            meta = self.client.account_metadata(self.restricted, self.until)
203
        if meta == None:
204
            print 'Entity does not exist'
205
        else:
206
            print_dict(meta, header=None)
207

    
208
@cli_command('create')
209
class CreateContainer(Command):
210
    syntax = '<container> [key=val] [...]'
211
    description = 'create a container'
212
    
213
    def execute(self, container, *args):
214
        headers = {}
215
        for arg in 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)
219
        if not ret:
220
            print 'Container already exists'
221

    
222
@cli_command('delete', 'rm')
223
class Delete(Command):
224
    syntax = '<container>[/<object>]'
225
    description = 'delete a container or an object'
226
    
227
    def execute(self, path):
228
        container, sep, object = path.partition('/')
229
        if object:
230
            self.client.delete_object(container, object)
231
        else:
232
            self.client.delete_container(container)
233

    
234
@cli_command('get')
235
class GetObject(Command):
236
    syntax = '<container>/<object>'
237
    description = 'get the data of an object'
238
    
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 \
261
                               version')
262
        parser.add_option('--versionlist', action='store_true',
263
                          dest='versionlist', default=False,
264
                          help='get the full object version list')
265
    
266
    def execute(self, path):
267
        headers = {}
268
        if self.range:
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)]
273
        for a in attrs:
274
            headers[a.replace('-', '_').upper()] = getattr(self, a)
275
        container, sep, object = path.partition('/')
276
        if self.versionlist:
277
            self.version = 'list'
278
            self.detail = True
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
282
        if self.detail:
283
            data = json.loads(data)
284
            if self.versionlist:
285
                print_versions(data, f=f)
286
            else:
287
                print_dict(data, f=f)
288
        else:
289
            f.write(data)
290
        f.close()
291

    
292
@cli_command('mkdir')
293
class PutMarker(Command):
294
    syntax = '<container>/<directory marker>'
295
    description = 'create a directory marker'
296
    
297
    def execute(self, path):
298
        container, sep, object = path.partition('/')
299
        self.client.create_directory_marker(container, object)
300

    
301
@cli_command('put')
302
class PutObject(Command):
303
    syntax = '<container>/<object> <path> [key=val] [...]'
304
    description = 'create/override object with path contents or standard input'
305
    
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')
329
        parser.add_option('--sharing', action='store',
330
                          dest='sharing', default=None,
331
                          help='define sharing object policy')
332
        
333
    def execute(self, path, srcpath='-', *args):
334
        headers = {}
335
        if self.manifest:
336
            headers['X_OBJECT_MANIFEST'] = self.manifest
337
        if self.sharing:
338
            headers['X_OBJECT_SHARING'] = self.sharing
339
        
340
        attrs = ['etag', 'content-encoding', 'content-disposition',
341
                 'content-type']
342
        attrs = [a for a in attrs if getattr(self, a)]
343
        for a in attrs:
344
            headers[a.replace('-', '_').upper()] = getattr(self, a)
345
        
346
        #prepare user defined meta
347
        for arg in args:
348
            key, sep, val = arg.partition('=')
349
            headers['X_OBJECT_META_%s' %key.strip().upper()] = val.strip()
350
        
351
        container, sep, object = path.partition('/')
352
        
353
        f = None
354
        chunked = False
355
        if not self.touch:
356
            f = srcpath != '-' and open(srcpath) or stdin
357
            chunked = (self.chunked or f == stdin) and True or False
358
        self.client.create_object(container, object, f, chunked=chunked,
359
                                  headers=headers)
360
        if f:
361
            f.close()
362

    
363
@cli_command('copy', 'cp')
364
class CopyObject(Command):
365
    syntax = '<src container>/<src object> [<dst container>/]<dst object>'
366
    description = 'copies an object to a different location'
367
    
368
    def add_options(self, parser):
369
        parser.add_option('--version', action='store',
370
                          dest='version', default=False,
371
                          help='copy specific version')
372
    
373
    def execute(self, src, dst):
374
        src_container, sep, src_object = src.partition('/')
375
        dst_container, sep, dst_object = dst.partition('/')
376
        if not sep:
377
            dst_container = src_container
378
            dst_object = dst
379
        version = getattr(self, 'version')
380
        if version:
381
            headers = {}
382
            headers['X_SOURCE_VERSION'] = version
383
        self.client.copy_object(src_container, src_object, dst_container,
384
                                dst_object, headers)
385

    
386
@cli_command('set')
387
class SetMeta(Command):
388
    syntax = '[<container>[/<object>]] key=val [key=val] [...]'
389
    description = 'set metadata'
390
    
391
    def execute(self, path, *args):
392
        #in case of account fix the args
393
        if path.find('=') != -1:
394
            args = list(args)
395
            args.append(path)
396
            args = tuple(args)
397
            path = ''
398
        meta = {}
399
        for arg in args:
400
            key, sep, val = arg.partition('=')
401
            meta[key.strip()] = val.strip()
402
        container, sep, object = path.partition('/')
403
        if object:
404
            self.client.update_object_metadata(container, object, **meta)
405
        elif container:
406
            self.client.update_container_metadata(container, **meta)
407
        else:
408
            self.client.update_account_metadata(**meta)
409

    
410
@cli_command('update')
411
class UpdateObject(Command):
412
    syntax = '<container>/<object> path [key=val] [...]'
413
    description = 'update object metadata/data (default mode: append)'
414
    
415
    def add_options(self, parser):
416
        parser.add_option('-a', action='store_true', dest='append',
417
                          default=True, help='append data')
418
        parser.add_option('--start', action='store',
419
                          dest='start',
420
                          default=None, help='range of data to be updated')
421
        parser.add_option('--range', action='store', dest='content-range',
422
                          default=None, help='range of data to be updated')
423
        parser.add_option('--chunked', action='store_true', dest='chunked',
424
                          default=False, help='set chunked transfer mode')
425
        parser.add_option('--content-encoding', action='store',
426
                          dest='content-encoding', default=None,
427
                          help='provide the object MIME content type')
428
        parser.add_option('--content-disposition', action='store', type='str',
429
                          dest='content-disposition', default=None,
430
                          help='provide the presentation style of the object')
431
        parser.add_option('--manifest', action='store', type='str',
432
                          dest='manifest', default=None,
433
                          help='use for large file support')        
434
        parser.add_option('--sharing', action='store',
435
                          dest='sharing', default=None,
436
                          help='define sharing object policy')
437

    
438
    
439
    def execute(self, path, srcpath='-', *args):
440
        headers = {}
441
        if self.manifest:
442
            headers['X_OBJECT_MANIFEST'] = self.manifest
443
        if self.sharing:
444
            headers['X_OBJECT_SHARING'] = self.sharing
445
        
446
        if getattr(self, 'start'):
447
            headers['CONTENT_RANGE'] = 'bytes %s-/*' % getattr(self, 'start')
448
        elif self.append:
449
            headers['CONTENT_RANGE'] = 'bytes */*'
450
        
451
        attrs = ['content-encoding', 'content-disposition']
452
        attrs = [a for a in attrs if getattr(self, a)]
453
        for a in attrs:
454
            headers[a.replace('-', '_').upper()] = getattr(self, a)
455
        
456
        #prepare user defined meta
457
        for arg in args:
458
            key, sep, val = arg.partition('=')
459
            headers['X_OBJECT_META_%s' %key.strip().upper()] = val.strip()
460
        
461
        container, sep, object = path.partition('/')
462
        
463
        f = srcpath != '-' and open(srcpath) or stdin
464
        chunked = (self.chunked or f == stdin) and True or False
465
        self.client.update_object(container, object, f, chunked=chunked,
466
                                  headers=headers)
467
        f.close()
468

    
469
@cli_command('move', 'mv')
470
class MoveObject(Command):
471
    syntax = '<src container>/<src object> [<dst container>/]<dst object>'
472
    description = 'moves an object to a different location'
473
    
474
    def execute(self, src, dst):
475
        src_container, sep, src_object = src.partition('/')
476
        dst_container, sep, dst_object = dst.partition('/')
477
        if not sep:
478
            dst_container = src_container
479
            dst_object = dst
480
        
481
        self.client.move_object(src_container, src_object, dst_container,
482
                                dst_object, headers)
483

    
484
@cli_command('remove', 'rm')
485
class TrashObject(Command):
486
    syntax = '<container>/<object>'
487
    description = 'trashes an object'
488
    
489
    def execute(self, src):
490
        src_container, sep, src_object = src.partition('/')
491
        
492
        self.client.trash_object(src_container, src_object)
493

    
494
@cli_command('restore')
495
class TrashObject(Command):
496
    syntax = '<container>/<object>'
497
    description = 'trashes an object'
498
    
499
    def execute(self, src):
500
        src_container, sep, src_object = src.partition('/')
501
        
502
        self.client.restore_object(src_container, src_object)
503

    
504
@cli_command('unset')
505
class TrashObject(Command):
506
    syntax = '<container>/[<object>] key [key] [...]'
507
    description = 'deletes metadata info'
508
    
509
    def execute(self, path, *args):
510
        #in case of account fix the args
511
        if len(args) == 0:
512
            args = list(args)
513
            args.append(path)
514
            args = tuple(args)
515
            path = ''
516
        meta = []
517
        for key in args:
518
            meta.append(key)
519
        container, sep, object = path.partition('/')
520
        if object:
521
            self.client.delete_object_metadata(container, object, meta)
522
        elif container:
523
            self.client.delete_container_metadata(container, meta)
524
        else:
525
            self.client.delete_account_metadata(meta)
526

    
527
@cli_command('group')
528
class SetGroup(Command):
529
    syntax = 'key=val [key=val] [...]'
530
    description = 'sets group account info'
531
    
532
    def execute(self, path='', **args):
533
        if len(args) == 0:
534
            args = list(args)
535
            args.append(path)
536
            args = tuple(args)
537
            path = ''
538
        groups = {}
539
        for arg in args:
540
            key, sep, val = arg.partition('=')
541
            groups[key] = val
542
        self.client.set_account_groups(groups)
543

    
544
def print_usage():
545
    cmd = Command([])
546
    parser = cmd.parser
547
    parser.usage = '%prog <command> [options]'
548
    parser.print_help()
549
    
550
    commands = []
551
    for cls in set(_cli_commands.values()):
552
        name = ', '.join(cls.commands)
553
        description = getattr(cls, 'description', '')
554
        commands.append('  %s %s' % (name.ljust(12), description))
555
    print '\nCommands:\n' + '\n'.join(sorted(commands))
556

    
557
def print_dict(d, header='name', f=stdout, detail=True):
558
    header = header in d and header or 'subdir'
559
    if header and header in d:
560
        f.write('%s\n' %d.pop(header))
561
    if detail:
562
        patterns = ['^x_(account|container|object)_meta_(\w+)$']
563
        patterns.append(patterns[0].replace('_', '-'))
564
        for key, val in sorted(d.items()):
565
            for p in patterns:
566
                p = re.compile(p)
567
                m = p.match(key)
568
                if m:
569
                    key = m.group(2)
570
            f.write('%s: %s\n' % (key.rjust(30), val))
571

    
572
def print_list(l, verbose=False, f=stdout, detail=True):
573
    for elem in l:
574
        #if it's empty string continue
575
        if not elem:
576
            continue
577
        if type(elem) == types.DictionaryType:
578
            print_dict(elem, f=f, detail=detail)
579
        elif type(elem) == types.StringType:
580
            if not verbose:
581
                elem = elem.split('Traceback')[0]
582
            f.write('%s\n' % elem)
583
        else:
584
            f.write('%s\n' % elem)
585

    
586
def print_versions(data, f=stdout):
587
    if 'versions' not in data:
588
        f.write('%s\n' %data)
589
        return
590
    f.write('versions:\n')
591
    for id, t in data['versions']:
592
        f.write('%s @ %s\n' % (str(id).rjust(30), datetime.fromtimestamp(t)))
593

    
594
def main():
595
    try:
596
        name = argv[1]
597
        cls = class_for_cli_command(name)
598
    except (IndexError, KeyError):
599
        print_usage()
600
        exit(1)
601
    
602
    cmd = cls(argv[2:])
603
    
604
    try:
605
        cmd.execute(*cmd.args)
606
    except TypeError, e:
607
        cmd.parser.usage = '%%prog %s [options] %s' % (name, cmd.syntax)
608
        cmd.parser.print_help()
609
        exit(1)
610
    except Fault, f:
611
        status = f.status and '%s ' % f.status or ''
612
        print '%s%s' % (status, f.data)
613

    
614
if __name__ == '__main__':
615
    main()