Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-tools / pithos / tools / lib / client.py @ ca3ab617

History | View | Annotate | Download (41.1 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):
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
        headers = {'if-modified-since':if_modified_since,
572
                   'if-unmodified-since':if_unmodified_since}
573
        return OOS_Client.list_containers(self, account=account, format=format,
574
                                          limit=limit, marker=marker,
575
                                          params=params, **headers)
576
    
577
    def retrieve_account_metadata(self, restricted=False, until=None,
578
                                  account=None):
579
        """returns the account metadata"""
580
        account = account or self.account
581
        params = {'until':until} if until else {}
582
        return OOS_Client.retrieve_account_metadata(self, account=account,
583
                                                    restricted=restricted,
584
                                                    **params)
585
    
586
    def set_account_groups(self, account=None, **groups):
587
        """create account groups"""
588
        account = account or self.account
589
        path = '/%s' % account
590
        headers = {}
591
        for k, v in groups.items():
592
            headers['x-account-group-%s' % k] = v
593
        params = {'update':None}
594
        return self.post(path, headers=headers, params=params)
595
    
596
    def retrieve_account_groups(self, account=None):
597
        """returns the account groups"""
598
        account = account or self.account
599
        meta = self.retrieve_account_metadata(account=account)
600
        prefix = 'x-account-group-'
601
        prefixlen = len(prefix)
602
        groups = {}
603
        for key, val in meta.items():
604
            if prefix and not key.startswith(prefix):
605
                continue
606
            elif prefix and key.startswith(prefix):
607
                key = key[prefixlen:]
608
            groups[key] = val
609
        return groups
610
    
611
    def unset_account_groups(self, groups=[], account=None):
612
        """delete account groups"""
613
        account = account or self.account
614
        path = '/%s' % account
615
        headers = {}
616
        for elem in groups:
617
            headers['x-account-group-%s' % elem] = ''
618
        params = {'update':None}
619
        return self.post(path, headers=headers, params=params)
620
    
621
    def reset_account_groups(self, account=None, **groups):
622
        """overrides account groups"""
623
        account = account or self.account
624
        path = '/%s' % account
625
        headers = {}
626
        for k, v in groups.items():
627
            v = v.strip()
628
            headers['x-account-group-%s' % k] = v
629
        meta = self.retrieve_account_metadata(restricted=True)
630
        prefix = 'x-account-meta-'
631
        for k,v in meta.items():
632
            k = '%s%s' % (prefix, k)
633
            headers[k] = v
634
        return self.post(path, headers=headers)
635
    
636
    # Storage Container Services
637
    def create_container(self, container, account=None, meta={}, policies={}):
638
        """creates a container"""
639
        args = {}
640
        for k, v in policies.items():
641
            args['X-Container-Policy-%s' % k.capitalize()] = v
642
        return OOS_Client.create_container(self, container, account, meta, **args)
643
    
644
    def list_objects(self, container, format='text',
645
                     limit=None, marker=None, prefix=None, delimiter=None,
646
                     path=None, shared=False, include_trashed=False, params={},
647
                     if_modified_since=None, if_unmodified_since=None, meta='',
648
                     until=None, account=None):
649
        """returns a list with the container objects"""
650
        account = account or self.account
651
        params = {'until':until, 'meta':meta}
652
        if shared:
653
            params['shared'] = None
654
        args = locals().copy()
655
        for elem in ['self', 'container', 'params', 'until', 'meta']:
656
            args.pop(elem)
657
        return OOS_Client.list_objects(self, container, params=params, **args)
658
    
659
    def retrieve_container_metadata(self, container, restricted=False,
660
                                    until=None, account=None):
661
        """returns container's metadata"""
662
        account = account or self.account
663
        params = {'until':until} if until else {}
664
        return OOS_Client.retrieve_container_metadata(self, container,
665
                                                      account=account,
666
                                                      restricted=restricted,
667
                                                      **params)
668
    
669
    def set_container_policies(self, container, account=None,
670
                               **policies):
671
        """sets containers policies"""
672
        account = account or self.account
673
        path = '/%s/%s' % (account, container)
674
        headers = {}
675
        for key, val in policies.items():
676
            headers['x-container-policy-%s' % key] = val
677
        return self.post(path, headers=headers)
678
    
679
    def update_container_data(self, container, f=stdin):
680
        """adds blocks of data to the container"""
681
        account = self.account
682
        path = '/%s/%s' % (account, container)
683
        params = {'update': None}
684
        headers = {'content_type': 'application/octet-stream'}
685
        data = f.read() if f else None
686
        headers['content_length'] = len(data)
687
        return self.post(path, data, headers=headers, params=params)
688
    
689
    def delete_container(self, container, until=None, account=None):
690
        """deletes a container or the container history until the date provided"""
691
        account = account or self.account
692
        params = {'until':until} if until else {}
693
        return OOS_Client.delete_container(self, container, account=account,
694
                                           params=params)
695
    
696
    # Storage Object Services
697
    
