Statistics
| Branch: | Tag: | Revision:

root / pithos / lib / client.py @ 6749c3bd

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