Statistics
| Branch: | Tag: | Revision:

root / tools / lib / client.py @ 9471fece

History | View | Annotate | Download (40.5 kB)

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

    
34
from httplib import HTTPConnection, HTTP
35
from sys import stdin
36
from xml.dom import minidom
37
from StringIO import StringIO
38

    
39
import json
40
import types
41
import socket
42
import urllib
43
import datetime
44

    
45
ERROR_CODES = {304:'Not Modified',
46
               400:'Bad Request',
47
               401:'Unauthorized',
48
               404:'Not Found',
49
               409:'Conflict',
50
               411:'Length Required',
51
               412:'Precondition Failed',
52
               416:'Range Not Satisfiable',
53
               422:'Unprocessable Entity',
54
               503:'Service Unavailable'}
55

    
56
class Fault(Exception):
57
    def __init__(self, data='', status=None):
58
        if data == '' and status in ERROR_CODES.keys():
59
            data = ERROR_CODES[status]
60
        Exception.__init__(self, data)
61
        self.data = data
62
        self.status = status
63

    
64
class Client(object):
65
    def __init__(self, host, token, account, api='v1', verbose=False, debug=False):
66
        """`host` can also include a port, e.g '127.0.0.1:8000'."""
67
        
68
        self.host = host
69
        self.account = account
70
        self.api = api
71
        self.verbose = verbose or debug
72
        self.debug = debug
73
        self.token = token
74
    
75
    def _req(self, method, path, body=None, headers={}, format='text', params={}):
76
        slash = '/' if self.api else ''
77
        full_path = '%s%s%s?format=%s' % (slash, self.api, path, format)
78
        
79
        for k,v in params.items():
80
            if v:
81
                full_path = '%s&%s=%s' %(full_path, k, v)
82
            else:
83
                full_path = '%s&%s=' %(full_path, k)
84
        conn = HTTPConnection(self.host)
85
        
86
        #encode whitespace
87
        full_path = full_path.replace(' ', '%20')
88
        
89
        kwargs = {}
90
        for k,v in headers.items():
91
            headers.pop(k)
92
            k = k.replace('_', '-')
93
            headers[k] = v
94
        
95
        kwargs['headers'] = headers
96
        kwargs['headers']['X-Auth-Token'] = self.token
97
        if body:
98
            kwargs['body'] = body
99
            kwargs['headers'].setdefault('content-type', 'application/octet-stream')
100
        kwargs['headers'].setdefault('content-length', len(body) if body else 0)
101
        
102
        #print '#', method, full_path, kwargs
103
        t1 = datetime.datetime.utcnow()
104
        conn.request(method, full_path, **kwargs)
105
        
106
        resp = conn.getresponse()
107
        t2 = datetime.datetime.utcnow()
108
        #print 'response time:', str(t2-t1)
109
        headers = dict(resp.getheaders())
110
        
111
        if self.verbose:
112
            print '%d %s' % (resp.status, resp.reason)
113
            for key, val in headers.items():
114
                print '%s: %s' % (key.capitalize(), val)
115
            print
116
        
117
        length = resp.getheader('content-length', None)
118
        data = resp.read(length)
119
        if self.debug:
120
            print data
121
            print
122
        
123
        if int(resp.status) in ERROR_CODES.keys():
124
            raise Fault(data, int(resp.status))
125
        
126
        #print '**',  resp.status, headers, data, '\n'
127
        return resp.status, headers, data
128
    
129
    def _chunked_transfer(self, path, method='PUT', f=stdin, headers=None,
130
                          blocksize=1024, params={}):
131
        """perfomrs a chunked request"""
132
        http = HTTPConnection(self.host)
133
        
134
        # write header
135
        full_path = '/%s%s?' % (self.api, path)
136
        
137
        for k,v in params.items():
138
            if v:
139
                full_path = '%s&%s=%s' %(full_path, k, v)
140
            else:
141
                full_path = '%s&%s=' %(full_path, k)
142
        http.putrequest(method, full_path)
143
        http.putheader('x-auth-token', self.token)
144
        http.putheader('content-type', 'application/octet-stream')
145
        http.putheader('transfer-encoding', 'chunked')
146
        if headers:
147
            for header,value in headers.items():
148
                http.putheader(header, value)
149
        http.endheaders()
150
        
151
        # write body
152
        data = ''
153
        while True:
154
            if f.closed:
155
                break
156
            block = f.read(blocksize)
157
            if block == '':
158
                break
159
            data = '%x\r\n%s\r\n' % (len(block), block)
160
            try:
