Statistics
| Branch: | Tag: | Revision:

root / tools / lib / client.py @ 908c6f81

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

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