Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-tools / pithos / tools / lib / client.py @ 2715ade4

History | View | Annotate | Download (41.8 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

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

    
70

    
71
class Client(object):
72
    def __init__(self, url, token, account, verbose=False, debug=False):
73
        """`url` can also include a port, e.g '127.0.0.1:8000'."""
74

    
75
        self.url = url
76
        self.account = account
77
        self.verbose = verbose or debug
78
        self.debug = debug
79
        self.token = token
80

    
81
    def _req(self, method, path, body=None, headers={}, format='text', params={}):
82
        p = urlparse(self.url)
83
        if p.scheme == 'http':
84
            conn = HTTPConnection(p.netloc)
85
        elif p.scheme == 'https':
86
            conn = HTTPSConnection(p.netloc)
87
        else:
88
            raise Exception('Unknown URL scheme')
89

    
90
        full_path = _prepare_path(p.path + path, format, params)
91

    
92
        kwargs = {}
93
        kwargs['headers'] = _prepare_headers(headers)
94
        kwargs['headers']['X-Auth-Token'] = self.token
95
        if body:
96
            kwargs['body'] = body
97
            kwargs['headers'].setdefault(
98
                'content-type', 'application/octet-stream')
99
        kwargs['headers'].setdefault('content-length', len(body)
100
                                     if body else 0)
101

    
102
        #print '#', method, full_path, kwargs
103
        #t1 = datetime.datetime.utcnow()
104
        conn.request(method, full_path, **kwargs)
105

    
106
        resp = conn.getresponse()
107
        #t2 = datetime.datetime.utcnow()
108
        #print 'response time:', str(t2-t1)
109
        return _handle_response(resp, self.verbose, self.debug)
110

    
111
    def _chunked_transfer(self, path, method='PUT', f=stdin, headers=None,
112
                          blocksize=1024, params={}):
113
        """perfomrs a chunked request"""
114
        p = urlparse(self.url)
115
        if p.scheme == 'http':
116
            conn = HTTPConnection(p.netloc)
117
        elif p.scheme == 'https':
118
            conn = HTTPSConnection(p.netloc)
119
        else:
120
            raise Exception('Unknown URL scheme')
121

    
122
        full_path = _prepare_path(p.path + path, params=params)
123

    
124
        headers.setdefault('content-type', 'application/octet-stream')
125

    
126
        conn.putrequest(method, full_path)
127
        conn.putheader('x-auth-token', self.token)
128
        conn.putheader('transfer-encoding', 'chunked')
129
        for k, v in _prepare_headers(headers).items():
130
            conn.putheader(k, v)
131
        conn.endheaders()
132

    
133
        # write body
134
        data = ''
135
        while True:
136
            if f.closed:
137
                break
138
            block = f.read(blocksize)
139
            if block == '':
140
                break
141
            data = '%x\r\n%s\r\n' % (len(block), block)
142
            try:
143
                conn.send(data)
144
            except:
145
                #retry
146
                conn.send(data)
147
        data = '0\r\n\r\n'
148
        try:
149
            conn.send(data)
150
        except:
151
            #retry
152
            conn.send(data)
153

    
154
        resp = conn.getresponse()
155
        return _handle_response(resp, self.verbose, self.debug)
156

    
157
    def delete(self, path, format='text', params={}):
158
        return self._req('DELETE', path, format=format, params=params)
159

    
160
    def get(self, path, format='text', headers={}, params={}):
161
        return self._req('GET', path, headers=headers, format=format,
162
                         params=params)
163

    
164
    def head(self, path, format='text', params={}):
165
        return self._req('HEAD', path, format=format, params=params)
166

    
167
    def post(self, path, body=None, format='text', headers=None, params={}):
168
        return self._req('POST', path, body, headers=headers, format=format,
169
                         params=params)
170

    
171
    def put(self, path, body=None, format='text', headers=None, params={}):
172
        return self._req('PUT', path, body, headers=headers, format=format,
173
                         params=params)
174

    
175
    def _list(self, path, format='text', params={}, **headers):
176
        status, headers, data = self.get(path, format=format, headers=headers,
177
                                         params=params)
178
        if format == 'json':
179
            data = json.loads(data) if data else ''
180
        elif format == 'xml':
181
            data = minidom.parseString(data)
182
        else:
183
            data = data.split('\n')[:-1] if data else ''
184
        return data
185

    
186
    def _get_metadata(self, path, prefix=None, params={}):
187
        status, headers, data = self.head(path, params=params)
188
        prefixlen = len(prefix) if prefix else 0
189
        meta = {}
190
        for key, val in headers.items():
191
            if prefix and not key.startswith(prefix):
192
                continue
193
            elif prefix and key.startswith(prefix):
194
                key = key[prefixlen:]
195
            meta[key] = val
196
        return meta
197

    
198
    def _filter(self, l, d):
199
        """
200
        filter out from l elements having the metadata values provided
201
        """
202
        ll = l
203
        for elem in l:
204
            if isinstance(elem, types.DictionaryType):
205
                for key in d.keys():
206
                    k = 'x_object_meta_%s' % key
207
                    if k in elem.keys() and elem[k] == d[key]:
208
                        ll.remove(elem)
209
                        break
210
        return ll
211

    
212

    
213
class OOS_Client(Client):
214
    """Openstack Object Storage Client"""
215

    
216
    def _update_metadata(self, path, entity, **meta):
217
        """adds new and updates the values of previously set metadata"""
218
        ex_meta = self.retrieve_account_metadata(restricted=True)
219
        ex_meta.update(meta)
220
        headers = {}
221
        prefix = 'x-%s-meta-' % entity
222
        for k, v in ex_meta.items():
223
            k = '%s%s' % (prefix, k)
224
            headers[k] = v
225
        return self.post(path, headers=headers)
226

    
227
    def _reset_metadata(self, path, entity, **meta):
228
        """
229
        overwrites all user defined metadata
230
        """
231
        headers = {}
232
        prefix = 'x-%s-meta-' % entity
233
        for k, v in meta.items():
234
            k = '%s%s' % (prefix, k)
235
            headers[k] = v
236
        return self.post(path, headers=headers)
237

    
238
    def _delete_metadata(self, path, entity, meta=[]):
239
        """delete previously set metadata"""
240
        ex_meta = self.retrieve_account_metadata(restricted=True)
241
        headers = {}
242
        prefix = 'x-%s-meta-' % entity
243
        for k in ex_meta.keys():
244
            if k in meta:
245
                headers['%s%s' % (prefix, k)] = ex_meta[k]
246
        return self.post(path, headers=headers)
247

    
248
    # Storage Account Services
249

    
250
    def list_containers(self, format='text', limit=None,
251
                        marker=None, params={}, account=None, **headers):
252
        """lists containers"""
253
        account = account or self.account
254
        path = '/%s' % account
255
        params.update({'limit': limit, 'marker': marker})
256
        return self._list(path, format, params, **headers)
257

    
258
    def retrieve_account_metadata(self, restricted=False, account=None, **params):
259
        """returns the account metadata"""
260
        account = account or self.account
261
        path = '/%s' % account
262
        prefix = 'x-account-meta-' if restricted else None
263
        return self._get_metadata(path, prefix, params)
264

    
265
    def update_account_metadata(self, account=None, **meta):
266
        """updates the account metadata"""
267
        account = account or self.account
268
        path = '/%s' % account
269
        return self._update_metadata(path, 'account', **meta)
270

    
271
    def delete_account_metadata(self, meta=[], account=None):
272
        """deletes the account metadata"""
273
        account = account or self.account
274
        path = '/%s' % account
275
        return self._delete_metadata(path, 'account', meta)
276

    
277
    def reset_account_metadata(self, account=None, **meta):
278
        """resets account metadata"""
279
        account = account or self.account
280
        path = '/%s' % account
281
        return self._reset_metadata(path, 'account', **meta)
282

    
283
    # Storage Container Services
284

    
285
    def _filter_trashed(self, l):
286
        return self._filter(l, {'trash': 'true'})
287

    
288
    def list_objects(self, container, format='text',
289
                     limit=None, marker=None, prefix=None, delimiter=None,
290
                     path=None, include_trashed=False, params={}, account=None,
291
                     **headers):
292
        """returns a list with the container objects"""
293
        account = account or self.account
294
        params.update({'limit': limit, 'marker': marker, 'prefix': prefix,
295
                       'delimiter': delimiter, 'path': path})
296
        l = self._list('/%s/%s' % (account, container), format, params,
297
                       **headers)
298
        #TODO support filter trashed with xml also
299
        if format != 'xml' and not include_trashed:
300
            l = self._filter_trashed(l)
301
        return l
302

    
303
    def create_container(self, container, account=None, meta={}, **headers):
304
        """creates a container"""
305
        account = account or self.account
306
        if not headers:
307
            headers = {}
308
        for k, v in meta.items():
309
            headers['x-container-meta-%s' % k.strip().upper()] = v.strip()
310
        status, header, data = self.put('/%s/%s' % (account, container),
311
                                        headers=headers)
312
        if status == 202:
313
            return False
314
        elif status != 201:
315
            raise Fault(data, int(status))
316
        return True
317

    
318
    def delete_container(self, container, params={}, account=None):
319
        """deletes a container"""
320
        account = account or self.account
321
        return self.delete('/%s/%s' % (account, container), params=params)
322

    
323
    def retrieve_container_metadata(self, container, restricted=False,
324
                                    account=None, **params):
325
        """returns the container metadata"""
326
        account = account or self.account
327
        prefix = 'x-container-meta-' if restricted else None
328
        return self._get_metadata('/%s/%s' % (account, container), prefix,
329
                                  params)
330

    
331
    def update_container_metadata(self, container, account=None, **meta):
332
        """unpdates the container metadata"""
333
        account = account or self.account
334
        return self._update_metadata('/%s/%s' % (account, container),
335
                                     'container', **meta)
336

    
337
    def delete_container_metadata(self, container, meta=[], account=None):
338
        """deletes the container metadata"""
339
        account = account or self.account
340
        path = '/%s/%s' % (account, container)
341
        return self._delete_metadata(path, 'container', meta)
342

    
343
    # Storage Object Services
344

    
345
    def request_object(self, container, object, format='text', params={},
346
                       account=None, **headers):
347
        """returns tuple containing the status, headers and data response for an object request"""
348
        account = account or self.account
349
        path = '/%s/%s/%s' % (account, container, object)
350
        status, headers, data = self.get(path, format, headers, params)
351
        return status, headers, data
352

    
353
    def retrieve_object(self, container, object, format='text', params={},
354
                        account=None, **headers):
355
        """returns an object's data"""
356
        account = account or self.account
357
        t = self.request_object(container, object, format, params, account,
358
                                **headers)
359
        data = t[2]
360
        if format == 'json':
361
            data = json.loads(data) if data else ''
362
        elif format == 'xml':
363
            data = minidom.parseString(data)
364
        return data
365

    
366
    def retrieve_object_hashmap(
367
        self, container, object, format='json', params={},
368
            account=None, **headers):
369
        """returns the hashmap representing object's data"""
370
        if not params:
371
            params = {}
372
        params.update({'hashmap': None})
373
        return self.retrieve_object(container, object, params, format, account, **headers)
374

    
375
    def create_directory_marker(self, container, object, account=None):
376
        """creates a dierectory marker"""
377
        account = account or self.account
378
        if not object:
379
            raise Fault('Directory markers have to be nested in a container')
380
        h = {'content_type': 'application/directory'}
381
        return self.create_zero_length_object(
382
            container, object, account=account,
383
            **h)
384

    
385
    def create_object(self, container, object, f=stdin, format='text', meta={},
386
                      params={}, etag=None, content_type=None, content_encoding=None,
387
                      content_disposition=None, account=None, **headers):
388
        """creates a zero-length object"""
389
        account = account or self.account
390
        path = '/%s/%s/%s' % (account, container, object)
391
        for k, v in headers.items():
392
            if v is None:
393
                headers.pop(k)
394

    
395
        l = ['etag', 'content_encoding', 'content_disposition', 'content_type']
396
        l = [elem for elem in l if eval(elem)]
397
        for elem in l:
398
            headers.update({elem: eval(elem)})
399
        headers.setdefault('content-type', 'application/octet-stream')
400

    
401
        for k, v in meta.items():
402
            headers['x-object-meta-%s' % k.strip()] = v.strip()
403
        data = f.read() if f else None
404
        return self.put(path, data, format, headers=headers, params=params)
405

    
406
    def create_zero_length_object(self, container, object, meta={}, etag=None,
407
                                  content_type=None, content_encoding=None,
408
                                  content_disposition=None, account=None,
409
                                  **headers):
410
        account = account or self.account
411
        args = locals().copy()
412
        for elem in ['self', 'container', 'headers', 'account']:
413
            args.pop(elem)
414
        args.update(headers)
415
        return self.create_object(container, account=account, f=None, **args)
416

    
417
    def update_object(self, container, object, f=stdin,
418
                      offset=None, meta={}, params={}, content_length=None,
419
                      content_type=None, content_encoding=None,
420
                      content_disposition=None, account=None, **headers):
421
        account = account or self.account
422
        path = '/%s/%s/%s' % (account, container, object)
423
        for k, v in headers.items():
424
            if v is None:
425
                headers.pop(k)
426

    
427
        l = ['content_encoding', 'content_disposition', 'content_type',
428
             'content_length']
429
        l = [elem for elem in l if eval(elem)]
430
        for elem in l:
431
            headers.update({elem: eval(elem)})
432

    
433
        if 'content_range' not in headers.keys():
434
            if offset is not None:
435
                headers['content_range'] = 'bytes %s-/*' % offset
436
            else:
437
                headers['content_range'] = 'bytes */*'
438

    
439
        for k, v in meta.items():
440
            headers['x-object-meta-%s' % k.strip()] = v.strip()
441
        data = f.read() if f else None
442
        return self.post(path, data, headers=headers, params=params)
443

    
444
    def update_object_using_chunks(self, container, object, f=stdin,
445
                                   blocksize=1024, offset=None, meta={},
446
                                   params={}, content_type=None, content_encoding=None,
447
                                   content_disposition=None, account=None, **headers):
448
        """updates an object (incremental upload)"""
449
        account = account or self.account
450
        path = '/%s/%s/%s' % (account, container, object)
451
        headers = headers if not headers else {}
452
        l = ['content_type', 'content_encoding', 'content_disposition']
453
        l = [elem for elem in l if eval(elem)]
454
        for elem in l:
455
            headers.update({elem: eval(elem)})
456

    
457
        if offset is not None:
458
            headers['content_range'] = 'bytes %s-/*' % offset
459
        else:
460
            headers['content_range'] = 'bytes */*'
461

    
462
        for k, v in meta.items():
463
            v = v.strip()
464
            headers['x-object-meta-%s' % k.strip()] = v
465
        return self._chunked_transfer(path, 'POST', f, headers=headers,
466
                                      blocksize=blocksize, params=params)
467

    
468
    def _change_obj_location(self, src_container, src_object, dst_container,
469
                             dst_object, remove=False, meta={}, account=None,
470
                             content_type=None, delimiter=None, **headers):
471
        account = account or self.account
472
        path = '/%s/%s/%s' % (account, dst_container, dst_object)
473
        headers = {} if not headers else headers
474
        params = {}
475
        for k, v in meta.items():
476
            headers['x-object-meta-%s' % k] = v
477
        if remove:
478
            headers['x-move-from'] = '/%s/%s' % (src_container, src_object)
479
        else:
480
            headers['x-copy-from'] = '/%s/%s' % (src_container, src_object)
481
        headers['content_length'] = 0
482
        if content_type:
483
            headers['content_type'] = content_type
484
        else:
485
            params['ignore_content_type'] = ''
486
        if delimiter:
487
            params['delimiter'] = delimiter
488
        return self.put(path, headers=headers, params=params)
489

    
490
    def copy_object(self, src_container, src_object, dst_container, dst_object,
491
                    meta={}, account=None, content_type=None, delimiter=None, **headers):
492
        """copies an object"""
493
        account = account or self.account
494
        return self._change_obj_location(src_container, src_object,
495
                                         dst_container, dst_object, account=account,
496
                                         remove=False, meta=meta,
497
                                         content_type=content_type, delimiter=delimiter, **headers)
498

    
499
    def move_object(self, src_container, src_object, dst_container,
500
                    dst_object, meta={}, account=None,
501
                    content_type=None, **headers):
502
        """moves an object"""
503
        account = account or self.account
504
        return self._change_obj_location(src_container, src_object,
505
                                         dst_container, dst_object,
506
                                         account=account, remove=True,
507
                                         meta=meta, content_type=content_type,
508
                                         **headers)
509

    
510
    def delete_object(self, container, object, params={}, account=None):
511
        """deletes an object"""
512
        account = account or self.account
513
        return self.delete('/%s/%s/%s' % (account, container, object),
514
                           params=params)
515

    
516
    def retrieve_object_metadata(self, container, object, restricted=False,
517
                                 version=None, account=None):
518
        """
519
        set restricted to True to get only user defined metadata
520
        """
521
        account = account or self.account
522
        path = '/%s/%s/%s' % (account, container, object)
523
        prefix = 'x-object-meta-' if restricted else None
524
        params = {'version': version} if version else {}
525
        return self._get_metadata(path, prefix, params=params)
526

    
527
    def update_object_metadata(self, container, object, account=None,
528
                               **meta):
529
        """
530
        updates object's metadata
531
        """
532
        account = account or self.account
533
        path = '/%s/%s/%s' % (account, container, object)
534
        return self._update_metadata(path, 'object', **meta)
535

    
536
    def delete_object_metadata(self, container, object, meta=[], account=None):
537
        """
538
        deletes object's metadata
539
        """
540
        account = account or self.account
541
        path = '/%s/%s' % (account, container, object)
542
        return self._delete_metadata(path, 'object', meta)
543

    
544

    
545
class Pithos_Client(OOS_Client):
546
    """Pithos Storage Client. Extends OOS_Client"""
547

    
548
    def _update_metadata(self, path, entity, **meta):
549
        """
550
        adds new and updates the values of previously set metadata
551
        """
552
        params = {'update': None}
553
        headers = {}
554
        prefix = 'x-%s-meta-' % entity
555
        for k, v in meta.items():
556
            k = '%s%s' % (prefix, k)
557
            headers[k] = v
558
        return self.post(path, headers=headers, params=params)
559

    
560
    def _delete_metadata(self, path, entity, meta=[]):
561
        """
562
        delete previously set metadata
563
        """
564
        params = {'update': None}
565
        headers = {}
566
        prefix = 'x-%s-meta-' % entity
567
        for m in meta:
568
            headers['%s%s' % (prefix, m)] = ''
569
        return self.post(path, headers=headers, params=params)
570

    
571
    # Storage Account Services
572

    
573
    def list_containers(self, format='text', if_modified_since=None,
574
                        if_unmodified_since=None, limit=None, marker=None,
575
                        shared=False, until=None, account=None, public=False):
576
        """returns a list with the account containers"""
577
        account = account or self.account
578
        params = {'until': until} if until else {}
579
        if shared:
580
            params['shared'] = None
581
        if public:
582
            params['public'] = None
583
        headers = {'if-modified-since': if_modified_since,
584
                   'if-unmodified-since': if_unmodified_since}
585
        return OOS_Client.list_containers(self, account=account, format=format,
586
                                          limit=limit, marker=marker,
587
                                          params=params, **headers)
588

    
589
    def retrieve_account_metadata(self, restricted=False, until=None,
590
                                  account=None):
591
        """returns the account metadata"""
592
        account = account or self.account
593
        params = {'until': until} if until else {}
594
        return OOS_Client.retrieve_account_metadata(self, account=account,
595
                                                    restricted=restricted,
596
                                                    **params)
597

    
598
    def set_account_groups(self, account=None, **groups):
599
        """create account groups"""
600
        account = account or self.account
601
        path = '/%s' % account
602
        headers = {}
603
        for k, v in groups.items():
604
            headers['x-account-group-%s' % k] = v
605
        params = {'update': None}
606
        return self.post(path, headers=headers, params=params)
607

    
608
    def retrieve_account_groups(self, account=None):
609
        """returns the account groups"""
610
        account = account or self.account
611
        meta = self.retrieve_account_metadata(account=account)
612
        prefix = 'x-account-group-'
613
        prefixlen = len(prefix)
614
        groups = {}
615
        for key, val in meta.items():
616
            if prefix and not key.startswith(prefix):
617
                continue
618
            elif prefix and key.startswith(prefix):
619
                key = key[prefixlen:]
620
            groups[key] = val
621
        return groups
622

    
623
    def unset_account_groups(self, groups=[], account=None):
624
        """delete account groups"""
625
        account = account or self.account
626
        path = '/%s' % account
627
        headers = {}
628
        for elem in groups:
629
            headers['x-account-group-%s' % elem] = ''
630
        params = {'update': None}
631
        return self.post(path, headers=headers, params=params)
632

    
633
    def reset_account_groups(self, account=None, **groups):
634
        """overrides account groups"""
635
        account = account or self.account
636
        path = '/%s' % account
637
        headers = {}
638
        for k, v in groups.items():
639
            v = v.strip()
640
            headers['x-account-group-%s' % k] = v
641
        meta = self.retrieve_account_metadata(restricted=True)
642
        prefix = 'x-account-meta-'
643
        for k, v in meta.items():
644
            k = '%s%s' % (prefix, k)
645
            headers[k] = v
646
        return self.post(path, headers=headers)
647

    
648
    # Storage Container Services
649
    def create_container(self, container, account=None, meta={}, policies={}):
650
        """creates a container"""
651
        args = {}
652
        for k, v in policies.items():
653
            args['X-Container-Policy-%s' % k.capitalize()] = v
654
        return OOS_Client.create_container(self, container, account, meta, **args)
655

    
656
    def list_objects(self, container, format='text',
657
                     limit=None, marker=None, prefix=None, delimiter=None,
658
                     path=None, shared=False, include_trashed=False, params={},
659
                     if_modified_since=None, if_unmodified_since=None, meta='',
660
                     until=None, account=None, public=False):
661
        """returns a list with the container objects"""
662
        account = account or self.account
663
        params = {'until': until, 'meta': meta}
664
        if shared:
665
            params['shared'] = None
666
        if public:
667
            params['public'] = None
668
        args = locals().copy()
669
        for elem in ['self', 'container', 'params', 'until', 'meta']:
670
            args.pop(elem)
671
        return OOS_Client.list_objects(self, container, params=params, **args)
672

    
673
    def retrieve_container_metadata(self, container, restricted=False,
674
                                    until=None, account=None):
675
        """returns container's metadata"""
676
        account = account or self.account
677
        params = {'until': until} if until else {}
678
        return OOS_Client.retrieve_container_metadata(self, container,
679
                                                      account=account,
680
                                                      restricted=restricted,
681
                                                      **params)
682

    
683
    def set_container_policies(self, container, account=None,
684
                               **policies):
685
        """sets containers policies"""
686
        account = account or self.account
687
        path = '/%s/%s' % (account, container)
688
        headers = {}
689
        for key, val in policies.items():
690
            headers['x-container-policy-%s' % key] = val
691
        return self.post(path, headers=headers)
692

    
693
    def update_container_data(self, container, f=stdin):
694
        """adds blocks of data to the container"""
695
        account = self.account
696
        path = '/%s/%s' % (account, container)
697
        params = {'update': None}
698
        headers = {'content_type': 'application/octet-stream'}
699
        data = f.read() if f else None
700
        headers['content_length'] = len(data)
701
        return self.post(path, data, headers=headers, params=params)
702

    
703
    def delete_container(self, container, until=None, account=None, delimiter=None):
704
        """deletes a container or the container history until the date provided"""
705
        account = account or self.account
706
        params = {'until': until} if until else {}
707
        if delimiter:
708
            params['delimiter'] = delimiter
709
        return OOS_Client.delete_container(self, container, account=account,
710
                                           params=params)
711

    
712
    # Storage Object Services
713

    
714
    def retrieve_object(self, container, object, params={}, format='text',
715
                        range=None, if_range=None,
716
                        if_match=None, if_none_match=None,
717
                        if_modified_since=None, if_unmodified_since=None,
718
                        account=None, **headers):
719
        """returns an object"""
720
        account = account or self.account
721
        headers = {}
722
        l = ['range', 'if_range', 'if_match', 'if_none_match',
723
             'if_modified_since', 'if_unmodified_since']
724
        l = [elem for elem in l if eval(elem)]
725
        for elem in l:
726
            headers.update({elem: eval(elem)})
727
        if format != 'text':
728
            params['hashmap'] = None
729
        return OOS_Client.retrieve_object(self, container, object,
730
                                          account=account, format=format,
731
                                          params=params, **headers)
732

    
733
    def retrieve_object_version(self, container, object, version,
734
                                format='text', range=None, if_range=None,
735
                                if_match=None, if_none_match=None,
736
                                if_modified_since=None, if_unmodified_since=None,
737
                                account=None):
738
        """returns a specific object version"""
739
        account = account or self.account
740
        args = locals().copy()
741
        l = ['self', 'container', 'object']
742
        for elem in l:
743
            args.pop(elem)
744
        params = {'version': version}
745
        return self.retrieve_object(container, object, params=params, **args)
746

    
747
    def retrieve_object_versionlist(self, container, object, range=None,
748
                                    if_range=None, if_match=None,
749
                                    if_none_match=None, if_modified_since=None,
750
                                    if_unmodified_since=None, account=None):
751
        """returns the object version list"""
752
        account = account or self.account
753
        args = locals().copy()
754
        l = ['self', 'container', 'object']
755
        for elem in l:
756
            args.pop(elem)
757

    
758
        return self.retrieve_object_version(container, object, version='list',
759
                                            format='json', **args)
760

    
761
    def create_zero_length_object(self, container, object,
762
                                  meta={}, etag=None, content_type=None,
763
                                  content_encoding=None,
764
                                  content_disposition=None,
765
                                  x_object_manifest=None, x_object_sharing=None,
766
                                  x_object_public=None, account=None):
767
        """createas a zero length object"""
768
        account = account or self.account
769
        args = locals().copy()
770
        for elem in ['self', 'container', 'object']:
771
            args.pop(elem)
772
        return OOS_Client.create_zero_length_object(self, container, object,
773
                                                    **args)
774

    
775
    def create_folder(self, container, name,
776
                      meta={}, etag=None,
777
                      content_encoding=None,
778
                      content_disposition=None,
779
                      x_object_manifest=None, x_object_sharing=None,
780
                      x_object_public=None, account=None):
781
        args = locals().copy()
782
        for elem in ['self', 'container', 'name']:
783
            args.pop(elem)
784
        args['content_type'] = 'application/directory'
785
        return self.create_zero_length_object(container, name, **args)
786

    
787
    def create_object(self, container, object, f=stdin, format='text',
788
                      meta={}, params={}, etag=None, content_type=None,
789
                      content_encoding=None, content_disposition=None,
790
                      x_object_manifest=None, x_object_sharing=None,
791
                      x_object_public=None, account=None):
792
        """creates an object"""
793
        account = account or self.account
794
        args = locals().copy()
795
        for elem in ['self', 'container', 'object']:
796
            args.pop(elem)
797
        if format != 'text':
798
            params.update({'hashmap': None})
799
        return OOS_Client.create_object(self, container, object, **args)
800

    
801
    def create_object_using_chunks(self, container, object,
802
                                   f=stdin, blocksize=1024, meta={}, etag=None,
803
                                   content_type=None, content_encoding=None,
804
                                   content_disposition=None,
805
                                   x_object_sharing=None, x_object_manifest=None,
806
                                   x_object_public=None, account=None):
807
        """creates an object (incremental upload)"""
808
        account = account or self.account
809
        path = '/%s/%s/%s' % (account, container, object)
810
        headers = {}
811
        l = ['etag', 'content_type', 'content_encoding', 'content_disposition',
812
             'x_object_sharing', 'x_object_manifest', 'x_object_public']
813
        l = [elem for elem in l if eval(elem)]
814
        for elem in l:
815
            headers.update({elem: eval(elem)})
816
        headers.setdefault('content-type', 'application/octet-stream')
817

    
818
        for k, v in meta.items():
819
            v = v.strip()
820
            headers['x-object-meta-%s' % k.strip()] = v
821

    
822
        return self._chunked_transfer(path, 'PUT', f, headers=headers,
823
                                      blocksize=blocksize)
824

    
825
    def create_object_by_hashmap(self, container, object, hashmap={},
826
                                 meta={}, etag=None, content_encoding=None,
827
                                 content_disposition=None, content_type=None,
828
                                 x_object_sharing=None, x_object_manifest=None,
829
                                 x_object_public=None, account=None):
830
        """creates an object by uploading hashes representing data instead of data"""
831
        account = account or self.account
832
        args = locals().copy()
833
        for elem in ['self', 'container', 'object', 'hashmap']:
834
            args.pop(elem)
835

    
836
        try:
837
            data = json.dumps(hashmap)
838
        except SyntaxError:
839
            raise Fault('Invalid formatting')
840
        args['params'] = {'hashmap': None}
841
        args['format'] = 'json'
842

    
843
        return self.create_object(container, object, f=StringIO(data), **args)
844

    
845
    def create_manifestation(self, container, object, manifest, account=None):
846
        """creates a manifestation"""
847
        account = account or self.account
848
        headers = {'x_object_manifest': manifest}
849
        return self.create_object(container, object, f=None, account=account,
850
                                  **headers)
851

    
852
    def update_object(self, container, object, f=stdin,
853
                      offset=None, meta={}, replace=False, content_length=None,
854
                      content_type=None, content_range=None,
855
                      content_encoding=None, content_disposition=None,
856
                      x_object_bytes=None, x_object_manifest=None,
857
                      x_object_sharing=None, x_object_public=None,
858
                      x_source_object=None, account=None):
859
        """updates an object"""
860
        account = account or self.account
861
        args = locals().copy()
862
        for elem in ['self', 'container', 'object', 'replace']:
863
            args.pop(elem)
864
        if not replace:
865
            args['params'] = {'update': None}
866
        return OOS_Client.update_object(self, container, object, **args)
867

    
868
    def update_object_using_chunks(self, container, object, f=stdin,
869
                                   blocksize=1024, offset=None, meta={},
870
                                   replace=False, content_type=None, content_encoding=None,
871
                                   content_disposition=None, x_object_bytes=None,
872
                                   x_object_manifest=None, x_object_sharing=None,
873
                                   x_object_public=None, account=None):
874
        """updates an object (incremental upload)"""
875
        account = account or self.account
876
        args = locals().copy()
877
        for elem in ['self', 'container', 'object', 'replace']:
878
            args.pop(elem)
879
        if not replace:
880
            args['params'] = {'update': None}
881
        return OOS_Client.update_object_using_chunks(self, container, object, **args)
882

    
883
    def update_from_other_source(self, container, object, source,
884
                                 offset=None, meta={}, content_range=None,
885
                                 content_encoding=None, content_disposition=None,
886
                                 x_object_bytes=None, x_object_manifest=None,
887
                                 x_object_sharing=None, x_object_public=None, account=None):
888
        """updates an object"""
889
        account = account or self.account
890
        args = locals().copy()
891
        for elem in ['self', 'container', 'object', 'source']:
892
            args.pop(elem)
893

    
894
        args['x_source_object'] = source
895
        return self.update_object(container, object, f=None, **args)
896

    
897
    def delete_object(self, container, object, until=None, account=None, delimiter=None):
898
        """deletes an object or the object history until the date provided"""
899
        account = account or self.account
900
        params = {'until': until} if until else {}
901
        if delimiter:
902
            params['delimiter'] = delimiter
903
        return OOS_Client.delete_object(self, container, object, params, account)
904

    
905
    def trash_object(self, container, object):
906
        """trashes an object"""
907
        account = account or self.account
908
        path = '/%s/%s' % (container, object)
909
        meta = {'trash': 'true'}
910
        return self._update_metadata(path, 'object', **meta)
911

    
912
    def restore_object(self, container, object, account=None):
913
        """restores a trashed object"""
914
        account = account or self.account
915
        return self.delete_object_metadata(container, object, account, ['trash'])
916

    
917
    def publish_object(self, container, object, account=None):
918
        """sets a previously created object publicly accessible"""
919
        account = account or self.account
920
        path = '/%s/%s/%s' % (account, container, object)
921
        headers = {}
922
        headers['x_object_public'] = True
923
        params = {'update': None}
924
        return self.post(path, headers=headers, params=params)
925

    
926
    def unpublish_object(self, container, object, account=None):
927
        """unpublish an object"""
928
        account = account or self.account
929
        path = '/%s/%s/%s' % (account, container, object)
930
        headers = {}
931
        headers['x_object_public'] = False
932
        params = {'update': None}
933
        return self.post(path, headers=headers, params=params)
934

    
935
    def copy_object(self, src_container, src_object, dst_container, dst_object,
936
                    meta={}, public=False, version=None, account=None,
937
                    content_type=None, delimiter=None):
938
        """copies an object"""
939
        account = account or self.account
940
        headers = {}
941
        headers['x_object_public'] = public
942
        if version:
943
            headers['x_source_version'] = version
944
        return OOS_Client.copy_object(self, src_container, src_object,
945
                                      dst_container, dst_object, meta=meta,
946
                                      account=account, content_type=content_type,
947
                                      delimiter=delimiter,
948
                                      **headers)
949

    
950
    def move_object(self, src_container, src_object, dst_container,
951
                    dst_object, meta={}, public=False,
952
                    account=None, content_type=None, delimiter=None):
953
        """moves an object"""
954
        headers = {}
955
        headers['x_object_public'] = public
956
        return OOS_Client.move_object(self, src_container, src_object,
957
                                      dst_container, dst_object, meta=meta,
958
                                      account=account, content_type=content_type,
959
                                      delimiter=delimiter,
960
                                      **headers)
961

    
962
    def list_shared_by_others(self, limit=None, marker=None, format='text'):
963
        """lists other accounts that share objects to the user"""
964
        l = ['limit', 'marker']
965
        params = {}
966
        for elem in [elem for elem in l if eval(elem)]:
967
            params[elem] = eval(elem)
968
        return self._list('', format, params)
969

    
970
    def share_object(self, container, object, l, read=True):
971
        """gives access(read by default) to an object to a user/group list"""
972
        action = 'read' if read else 'write'
973
        sharing = '%s=%s' % (action, ','.join(l))
974
        self.update_object(container, object, f=None, x_object_sharing=sharing)
975

    
976

    
977
def _prepare_path(path, format='text', params={}):
978
    full_path = '%s?format=%s' % (quote(path), format)
979

    
980
    for k, v in params.items():
981
        value = quote(str(v)) if v else ''
982
        full_path = '%s&%s=%s' % (full_path, quote(k), value)
983
    return full_path
984

    
985

    
986
def _prepare_headers(headers):
987
    for k, v in headers.items():
988
        headers.pop(k)
989
        k = k.replace('_', '-')
990
        headers[quote(k)] = quote(
991
            v, safe='/=,:@ *"') if isinstance(v, types.StringType) else v
992
    return headers
993

    
994

    
995
def _handle_response(response, verbose=False, debug=False):
996
    headers = response.getheaders()
997
    headers = dict((unquote(h), unquote(v)) for h, v in headers)
998

    
999
    if verbose:
1000
        print '%d %s' % (response.status, response.reason)
1001
        for key, val in headers.items():
1002
            print '%s: %s' % (key.capitalize(), val)
1003
        print
1004

    
1005
    length = response.getheader('content-length', None)
1006
    data = response.read(length)
1007
    if debug:
1008
        print data
1009
        print
1010

    
1011
    if int(response.status) in ERROR_CODES.keys():
1012
        raise Fault(data, int(response.status))
1013

    
1014
    #print '**',  response.status, headers, data, '\n'
1015
    return response.status, headers, data