161
                http.send(data)
162
            except:
163
                #retry
164
                http.send(data)
165
        data = '0\r\n\r\n'
166
        try:
167
            http.send(data)
168
        except:
169
            #retry
170
            http.send(data)
171
        
172
        # get response
173
        resp = http.getresponse()
174
        
175
        headers = dict(resp.getheaders())
176
        
177
        if self.verbose:
178
            print '%d %s' % (resp.status, resp.reason)
179
            for key, val in headers.items():
180
                print '%s: %s' % (key.capitalize(), val)
181
            print
182
        
183
        length = resp.getheader('Content-length', None)
184
        data = resp.read(length)
185
        if self.debug:
186
            print data
187
            print
188
        
189
        if int(resp.status) in ERROR_CODES.keys():
190
            raise Fault(data, int(resp.status))
191
        
192
        #print '*',  resp.status, headers, data
193
        return resp.status, headers, data
194
    
195
    def delete(self, path, format='text', params={}):
196
        return self._req('DELETE', path, format=format, params=params)
197
    
198
    def get(self, path, format='text', headers={}, params={}):
199
        return self._req('GET', path, headers=headers, format=format,
200
                        params=params)
201
    
202
    def head(self, path, format='text', params={}):
203
         return self._req('HEAD', path, format=format, params=params)
204
    
205
    def post(self, path, body=None, format='text', headers=None, params={}):
206
        return self._req('POST', path, body, headers=headers, format=format,
207
                        params=params)
208
    
209
    def put(self, path, body=None, format='text', headers=None, params={}):
210
        return self._req('PUT', path, body, headers=headers, format=format,
211
                         params=params)
212
    
213
    def _list(self, path, format='text', params={}, **headers):
214
        status, headers, data = self.get(path, format=format, headers=headers,
215
                                         params=params)
216
        if format == 'json':
217
            data = json.loads(data) if data else ''
218
        elif format == 'xml':
219
            data = minidom.parseString(data)
220
        else:
221
            data = data.strip().split('\n') if data else ''
222
        return data
223
    
224
    def _get_metadata(self, path, prefix=None, params={}):
225
        status, headers, data = self.head(path, params=params)
226
        prefixlen = len(prefix) if prefix else 0
227
        meta = {}
228
        for key, val in headers.items():
229
            if prefix and not key.startswith(prefix):
230
                continue
231
            elif prefix and key.startswith(prefix):
232
                key = key[prefixlen:]
233
            meta[key] = val
234
        return meta
235
    
236
    def _filter(self, l, d):
237
        """
238
        filter out from l elements having the metadata values provided
239
        """
240
        ll = l
241
        for elem in l:
242
            if type(elem) == types.DictionaryType:
243
                for key in d.keys():
244
                    k = 'x_object_meta_%s' % key
245
                    if k in elem.keys() and elem[k] == d[key]:
246
                        ll.remove(elem)
247
                        break
248
        return ll
249
    
250
class OOS_Client(Client):
251
    """Openstack Object Storage Client"""
252
    
253
    def _update_metadata(self, path, entity, **meta):
254
        """adds new and updates the values of previously set metadata"""
255
        ex_meta = self.retrieve_account_metadata(restricted=True)
256
        ex_meta.update(meta)
257
        headers = {}
258
        prefix = 'x-%s-meta-' % entity
259
        for k,v in ex_meta.items():
260
            k = '%s%s' % (prefix, k)
261
            headers[k] = v
262
        return self.post(path, headers=headers)
263
    
264
    def _reset_metadata(self, path, entity, **meta):
265
        """
266
        overwrites all user defined metadata
267
        """
268
        headers = {}
269
        prefix = 'x-%s-meta-' % entity
270
        for k,v in meta.items():
271
            k = '%s%s' % (prefix, k)
272
            headers[k] = v
273
        return self.post(path, headers=headers)
274
    
275
    def _delete_metadata(self, path, entity, meta=[]):
276
        """delete previously set metadata"""
277
        ex_meta = self.retrieve_account_metadata(restricted=True)
278
        headers = {}
279
        prefix = 'x-%s-meta-' % entity
280
        for k in ex_meta.keys():
281
            if k in meta:
282
                headers['%s%s' % (prefix, k)] = ex_meta[k]
283
        return self.post(path, headers=headers)
284
    
285
    # Storage Account Services
286
    
287
    def list_containers(self, format='text', limit=None,
288
                        marker=None, params={}, account=None, **headers):
289
        """lists containers"""
290
        account = account or self.account
291
        path = '/%s' % account
