Statistics
| Branch: | Tag: | Revision:

root / pithos / lib / client.py @ 615e561b

History | View | Annotate | Download (19 kB)

1
# Copyright 2011 GRNET S.A. All rights reserved.
2
# 
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
# 
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
# 
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
# 
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
# 
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from httplib import HTTPConnection, HTTP
35
from sys import stdin
36

    
37
import json
38
import types
39
import socket
40
import pithos.api.faults
41

    
42
ERROR_CODES = {304:'Not Modified',
43
               400:'Bad Request',
44
               401:'Unauthorized',
45
               404:'Not Found',
46
               409:'Conflict',
47
               411:'Length Required',
48
               412:'Precondition Failed',
49
               416:'Range Not Satisfiable',
50
               422:'Unprocessable Entity',
51
               503:'Service Unavailable'}
52

    
53
class Fault(Exception):
54
    def __init__(self, data='', status=None):
55
        if data == '' and status in ERROR_CODES.keys():
56
            data = ERROR_CODES[status]
57
        Exception.__init__(self, data)
58
        self.data = data
59
        self.status = status
60

    
61
class Client(object):
62
    def __init__(self, host, token, account, api='v1', verbose=False, debug=False):
63
        """`host` can also include a port, e.g '127.0.0.1:8000'."""
64
        
65
        self.host = host
66
        self.account = account
67
        self.api = api
68
        self.verbose = verbose or debug
69
        self.debug = debug
70
        self.token = token
71
    
72
    def _chunked_transfer(self, path, method='PUT', f=stdin, headers=None,
73
                          blocksize=1024):
74
        
75
        http = HTTPConnection(self.host)
76
        
77
        # write header
78
        path = '/%s/%s%s' % (self.api, self.account, path)
79
        http.putrequest(method, path)
80
        http.putheader('x-auth-token', self.token)
81
        http.putheader('content-type', 'application/octet-stream')
82
        http.putheader('transfer-encoding', 'chunked')
83
        if headers:
84
            for header,value in headers.items():
85
                http.putheader(header, value)
86
        http.endheaders()
87
        
88
        # write body
89
        data = ''
90
        while True:
91
            if f.closed:
92
                break
93
            block = f.read(blocksize)
94
            if block == '':
95
                break
96
            data = '%s\r\n%s\r\n' % (hex(len(block)), block)
97
            try:
98
                http.send(data)
99
            except:
100
                #retry
101
                http.send(data)
102
        data = '0x0\r\n'
103
        try:
104
            http.send(data)
105
        except:
106
            #retry
107
            http.send(data)
108
        
109
        # get response
110
        resp = http.getresponse()
111
        
112
        headers = dict(resp.getheaders())
113
        
114
        if self.verbose:
115
            print '%d %s' % (resp.status, resp.reason)
116
            for key, val in headers.items():
117
                print '%s: %s' % (key.capitalize(), val)
118
            print
119
        
120
        length = resp.getheader('Content-length', None)
121
        data = resp.read(length)
122
        if self.debug:
123
            print data
124
            print
125
        
126
        if int(resp.status) in ERROR_CODES.keys():
127
            raise Fault(data, int(resp.status))
128
        
129
        #print '*',  resp.status, headers, data
130
        return resp.status, headers, data
131
    
132
    def _req(self, method, path, body=None, headers=None, format='text',
133
            params=None):
134
        full_path = '/%s/%s%s?format=%s' % (self.api, self.account, path,
135
                                            format)
136
        if params:
137
            for k,v in params.items():
138
                if v:
139
                    full_path = '%s&%s=%s' %(full_path, k, v)
140
                else:
141
                    full_path = '%s&%s' %(full_path, k)
142
        conn = HTTPConnection(self.host)
143
        
144
        #encode whitespace
145
        full_path = full_path.replace(' ', '%20')
146
        
147
        kwargs = {}
148
        kwargs['headers'] = headers or {}
149
        kwargs['headers']['X-Auth-Token'] = self.token
150
        if not headers or \
151
        'transfer-encoding' not in headers \
152
        or headers['transfer-encoding'] != 'chunked':
153
            kwargs['headers']['content-length'] = len(body) if body else 0
154
        if body:
155
            kwargs['body'] = body
156
        else:
157
            kwargs['headers']['content-type'] = ''
158
        kwargs['headers'].setdefault('content-type', 'application/octet-stream')
