Statistics
| Branch: | Tag: | Revision:

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

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

    
954
def _prepare_path(path, format='text', params={}):
955
    full_path = '%s?format=%s' % (quote(path), format)
956
    
957
    for k,v in params.items():
958
        value = quote(str(v)) if v else ''
959
        full_path = '%s&%s=%s' %(full_path, quote(k), value)
960
    return full_path
961

    
962
def _prepare_headers(headers):
963
    for k,v in headers.items():
964
        headers.pop(k)
965
        k = k.replace('_', '-')
966
        headers[quote(k)] = quote(v, safe='/=,:@ *"') if type(v) == types.StringType else v
967
    return headers
968

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