292
        params.update({'limit':limit, 'marker':marker})
293
        return self._list(path, format, params, **headers)
294
    
295
    def retrieve_account_metadata(self, restricted=False, account=None, **params):
296
        """returns the account metadata"""
297
        account = account or self.account
298
        path = '/%s' % account
299
        prefix = 'x-account-meta-' if restricted else None
300
        return self._get_metadata(path, prefix, params)
301
    
302
    def update_account_metadata(self, account=None, **meta):
303
        """updates the account metadata"""
304
        account = account or self.account
305
        path = '/%s' % account
306
        return self._update_metadata(path, 'account', **meta)
307
        
308
    def delete_account_metadata(self, meta=[], account=None):
309
        """deletes the account metadata"""
310
        account = account or self.account
311
        path = '/%s' % account
312
        return self._delete_metadata(path, 'account', meta)
313
    
314
    def reset_account_metadata(self, account=None, **meta):
315
        """resets account metadata"""
316
        account = account or self.account
317
        path = '/%s' % account
318
        return self._reset_metadata(path, 'account', **meta)
319
    
320
    # Storage Container Services
321
    
322
    def _filter_trashed(self, l):
323
        return self._filter(l, {'trash':'true'})
324
    
325
    def list_objects(self, container, format='text',
326
                     limit=None, marker=None, prefix=None, delimiter=None,
327
                     path=None, include_trashed=False, params={}, account=None,
328
                     **headers):
329
        """returns a list with the container objects"""
330
        account = account or self.account
331
        params.update({'limit':limit, 'marker':marker, 'prefix':prefix,
332
                       'delimiter':delimiter, 'path':path})
333
        l = self._list('/%s/%s' % (account, container), format, params,
334
                       **headers)
335
        #TODO support filter trashed with xml also
336
        if format != 'xml' and not include_trashed:
337
            l = self._filter_trashed(l)
338
        return l
339
    
340
    def create_container(self, container, account=None, **meta):
341
        """creates a container"""
342
        account = account or self.account
343
        headers = {}
344
        for k,v in meta.items():
345
            headers['x-container-meta-%s' %k.strip().upper()] = v.strip()
346
        status, header, data = self.put('/%s/%s' % (account, container),
347
                                        headers=headers)
348
        if status == 202:
349
            return False
350
        elif status != 201:
351
            raise Fault(data, int(status))
352
        return True
353
    
354
    def delete_container(self, container, params={}, account=None):
355
        """deletes a container"""
356
        account = account or self.account
357
        return self.delete('/%s/%s' % (account, container), params=params)
358
    
359
    def retrieve_container_metadata(self, container, restricted=False,
360
                                    account=None, **params):
361
        """returns the container metadata"""
362
        account = account or self.account
363
        prefix = 'x-container-meta-' if restricted else None
364
        return self._get_metadata('/%s/%s' % (account, container), prefix,
365
                                  params)
366
    
367
    def update_container_metadata(self, container, account=None, **meta):
368
        """unpdates the container metadata"""
369
        account = account or self.account
370
        return self._update_metadata('/%s/%s' % (account, container),
371
                                     'container', **meta)
372
        
373
    def delete_container_metadata(self, container, meta=[], account=None):
374
        """deletes the container metadata"""
375
        account = account or self.account
376
        path = '/%s/%s' % (account, container)
377
        return self._delete_metadata(path, 'container', meta)
378
    
379
    # Storage Object Services
380
    
381
    def request_object(self, container, object, format='text', params={},
382
                       account=None, **headers):
383
        """returns tuple containing the status, headers and data response for an object request"""
384
        account = account or self.account
385
        path = '/%s/%s/%s' % (account, container, object)
386
        status, headers, data = self.get(path, format, headers, params)
387
        return status, headers, data
388
    
389
    def retrieve_object(self, container, object, format='text', params={},
390
                        account=None, **headers):
391
        """returns an object's data"""
392
        account = account or self.account
393
        t = self.request_object(container, object, format, params, account,
394
                                **headers)
395
        data = t[2]
396
        if format == 'json':
397
            data = json.loads(data) if data else ''
398
        elif format == 'xml':
399
            data = minidom.parseString(data)
400
        return data
401
    
402
    def retrieve_object_hashmap(self, container, object, params={},
403
                        account=None, **headers):
404
        """returns the hashmap representing object's data"""
405
        args = locals().copy()
406
        for elem in ['self', 'container', 'object']:
407
            args.pop(elem)
408
        data = self.retrieve_object(container, object, format='json', **args)