159
        try:
160
            #print '*', method, full_path, kwargs
161
            conn.request(method, full_path, **kwargs)
162
        except socket.error, e:
163
            raise Fault(status=503)
164
            
165
        resp = conn.getresponse()
166
        headers = dict(resp.getheaders())
167
        
168
        if self.verbose:
169
            print '%d %s' % (resp.status, resp.reason)
170
            for key, val in headers.items():
171
                print '%s: %s' % (key.capitalize(), val)
172
            print
173
        
174
        length = resp.getheader('content-length', None)
175
        data = resp.read(length)
176
        if self.debug:
177
            print data
178
            print
179
        
180
        if int(resp.status) in ERROR_CODES.keys():
181
            raise Fault(data, int(resp.status))
182
        
183
        #print '*',  resp.status, headers, data
184
        return resp.status, headers, data
185
    
186
    def delete(self, path, format='text'):
187
        return self._req('DELETE', path, format=format)
188
    
189
    def get(self, path, format='text', headers=None, params=None):
190
        return self._req('GET', path, headers=headers, format=format,
191
                        params=params)
192
    
193
    def head(self, path, format='text', params=None):
194
        return self._req('HEAD', path, format=format, params=params)
195
    
196
    def post(self, path, body=None, format='text', headers=None, params=None):
197
        return self._req('POST', path, body, headers=headers, format=format,
198
                        params=params)
199
    
200
    def put(self, path, body=None, format='text', headers=None):
201
        return self._req('PUT', path, body, headers=headers, format=format)
202
    
203
    def _list(self, path, detail=False, params=None, headers=None):
204
        format = 'json' if detail else 'text'
205
        status, headers, data = self.get(path, format=format, headers=headers,
206
                                         params=params)
207
        if detail:
208
            data = json.loads(data) if data else ''
209
        else:
210
            data = data.strip().split('\n')
211
        return data
212
    
213
    def _get_metadata(self, path, prefix=None, params=None):
214
        status, headers, data = self.head(path, params=params)
215
        prefixlen = len(prefix) if prefix else 0
216
        meta = {}
217
        for key, val in headers.items():
218
            if prefix and not key.startswith(prefix):
219
                continue
220
            elif prefix and key.startswith(prefix):
221
                key = key[prefixlen:]
222
            meta[key] = val
223
        return meta
224
    
225
    def _update_metadata(self, path, entity, **meta):
226
        """
227
        adds new and updates the values of previously set metadata
228
        """
229
        params = {'update':None}
230
        headers = {}
231
        prefix = 'x-%s-meta-' % entity
232
        for k,v in meta.items():
233
            k = '%s%s' % (prefix, k)
234
            headers[k] = v
235
        return self.post(path, headers=headers, params=params)
236
    
237
    def _delete_metadata(self, path, entity, meta=[]):
238
        """
239
        delete previously set metadata
240
        """
241
        params = {'update':None}
242
        headers = {}
243
        prefix = 'x-%s-meta-' % entity
244
        for m in meta:
245
            headers['%s%s' % (prefix, m)] = None
246
        return self.post(path, headers=headers)
247
    
248
    # Storage Account Services
249
    
250
    def list_containers(self, detail=False, params=None, headers=None):
251
        return self._list('', detail, params, headers)
252
    
253
    def account_metadata(self, restricted=False, until=None):
254
        prefix = 'x-account-meta-' if restricted else None
255
        params = {'until':until} if until else None
256
        return self._get_metadata('', prefix, params=params)
257
    
258
    def update_account_metadata(self, **meta):
259
        return self._update_metadata('', 'account', **meta)
260
        
261
    def delete_account_metadata(self, meta=[]):
262
        return self._delete_metadata('', 'account', meta)
263
    
264
    def set_account_groups(self, **groups):
265
        """
266
        create account groups
267
        """
268
        headers = {}
269
        for key, val in groups.items():
270
            headers['x-account-group-%s' % key] = val
271
        params = {'update':None}
272
        return self.post('', headers=headers, params=params)
273
    
274
    def unset_account_groups(self, groups=[]):
275
        """
276
        delete account groups
277
        """
278
        headers = {}
279
        for elem in groups:
280
            headers['x-account-group-%s' % elem] = ''
281
        params = {'update':None}
282
        return self.post('', headers=headers, params=params)
