Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-tools / pithos / tools / lib / client.py @ 2535deff

History | View | Annotate | Download (41.3 kB)

1
# Copyright 2011-2012 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, HTTPSConnection, HTTP
35
from sys import stdin
36
from xml.dom import minidom
37
from StringIO import StringIO
38
from urllib import quote, unquote
39
from urlparse import urlparse
40

    
41
import json
42
import types
43
import socket
44
import urllib
45
import datetime
46

    
47
ERROR_CODES = {304:'Not Modified',
48
               400:'Bad Request',
49
               401:'Unauthorized',
50
               403:'Forbidden',
51
               404:'Not Found',
52
               409:'Conflict',
53
               411:'Length Required',
54
               412:'Precondition Failed',
55
               413:'Request Entity Too Large',
56
               416:'Range Not Satisfiable',
57
               422:'Unprocessable Entity',
58
               500:'Internal Server Error',
59
               501:'Not Implemented'}
60

    
61
class Fault(Exception):
62
    def __init__(self, data='', status=None):
63
        if data == '' and status in ERROR_CODES.keys():
64
            data = ERROR_CODES[status]
65
        Exception.__init__(self, data)
66
        self.data = data
67
        self.status = status
68

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

    
948
def _prepare_path(path, format='text', params={}):
949
    full_path = '%s?format=%s' % (quote(path), format)
950
    
951
    for k,v in params.items():
952
        value = quote(str(v)) if v else ''
953
        full_path = '%s&%s=%s' %(full_path, quote(k), value)
954
    return full_path
955

    
956
def _prepare_headers(headers):
957
    for k,v in headers.items():
958
        headers.pop(k)
959
        k = k.replace('_', '-')
960
        headers[quote(k)] = quote(v, safe='/=,:@ *"') if type(v) == types.StringType else v
961
    return headers
962

    
963
def _handle_response(response, verbose=False, debug=False):
964
    headers = response.getheaders()
965
    headers = dict((unquote(h), unquote(v)) for h,v in headers)
966
    
967
    if verbose:
968
        print '%d %s' % (response.status, response.reason)
969
        for key, val in headers.items():
970
            print '%s: %s' % (key.capitalize(), val)
971
        print
972
    
973
    length = response.getheader('content-length', None)
974
    data = response.read(length)
975
    if debug:
976
        print data
977
        print
978
    
979
    if int(response.status) in ERROR_CODES.keys():
980
        raise Fault(data, int(response.status))
981
    
982
    #print '**',  response.status, headers, data, '\n'
983
    return response.status, headers, data