409
        return data['hashes']
410
    
411
    def create_directory_marker(self, container, object, account=None):
412
        """creates a dierectory marker"""
413
        account = account or self.account
414
        if not object:
415
            raise Fault('Directory markers have to be nested in a container')
416
        h = {'content_type':'application/directory'}
417
        return self.create_zero_length_object(container, object, account=account,
418
                                              **h)
419
    
420
    def create_object(self, container, object, f=stdin, format='text', meta={},
421
                      params={}, etag=None, content_type=None, content_encoding=None,
422
                      content_disposition=None, account=None, **headers):
423
        """creates a zero-length object"""
424
        account = account or self.account
425
        path = '/%s/%s/%s' % (account, container, object)
426
        for k, v  in headers.items():
427
            if not v:
428
                headers.pop(k)
429
        
430
        l = ['etag', 'content_encoding', 'content_disposition', 'content_type']
431
        l = [elem for elem in l if eval(elem)]
432
        for elem in l:
433
            headers.update({elem:eval(elem)})
434
        headers.setdefault('content-type', 'application/octet-stream')
435
        
436
        for k,v in meta.items():
437
            headers['x-object-meta-%s' %k.strip()] = v.strip()
438
        data = f.read() if f else None
439
        return self.put(path, data, format, headers=headers, params=params)
440
    
441
    def create_zero_length_object(self, container, object, meta={}, etag=None,
442
                                  content_type=None, content_encoding=None,
443
                                  content_disposition=None, account=None,
444
                                  **headers):
445
        account = account or self.account
446
        args = locals().copy()
447
        for elem in ['self', 'container', 'headers', 'account']:
448
            args.pop(elem)
449
        args.update(headers)
450
        return self.create_object(container, account=account, f=None, **args)
451
    
452
    def update_object(self, container, object, f=stdin,
453
                      offset=None, meta={}, params={}, content_length=None,
454
                      content_type=None, content_encoding=None,
455
                      content_disposition=None,  account=None, **headers):
456
        account = account or self.account
457
        path = '/%s/%s/%s' % (account, container, object)
458
        for k, v  in headers.items():
459
            if not v:
460
                headers.pop(k)
461
        
462
        l = ['content_encoding', 'content_disposition', 'content_type',
463
             'content_length']
464
        l = [elem for elem in l if eval(elem)]
465
        for elem in l:
466
            headers.update({elem:eval(elem)})
467
            
468
        if 'content_range' not in headers.keys():
469
            if offset != None:
470
                headers['content_range'] = 'bytes %s-/*' % offset
471
            else:
472
                headers['content_range'] = 'bytes */*'
473
            
474
        for k,v in meta.items():
475
            headers['x-object-meta-%s' %k.strip()] = v.strip()
476
        data = f.read() if f else None
477
        return self.post(path, data, headers=headers, params=params)
478
    
479
    def update_object_using_chunks(self, container, object, f=stdin,
480
                                   blocksize=1024, offset=None, meta={},
481
                                   params={}, content_type=None, content_encoding=None,
482
                                   content_disposition=None, account=None, **headers):
483
        """updates an object (incremental upload)"""
484
        account = account or self.account
485
        path = '/%s/%s/%s' % (account, container, object)
486
        headers = headers if not headers else {}
487
        l = ['content_type', 'content_encoding', 'content_disposition']
488
        l = [elem for elem in l if eval(elem)]
489
        for elem in l:
490
            headers.update({elem:eval(elem)})
491
        
492
        if offset != None:
493
            headers['content_range'] = 'bytes %s-/*' % offset
494
        else:
495
            headers['content_range'] = 'bytes */*'
496
        
497
        for k,v in meta.items():
498
            v = v.strip()
499
            headers['x-object-meta-%s' %k.strip()] = v
500
        return self._chunked_transfer(path, 'POST', f, headers=headers,
501
                                      blocksize=blocksize, params=params)
502
    
503
    def _change_obj_location(self, src_container, src_object, dst_container,
504
                             dst_object, remove=False, meta={}, account=None,
505
                             content_type=None, **headers):
506
        account = account or self.account
507
        path = '/%s/%s/%s' % (account, dst_container, dst_object)
508
        headers = {} if not headers else headers
509
        for k, v in meta.items():
510
            headers['x-object-meta-%s' % k] = v
511
        if remove:
512
            headers['x-move-from'] = '/%s/%s' % (src_container, src_object)
513
        else:
514
            headers['x-copy-from'] = '/%s/%s' % (src_container, src_object)
515
        headers['content_length'] = 0
