Statistics
| Branch: | Tag: | Revision:

root / pithos / lib / client.py @ 4013a851

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