698
    def retrieve_object(self, container, object, params={}, format='text',
699
                        range=None, if_range=None,
700
                        if_match=None, if_none_match=None,
701
                        if_modified_since=None, if_unmodified_since=None,
702
                        account=None, **headers):
703
        """returns an object"""
704
        account = account or self.account
705
        headers={}
706
        l = ['range', 'if_range', 'if_match', 'if_none_match',
707
             'if_modified_since', 'if_unmodified_since']
708
        l = [elem for elem in l if eval(elem)]
709
        for elem in l:
710
            headers.update({elem:eval(elem)})
711
        if format != 'text':
712
            params['hashmap'] = None
713
        return OOS_Client.retrieve_object(self, container, object,
714
                                          account=account, format=format,
715
                                          params=params, **headers)
716
    
717
    def retrieve_object_version(self, container, object, version,
718
                                format='text', range=None, if_range=None,
719
                                if_match=None, if_none_match=None,
720
                                if_modified_since=None, if_unmodified_since=None,
721
                                account=None):
722
        """returns a specific object version"""
723
        account = account or self.account
724
        args = locals().copy()
725
        l = ['self', 'container', 'object']
726
        for elem in l:
727
            args.pop(elem)
728
        params = {'version':version}
729
        return self.retrieve_object(container, object, params=params, **args)
730
    
731
    def retrieve_object_versionlist(self, container, object, range=None,
732
                                    if_range=None, if_match=None,
733
                                    if_none_match=None, if_modified_since=None,
734
                                    if_unmodified_since=None, account=None):
735
        """returns the object version list"""
736
        account = account or self.account
737
        args = locals().copy()
738
        l = ['self', 'container', 'object']
739
        for elem in l:
740
            args.pop(elem)
741
        
742
        return self.retrieve_object_version(container, object, version='list',
743
                                            format='json', **args)
744
    
745
    def create_zero_length_object(self, container, object,
746
                                  meta={}, etag=None, content_type=None,
747
                                  content_encoding=None,
748
                                  content_disposition=None,
749
                                  x_object_manifest=None, x_object_sharing=None,
750
                                  x_object_public=None, account=None):
751
        """createas a zero length object"""
752
        account = account or self.account
753
        args = locals().copy()
754
        for elem in ['self', 'container', 'object']:
755
            args.pop(elem)
756
        return OOS_Client.create_zero_length_object(self, container, object,
757
                                                    **args)
758
    
759
    def create_object(self, container, object, f=stdin, format='text',
760
                      meta={}, params={}, etag=None, content_type=None,
761
                      content_encoding=None, content_disposition=None,
762
                      x_object_manifest=None, x_object_sharing=None,
763
                      x_object_public=None, account=None):
764
        """creates an object"""
765
        account = account or self.account
766
        args = locals().copy()
767
        for elem in ['self', 'container', 'object']:
768
            args.pop(elem)
769
        if format != 'text':
770
            params.update({'hashmap':None})
771
        return OOS_Client.create_object(self, container, object, **args)
772
        
773
    def create_object_using_chunks(self, container, object,
774
                                   f=stdin, blocksize=1024, meta={}, etag=None,
775
                                   content_type=None, content_encoding=None,
776
                                   content_disposition=None,
777
                                   x_object_sharing=None, x_object_manifest=None,
778
                                   x_object_public=None, account=None):
779
        """creates an object (incremental upload)"""
780
        account = account or self.account
781
        path = '/%s/%s/%s' % (account, container, object)
782
        headers = {}
783
        l = ['etag', 'content_type', 'content_encoding', 'content_disposition', 
784
             'x_object_sharing', 'x_object_manifest', 'x_object_public']
785
        l = [elem for elem in l if eval(elem)]
786
        for elem in l:
787
            headers.update({elem:eval(elem)})
788
        headers.setdefault('content-type', 'application/octet-stream')
789
        
790
        for k,v in meta.items():
791
            v = v.strip()
792
            headers['x-object-meta-%s' %k.strip()] = v
793
        
794
        return self._chunked_transfer(path, 'PUT', f, headers=headers,
795
                                      blocksize=blocksize)
796
    
797
    def create_object_by_hashmap(self, container, object, hashmap={},
798
                                 meta={}, etag=None, content_encoding=None,
799
                                 content_disposition=None, content_type=None,
800
                                 x_object_sharing=None, x_object_manifest=None,
801
                                 x_object_public = None, account=None):
802
        """creates an object by uploading hashes representing data instead of data"""
803
        account = account or self.account
804
        args = locals().copy()
805
        for elem in ['self', 'container', 'object', 'hashmap']:
806
            args.pop(elem)
807
            
808
        try:
809
            data = json.dumps(hashmap)
810
        except SyntaxError:
811
            raise Fault('Invalid formatting')
812
        args['params'] = {'hashmap':None}
813
        args['format'] = 'json'
814
        
815
        return self.create_object(container, object, f=StringIO(data), **args)
816
    
817
    def create_manifestation(self, container, object, manifest, account=None):
818
        """creates a manifestation"""
819
        account = account or self.account
820
        headers={'x_object_manifest':manifest}