516
        if content_type:
517
            headers['content_type'] = content_type 
518
        return self.put(path, headers=headers)
519
    
520
    def copy_object(self, src_container, src_object, dst_container, dst_object,
521
                   meta={}, account=None, content_type=None, **headers):
522
        """copies an object"""
523
        account = account or self.account
524
        return self._change_obj_location(src_container, src_object,
525
                                   dst_container, dst_object, account=account,
526
                                   remove=False, meta=meta,
527
                                   content_type=content_type, **headers)
528
    
529
    def move_object(self, src_container, src_object, dst_container,
530
                             dst_object, meta={}, account=None,
531
                             content_type=None, **headers):
532
        """moves an object"""
533
        account = account or self.account
534
        return self._change_obj_location(src_container, src_object,
535
                                         dst_container, dst_object,
536
                                         account=account, remove=True,
537
                                         meta=meta, content_type=content_type,
538
                                         **headers)
539
    
540
    def delete_object(self, container, object, params={}, account=None):
541
        """deletes an object"""
542
        account = account or self.account
543
        return self.delete('/%s/%s/%s' % (account, container, object),
544
                           params=params)
545
    
546
    def retrieve_object_metadata(self, container, object, restricted=False,
547
                                 version=None, account=None):
548
        """
549
        set restricted to True to get only user defined metadata
550
        """
551
        account = account or self.account
552
        path = '/%s/%s/%s' % (account, container, object)
553
        prefix = 'x-object-meta-' if restricted else None
554
        params = {'version':version} if version else {}
555
        return self._get_metadata(path, prefix, params=params)
556
    
557
    def update_object_metadata(self, container, object, account=None,
558
                               **meta):
559
        """
560
        updates object's metadata
561
        """
562
        account = account or self.account
563
        path = '/%s/%s/%s' % (account, container, object)
564
        return self._update_metadata(path, 'object', **meta)
565
    
566
    def delete_object_metadata(self, container, object, meta=[], account=None):
567
        """
568
        deletes object's metadata
569
        """
570
        account = account or self.account
571
        path = '/%s/%s' % (account, container, object)
572
        return self._delete_metadata(path, 'object', meta)
573
    
574
class Pithos_Client(OOS_Client):
575
    """Pithos Storage Client. Extends OOS_Client"""
576
    
577
    def _update_metadata(self, path, entity, **meta):
578
        """
579
        adds new and updates the values of previously set metadata
580
        """
581
        params = {'update':None}
582
        headers = {}
583
        prefix = 'x-%s-meta-' % entity
584
        for k,v in meta.items():
585
            k = '%s%s' % (prefix, k)
586
            headers[k] = v
587
        return self.post(path, headers=headers, params=params)
588
    
589
    def _delete_metadata(self, path, entity, meta=[]):
590
        """
591
        delete previously set metadata
592
        """
593
        params = {'update':None}
594
        headers = {}
595
        prefix = 'x-%s-meta-' % entity
596
        for m in meta:
597
            headers['%s%s' % (prefix, m)] = ''
598
        return self.post(path, headers=headers, params=params)
599
    
600
    # Storage Account Services
601
    
602
    def list_containers(self, format='text', if_modified_since=None,
603
                        if_unmodified_since=None, limit=None, marker=None,
604
                        shared=False, until=None, account=None):
605
        """returns a list with the account containers"""
606
        account = account or self.account
607
        params = {'until':until} if until else {}
608
        if shared:
609
            params['shared'] = None
610
        headers = {'if-modified-since':if_modified_since,
611
                   'if-unmodified-since':if_unmodified_since}
612
        return OOS_Client.list_containers(self, account=account, format=format,
613
                                          limit=limit, marker=marker,
614
                                          params=params, **headers)
615
    
616
    def retrieve_account_metadata(self, restricted=False, until=None,
617
                                  account=None):
618
        """returns the account metadata"""
619
        account = account or self.account
620
        params = {'until':until} if until else {}
621
        return OOS_Client.retrieve_account_metadata(self, account=account,
622
                                                    restricted=restricted,
623
                                                    **params)
624
    
625
    def set_account_groups(self, account=None, **groups):
626
        """create account groups"""
627
        account = account or self.account
628
        path = '/%s' % account
629
        headers = {}
630
        for k, v in groups.items():
631
            headers['x-account-group-%s' % k] = v
632
        params = {'update':None}
633
        return self.post(path, headers=headers, params=params)
634
    
635
    def retrieve_account_groups(self, account=None):
636
        """returns the account groups"""