283
    
284
    # Storage Container Services
285
    
286
    def _filter(self, l, d):
287
        """
288
        filter out from l elements having the metadata values provided
289
        """
290
        ll = l
291
        for elem in l:
292
            if type(elem) == types.DictionaryType:
293
                for key in d.keys():
294
                    k = 'x_object_meta_%s' % key
295
                    if k in elem.keys() and elem[k] == d[key]:
296
                        ll.remove(elem)
297
                        break
298
        return ll
299
    
300
    def _filter_trashed(self, l):
301
        return self._filter(l, {'trash':'true'})
302
    
303
    def list_objects(self, container, detail=False, headers=None,
304
                     include_trashed=False, **params):
305
        l = self._list('/' + container, detail, params, headers)
306
        if not include_trashed:
307
            l = self._filter_trashed(l)
308
        return l
309
    
310
    def create_container(self, container, headers=None, **meta):
311
        for k,v in meta.items():
312
            headers['x-container-meta-%s' %k.strip().upper()] = v.strip()
313
        status, header, data = self.put('/' + container, headers=headers)
314
        if status == 202:
315
            return False
316
        elif status != 201:
317
            raise Fault(data, int(status))
318
        return True
319
    
320
    def delete_container(self, container):
321
        return self.delete('/' + container)
322
    
323
    def retrieve_container_metadata(self, container, restricted=False,
324
                                    until=None):
325
        prefix = 'x-container-meta-' if restricted else None
326
        params = {'until':until} if until else None
327
        return self._get_metadata('/%s' % container, prefix, params=params)
328
    
329
    def update_container_metadata(self, container, **meta):
330
        return self._update_metadata('/' + container, 'container', **meta)
331
        
332
    def delete_container_metadata(self, container, meta=[]):
333
        path = '/%s' % (container)
334
        return self._delete_metadata(path, 'container', meta)
335
    
336
    def set_container_policies(self, container, **policies):
337
        path = '/%s' % (container)
338
        headers = {}
339
        print ''
340
        for key, val in policies.items():
341
            headers['x-container-policy-%s' % key] = val
342
        return self.post(path, headers=headers)
343
    
344
    # Storage Object Services
345
    
346
    def retrieve_object(self, container, object, detail=False, headers=None,
347
                        version=None):
348
        path = '/%s/%s' % (container, object)
349
        format = 'json' if detail else 'text'
350
        params = {'version':version} if version else None 
351
        status, headers, data = self.get(path, format, headers, params)
352
        return data
353
    
354
    def create_directory_marker(self, container, object):
355
        if not object:
356
            raise Fault('Directory markers have to be nested in a container')
357
        h = {'Content-Type':'application/directory'}
358
        return self.create_object(container, object, f=None, headers=h)
359
    
360
    def _set_public_header(self, headers, public=False):
361
        """
362
        sets the public header
363
        """
364
        if public == None:
365
            return
366
        elif public:
367
            headers['x-object-public'] = public
368
        else:
369
            headers['x-object-public'] = ''
370
    
371
    def create_object(self, container, object, f=stdin, chunked=False,
372
                      blocksize=1024, headers={}, use_hashes=False,
373
                      public=None, **meta):
374
        """
375
        creates an object
376
        if f is None then creates a zero length object
377
        if f is stdin or chunked is set then performs chunked transfer 
378
        """
379
        path = '/%s/%s' % (container, object)
380
        for k,v in meta.items():
381
            headers['x-object-meta-%s' %k.strip().upper()] = v.strip()
382
        self._set_public_header(headers, public)
383
        headers = headers if headers else None
384
        if not chunked:
385
            format = 'json' if use_hashes else 'text'
386
            data = f.read() if f else None
387
            if data:
388
                if format == 'json':
389
                    try:
390
                        data = eval(data)
391
                        data = json.dumps(data)
392
                    except SyntaxError:
393
                        raise Fault('Invalid formatting')
394
            return self.put(path, data, headers=headers, format=format)
395
        else:
396
            return self._chunked_transfer(path, 'PUT', f, headers=headers,
397
                                   blocksize=1024)
398
    
399
    def update_object_data(self, container, object, data=None, headers={},
400
                      offset=None, public=None, **meta):
401
        path = '/%s/%s' % (container, object)
402
        for k,v in meta.items():