821
        return self.create_object(container, object, f=None, account=account,
822
                                  **headers)
823
    
824
    def update_object(self, container, object, f=stdin,
825
                      offset=None, meta={}, replace=False, content_length=None,
826
                      content_type=None, content_range=None,
827
                      content_encoding=None, content_disposition=None,
828
                      x_object_bytes=None, x_object_manifest=None,
829
                      x_object_sharing=None, x_object_public=None,
830
                      x_source_object=None, account=None):
831
        """updates an object"""
832
        account = account or self.account
833
        args = locals().copy()
834
        for elem in ['self', 'container', 'object', 'replace']:
835
            args.pop(elem)
836
        if not replace:
837
            args['params'] = {'update':None}
838
        return OOS_Client.update_object(self, container, object, **args)
839
    
840
    def update_object_using_chunks(self, container, object, f=stdin,
841
                                   blocksize=1024, offset=None, meta={},
842
                                   replace=False, content_type=None, content_encoding=None,
843
                                   content_disposition=None, x_object_bytes=None,
844
                                   x_object_manifest=None, x_object_sharing=None,
845
                                   x_object_public=None, account=None):
846
        """updates an object (incremental upload)"""
847
        account = account or self.account
848
        args = locals().copy()
849
        for elem in ['self', 'container', 'object', 'replace']:
850
            args.pop(elem)
851
        if not replace:
852
            args['params'] = {'update':None}
853
        return OOS_Client.update_object_using_chunks(self, container, object, **args)
854
    
855
    def update_from_other_source(self, container, object, source,
856
                      offset=None, meta={}, content_range=None,
857
                      content_encoding=None, content_disposition=None,
858
                      x_object_bytes=None, x_object_manifest=None,
859
                      x_object_sharing=None, x_object_public=None, account=None):
860
        """updates an object"""
861
        account = account or self.account
862
        args = locals().copy()
863
        for elem in ['self', 'container', 'object', 'source']:
864
            args.pop(elem)
865
        
866
        args['x_source_object'] = source
867
        return self.update_object(container, object, f=None, **args)
868
    
869
    def delete_object(self, container, object, until=None, account=None):
870
        """deletes an object or the object history until the date provided"""
871
        account = account or self.account
872
        params = {'until':until} if until else {}
873
        return OOS_Client.delete_object(self, container, object, params, account)
874
    
875
    def trash_object(self, container, object):
876
        """trashes an object"""
877
        account = account or self.account
878
        path = '/%s/%s' % (container, object)
879
        meta = {'trash':'true'}
880
        return self._update_metadata(path, 'object', **meta)
881
    
882
    def restore_object(self, container, object, account=None):
883
        """restores a trashed object"""
884
        account = account or self.account
885
        return self.delete_object_metadata(container, object, account, ['trash'])
886
    
887
    def publish_object(self, container, object, account=None):
888
        """sets a previously created object publicly accessible"""
889
        account = account or self.account
890
        path = '/%s/%s/%s' % (account, container, object)
891
        headers = {}
892
        headers['x_object_public'] = True
893
        params = {'update':None}
894
        return self.post(path, headers=headers, params=params)
895
    
896
    def unpublish_object(self, container, object, account=None):
897
        """unpublish an object"""
898
        account = account or self.account
899
        path = '/%s/%s/%s' % (account, container, object)
900
        headers = {}
901
        headers['x_object_public'] = False
902
        params = {'update':None}
903
        return self.post(path, headers=headers, params=params)
904
    
905
    def copy_object(self, src_container, src_object, dst_container, dst_object,
906
                    meta={}, public=False, version=None, account=None,
907
                    content_type=None):
908
        """copies an object"""
909
        account = account or self.account
910
        headers = {}
911
        headers['x_object_public'] = public
912
        if version:
913
            headers['x_source_version'] = version
914
        return OOS_Client.copy_object(self, src_container, src_object,
915
                                      dst_container, dst_object, meta=meta,
916
                                      account=account, content_type=content_type,
917
                                      **headers)
918
    
919
    def move_object(self, src_container, src_object, dst_container,
920
                             dst_object, meta={}, public=False,
921
                             account=None, content_type=None):
922
        """moves an object"""
923
        headers = {}
924
        headers['x_object_public'] = public
925
        return OOS_Client.move_object(self, src_container, src_object,
926
                                      dst_container, dst_object, meta=meta,
927
                                      account=account, content_type=content_type,
928
                                      **headers)
929
    
930
    def list_shared_by_others(self, limit=None, marker=None, format='text'):
931
        """lists other accounts that share objects to the user"""
932
        l = ['limit', 'marker']
933
        params = {}
934
        for elem in [elem for elem in l if eval(elem)]:
935
            params[elem] = eval(elem)
936
        return self._list('', format, params)
937
    
938
    def share_object(self, container, object, l, read=True):
939
        """gives access(read by default) to an object to a user/group list"""
940
        action = 'read' if read else 'write'
941
        sharing = '%s=%s' % (action, ','.join(l))
942
        self.update_object(container, object, f=None, x_object_sharing=sharing)
943

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

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

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