637
        account = account or self.account
638
        meta = self.retrieve_account_metadata(account=account)
639
        prefix = 'x-account-group-'
640
        prefixlen = len(prefix)
641
        groups = {}
642
        for key, val in meta.items():
643
            if prefix and not key.startswith(prefix):
644
                continue
645
            elif prefix and key.startswith(prefix):
646
                key = key[prefixlen:]
647
            groups[key] = val
648
        return groups
649
    
650
    def unset_account_groups(self, groups=[], account=None):
651
        """delete account groups"""
652
        account = account or self.account
653
        path = '/%s' % account
654
        headers = {}
655
        for elem in groups:
656
            headers['x-account-group-%s' % elem] = ''
657
        params = {'update':None}
658
        return self.post(path, headers=headers, params=params)
659
    
660
    def reset_account_groups(self, account=None, **groups):
661
        """overrides account groups"""
662
        account = account or self.account
663
        path = '/%s' % account
664
        headers = {}
665
        for k, v in groups.items():
666
            v = v.strip()
667
            headers['x-account-group-%s' % k] = v
668
        meta = self.retrieve_account_metadata(restricted=True)
669
        prefix = 'x-account-meta-'
670
        for k,v in meta.items():
671
            k = '%s%s' % (prefix, k)
672
            headers[k] = v
673
        return self.post(path, headers=headers)
674
    
675
    # Storage Container Services
676
    
677
    def list_objects(self, container, format='text',
678
                     limit=None, marker=None, prefix=None, delimiter=None,
679
                     path=None, shared=False, include_trashed=False, params={},
680
                     if_modified_since=None, if_unmodified_since=None, meta='',
681
                     until=None, account=None):
682
        """returns a list with the container objects"""
683
        account = account or self.account
684
        params = {'until':until, 'meta':meta}
685
        if shared:
686
            params['shared'] = None
687
        args = locals().copy()
688
        for elem in ['self', 'container', 'params', 'until', 'meta']:
689
            args.pop(elem)
690
        return OOS_Client.list_objects(self, container, params=params, **args)
691
    
692
    def retrieve_container_metadata(self, container, restricted=False,
693
                                    until=None, account=None):
694
        """returns container's metadata"""
695
        account = account or self.account
696
        params = {'until':until} if until else {}
697
        return OOS_Client.retrieve_container_metadata(self, container,
698
                                                      account=account,
699
                                                      restricted=restricted,
700
                                                      **params)
701
    
702
    def set_container_policies(self, container, account=None,
703
                               **policies):
704
        """sets containers policies"""
705
        account = account or self.account
706
        path = '/%s/%s' % (account, container)
707
        headers = {}
708
        for key, val in policies.items():
709
            headers['x-container-policy-%s' % key] = val
710
        return self.post(path, headers=headers)
711
    
712
    def delete_container(self, container, until=None, account=None):
713
        """deletes a container or the container history until the date provided"""
714
        account = account or self.account
715
        params = {'until':until} if until else {}
716
        return OOS_Client.delete_container(self, container, account=account,
717
                                           params=params)
718
    
719
    # Storage Object Services
720
    
721
    def retrieve_object(self, container, object, params={}, format='text',
722
                        range=None, if_range=None,
723
                        if_match=None, if_none_match=None,
724
                        if_modified_since=None, if_unmodified_since=None,
725
                        account=None, **headers):
726
        """returns an object"""
727
        account = account or self.account
728
        headers={}
729
        l = ['range', 'if_range', 'if_match', 'if_none_match',
730
             'if_modified_since', 'if_unmodified_since']
731
        l = [elem for elem in l if eval(elem)]
732
        for elem in l:
733
            headers.update({elem:eval(elem)})
734
        if format != 'text':
735
            params['hashmap'] = None
736
        return OOS_Client.retrieve_object(self, container, object,
737
                                          account=account, format=format,
738
                                          params=params, **headers)
739
    
740
    def retrieve_object_version(self, container, object, version,
741
                                format='text', range=None, if_range=None,
742
                                if_match=None, if_none_match=None,
743
                                if_modified_since=None, if_unmodified_since=None,
744
                                account=None):
745
        """returns a specific object version"""
746
        account = account or self.account
747
        args = locals().copy()
748
        l = ['self', 'container', 'object']
749
        for elem in l:
750
            args.pop(elem)
751
        params = {'version':version}
752
        return self.retrieve_object(container, object, params=params, **args)
753
    