403
            headers['x-object-meta-%s' %k.strip()] = v.strip()
404
        if 'content-range' not in headers.keys():
405
            if offset:
406
                headers['content-range'] = 'bytes %s-/*' % offset
407
            else:
408
                headers['content-range'] = 'bytes */*'
409
        self._set_public_header(headers, public)
410
        headers = headers if headers else None
411
        return self.post(path, data, headers=headers)
412
    
413
    def update_object(self, container, object, f=stdin, chunked=False,
414
                      blocksize=1024, headers={}, offset=None, public=None,
415
                      **meta):
416
        path = '/%s/%s' % (container, object)
417
        for k,v in meta.items():
418
            headers['x-object-meta-%s' %k.strip()] = v.strip()
419
        if offset:
420
            headers['content-range'] = 'bytes %s-/*' % offset
421
        else:
422
            headers['content-range'] = 'bytes */*'
423
        self._set_public_header(headers, public)
424
        headers = headers if headers else None
425
        if not chunked and f != stdin:
426
            data = f.read() if f else None
427
            return self.post(path, data, headers=headers)
428
        else:
429
            return self._chunked_transfer(path, 'POST', f, headers=headers,
430
                                   blocksize=1024)
431
    
432
    def _change_obj_location(self, src_container, src_object, dst_container,
433
                             dst_object, remove=False, public=False, **meta):
434
        path = '/%s/%s' % (dst_container, dst_object)
435
        headers = {}
436
        for k, v in meta.items():
437
            headers['x-object-meta-%s' % k] = v 
438
        if remove:
439
            headers['x-move-from'] = '/%s/%s' % (src_container, src_object)
440
        else:
441
            headers['x-copy-from'] = '/%s/%s' % (src_container, src_object)
442
        self._set_public_header(headers, public)
443
        self.headers = headers if headers else None
444
        headers['content-length'] = 0
445
        return self.put(path, headers=headers)
446
    
447
    def copy_object(self, src_container, src_object, dst_container,
448
                             dst_object, public=False, **meta):
449
        return self._change_obj_location(src_container, src_object,
450
                                   dst_container, dst_object, False,
451
                                   public, **meta)
452
    
453
    def move_object(self, src_container, src_object, dst_container,
454
                             dst_object, public=False, **meta):
455
        return self._change_obj_location(src_container, src_object,
456
                                   dst_container, dst_object, True,
457
                                   public, **meta)
458
    
459
    def delete_object(self, container, object):
460
        return self.delete('/%s/%s' % (container, object))
461
    
462
    def retrieve_object_metadata(self, container, object, restricted=False,
463
                                 version=None):
464
        """
465
        set restricted to True to get only user defined metadata
466
        """
467
        path = '/%s/%s' % (container, object)
468
        prefix = 'x-object-meta-' if restricted else None
469
        params = {'version':version} if version else None
470
        return self._get_metadata(path, prefix, params=params)
471
    
472
    def update_object_metadata(self, container, object, **meta):
473
        path = '/%s/%s' % (container, object)
474
        return self._update_metadata(path, 'object', **meta)
475
    
476
    def delete_object_metadata(self, container, object, meta=[]):
477
        path = '/%s/%s' % (container, object)
478
        return self._delete_metadata(path, 'object', meta)
479
    
480
    def trash_object(self, container, object):
481
        """
482
        trashes an object
483
        actually resets all object metadata with trash = true 
484
        """
485
        path = '/%s/%s' % (container, object)
486
        meta = {'trash':'true'}
487
        return self._update_metadata(path, 'object', **meta)
488
    
489
    def restore_object(self, container, object):
490
        """
491
        restores a trashed object
492
        actualy removes trash object metadata info
493
        """
494
        return self.delete_object_metadata(container, object, ['trash'])
495
    
496
    def publish_object(self, container, object):
497
        """
498
        sets a previously created object publicly accessible
499
        """
500
        path = '/%s/%s' % (container, object)
501
        headers = {'content-range':'bytes */*'}
502
        self._set_public_header(headers, public=True)
503
        return self.post(path, headers=headers)
504
    
505
    def unpublish_object(self, container, object):
506
        """
507
        unpublish an object
508
        """
509
        path = '/%s/%s' % (container, object)
510
        headers = {'content-range':'bytes */*'}
511
        self._set_public_header(headers, public=False)
512
        return self.post(path, headers=headers)