754
    def retrieve_object_versionlist(self, container, object, range=None,
755
                                    if_range=None, if_match=None,
756
                                    if_none_match=None, if_modified_since=None,
757
                                    if_unmodified_since=None, account=None):
758
        """returns the object version list"""
759
        account = account or self.account
760
        args = locals().copy()
761
        l = ['self', 'container', 'object']
762
        for elem in l:
763
            args.pop(elem)
764
        
765
        return self.retrieve_object_version(container, object, version='list',
766
                                            format='json', **args)
767
    
768
    def create_zero_length_object(self, container, object,
769
                                  meta={}, etag=None, content_type=None,
770
                                  content_encoding=None,
771
                                  content_disposition=None,
772
                                  x_object_manifest=None, x_object_sharing=None,
773
                                  x_object_public=None, account=None):
774
        """createas a zero length object"""
775
        account = account or self.account
776
        args = locals().copy()
777
        for elem in ['self', 'container', 'object']:
778
            args.pop(elem)
779
        return OOS_Client.create_zero_length_object(self, container, object,
780
                                                    **args)
781
    
782
    def create_object(self, container, object, f=stdin, format='text',
783
                      meta={}, params={}, etag=None, content_type=None,
784
                      content_encoding=None, content_disposition=None,
785
                      x_object_manifest=None, x_object_sharing=None,
786
                      x_object_public=None, account=None):
787
        """creates an object"""
788
        account = account or self.account
789
        args = locals().copy()
790
        for elem in ['self', 'container', 'object']:
791
            args.pop(elem)
792
        return OOS_Client.create_object(self, container, object, **args)
793
        
794
    def create_object_using_chunks(self, container, object,
795
                                   f=stdin, blocksize=1024, meta={}, etag=None,
796
                                   content_type=None, content_encoding=None,
797
                                   content_disposition=None,
798
                                   x_object_sharing=None, x_object_manifest=None,
799
                                   x_object_public=None, account=None):
800
        """creates an object (incremental upload)"""
801
        account = account or self.account
802
        path = '/%s/%s/%s' % (account, container, object)
803
        headers = {}
804
        l = ['etag', 'content_type', 'content_encoding', 'content_disposition', 
805
             'x_object_sharing', 'x_object_manifest', 'x_object_public']
806
        l = [elem for elem in l if eval(elem)]
807
        for elem in l:
808
            headers.update({elem:eval(elem)})
809
        headers.setdefault('content-type', 'application/octet-stream')
810
        
811
        for k,v in meta.items():
812
            v = v.strip()
813
            headers['x-object-meta-%s' %k.strip()] = v
814
        
815
        return self._chunked_transfer(path, 'PUT', f, headers=headers,
816
                                      blocksize=blocksize)
817
    
818
    def create_object_by_hashmap(self, container, object, hashmap={},
819
                                 meta={}, etag=None, content_encoding=None,
820
                                 content_disposition=None, content_type=None,
821
                                 x_object_sharing=None, x_object_manifest=None,
822
                                 x_object_public = None, account=None):
823
        """creates an object by uploading hashes representing data instead of data"""
824
        account = account or self.account
825
        args = locals().copy()
826
        for elem in ['self', 'container', 'object', 'hashmap']:
827
            args.pop(elem)
828
            
829
        try:
830
            data = json.dumps(hashmap)
831
        except SyntaxError:
832
            raise Fault('Invalid formatting')
833
        args['params'] = {'hashmap':None}
834
        args['format'] = 'json'
835
        
836
        return self.create_object(container, object, f=StringIO(data), **args)
837
    
838
    def create_manifestation(self, container, object, manifest, account=None):
839
        """creates a manifestation"""
840
        account = account or self.account
841
        headers={'x_object_manifest':manifest}
842
        return self.create_object(container, object, f=None, account=account,
843
                                  **headers)
844
    
845
    def update_object(self, container, object, f=stdin,
846
                      offset=None, meta={}, replace=False, content_length=None,
847
                      content_type=None, content_range=None,
848
                      content_encoding=None, content_disposition=None,
849
                      x_object_bytes=None, x_object_manifest=None,
850
                      x_object_sharing=None, x_object_public=None,
851
                      x_source_object=None, account=None):
852
        """updates an object"""
853
        account = account or self.account
854
        args = locals().copy()
855
        for elem in ['self', 'container', 'object', 'replace']:
856
            args.pop(elem)
857
        if not replace:
858
            args['params'] = {'update':None}
859
        return OOS_Client.update_object(self, container, object, **args)
860
    
861
    def update_object_using_chunks(self, container, object, f=stdin,
862
                                   blocksize=1024, offset=None, meta={},
863
                                   replace=False, content_type=None, content_encoding=None,
864
                                   content_disposition=None, x_object_bytes=None,
865
                                   x_object_manifest=None, x_object_sharing=None,
866
                                   x_object_public=None, account=None):
867
        """updates an object (incremental upload)"""
868
        account = account or self.account
869
        args = locals().copy()
870
        for elem in ['self', 'container', 'object', 'replace']:
871
            args.pop(elem)
872
        if not replace:
873
            args['params'] = {'update':None}
874
        return OOS_Client.update_object_using_chunks(self, container, object, **args)
875
    
876
    def update_from_other_source(self, container, object, source,
877
                      offset=None, meta={}, content_range=None,
878
                      content_encoding=None, content_disposition=None,
879
                      x_object_bytes=None, x_object_manifest=None,
880
                      x_object_sharing=None, x_object_public=None, account=None):
881
        """updates an object"""
882
        account = account or self.account
883
        args = locals().copy()
884
        for elem in ['self', 'container', 'object', 'source']:
885
            args.pop(elem)
886
        
887
        args['x_source_object'] = source
888
        return self.update_object(container, object, f=None, **args)
889
    
890
    def delete_object(self, container, object, until=None, account=None):
891
        """deletes an object or the object history until the date provided"""
892
        account = account or self.account
893
        params = {'until':until} if until else {}
894
        return OOS_Client.delete_object(self, container, object, params, account)
895
    
896
    def trash_object(self, container, object):
897
        """trashes an object"""
898
        account = account or self.account
899
        path = '/%s/%s' % (container, object)
900
        meta = {'trash':'true'}
901
        return self._update_metadata(path, 'object', **meta)
902
    
903
    def restore_object(self, container, object, account=None):
904
        """restores a trashed object"""
905
        account = account or self.account
906
        return self.delete_object_metadata(container, object, account, ['trash'])
907
    
908
    def publish_object(self, container, object, account=None):
909
        """sets a previously created object publicly accessible"""
910
        account = account or self.account
911
        path = '/%s/%s/%s' % (account, container, object)
912
        headers = {}
913
        headers['x_object_public'] = True
914
        params = {'update':None}
915
        return self.post(path, headers=headers, params=params)
916
    
917
    def unpublish_object(self, container, object, account=None):
918
        """unpublish an object"""
919
        account = account or self.account
920
        path = '/%s/%s/%s' % (account, container, object)
921
        headers = {}
922
        headers['x_object_public'] = False
923
        params = {'update':None}
924
        return self.post(path, headers=headers, params=params)
925
    
926
    def copy_object(self, src_container, src_object, dst_container, dst_object,
927
                    meta={}, public=False, version=None, account=None,
928
                    content_type=None):
929
        """copies an object"""
930
        account = account or self.account
931
        headers = {}
932
        headers['x_object_public'] = public
933
        if version:
934
            headers['x_object_version'] = version
935
        return OOS_Client.copy_object(self, src_container, src_object,
936
                                      dst_container, dst_object, meta=meta,
937
                                      account=account, content_type=content_type,
938
                                      **headers)
939
    
940
    def move_object(self, src_container, src_object, dst_container,
941
                             dst_object, meta={}, public=False, version=None,
942
                             account=None, content_type=None):
943
        """moves an object"""
944
        headers = {}
945
        headers['x_object_public'] = public
946
        if version:
947
            headers['x_object_version'] = version
948
        return OOS_Client.move_object(self, src_container, src_object,
949
                                      dst_container, dst_object, meta=meta,
950
                                      account=account, content_type=content_type,
951
                                      **headers)
952
    
953
    def list_shared_by_others(self, limit=None, marker=None, format='text'):
954
        """lists other accounts that share objects to the user"""
955
        l = ['limit', 'marker']
956
        params = {}
957
        for elem in [elem for elem in l if eval(elem)]:
958
            params[elem] = eval(elem)
959
        return self._list('', format, params)
960
    
961
    def share_object(self, container, object, l, read=True):
962
        """gives access(read by default) to an object to a user/group list"""
963
        action = 'read' if read else 'write'
964
        sharing = '%s=%s' % (action, ','.join(l))
965
        self.update_object(container, object, f=None, x_object_sharing=sharing)
966

    
967
def _encode_headers(headers):
968
    h = {}
969
    for k, v in headers.items():
970
        k = urllib.quote(k)
971
        if v and type(v) == types.StringType:
972
            v = urllib.quote(v, '/=,-* :"')
973
        h[k] = v
974
    return h