Statistics
| Branch: | Tag: | Revision:

root / pithos / lib / client.py @ f7ab99df

History | View | Annotate | Download (12.8 kB)

1
from httplib import HTTPConnection, HTTP
2
from sys import stdin
3

    
4
import json
5
import types
6
import socket
7
import pithos.api.faults
8

    
9
ERROR_CODES = {304:'Not Modified',
10
               400:'Bad Request',
11
               401:'Unauthorized',
12
               404:'Not Found',
13
               409:'Conflict',
14
               411:'Length Required',
15
               412:'Precondition Failed',
16
               416:'Range Not Satisfiable',
17
               422:'Unprocessable Entity',
18
               503:'Service Unavailable'}
19

    
20
class Fault(Exception):
21
    def __init__(self, data='', status=None):
22
        if data == '' and status in ERROR_CODES.keys():
23
            data = ERROR_CODES[status]
24
        Exception.__init__(self, data)
25
        self.data = data
26
        self.status = status
27

    
28
class Client(object):
29
    def __init__(self, host, account, api='v1', verbose=False, debug=False):
30
        """`host` can also include a port, e.g '127.0.0.1:8000'."""
31
        
32
        self.host = host
33
        self.account = account
34
        self.api = api
35
        self.verbose = verbose or debug
36
        self.debug = debug
37
    
38
    def _chunked_transfer(self, path, method='PUT', f=stdin, headers=None,
39
                          blocksize=1024):
40
        http = HTTPConnection(self.host)
41
        
42
        # write header
43
        path = '/%s/%s%s' % (self.api, self.account, path)
44
        http.putrequest(method, path)
45
        http.putheader('Content-Type', 'application/octet-stream')
46
        http.putheader('Transfer-Encoding', 'chunked')
47
        if headers:
48
            for header,value in headers.items():
49
                http.putheader(header, value)
50
        http.endheaders()
51
        
52
        # write body
53
        data = ''
54
        while True:
55
            if f.closed:
56
                break
57
            block = f.read(blocksize)
58
            if block == '':
59
                break
60
            data = '%s\r\n%s\r\n' % (hex(len(block)), block)
61
            try:
62
                http.send(data)
63
            except:
64
                #retry
65
                http.send(data)
66
        data = '0x0\r\n'
67
        try:
68
            http.send(data)
69
        except:
70
            #retry
71
            http.send(data)
72
        
73
        # get response
74
        resp = http.getresponse()
75
        
76
        headers = dict(resp.getheaders())
77
        
78
        if self.verbose:
79
            print '%d %s' % (resp.status, resp.reason)
80
            for key, val in headers.items():
81
                print '%s: %s' % (key.capitalize(), val)
82
            print
83
        
84
        length = resp.getheader('Content-length', None)
85
        data = resp.read(length)
86
        if self.debug:
87
            print data
88
            print
89
        
90
        if int(resp.status) in ERROR_CODES.keys():
91
            raise Fault(data, int(resp.status))
92
        
93
        #print '*',  resp.status, headers, data
94
        return resp.status, headers, data
95
    
96
    def req(self, method, path, body=None, headers=None, format='text',
97
            params=None):
98
        full_path = '/%s/%s%s?format=%s' % (self.api, self.account, path,
99
                                            format)
100
        if params:
101
            for k,v in params.items():
102
                if v:
103
                    full_path = '%s&%s=%s' %(full_path, k, v)
104
        conn = HTTPConnection(self.host)
105
        
106
        #encode whitespace
107
        full_path = full_path.replace(' ', '%20')
108
        
109
        kwargs = {}
110
        kwargs['headers'] = headers or {}
111
        if not headers or \
112
        'Transfer-Encoding' not in headers \
113
        or headers['Transfer-Encoding'] != 'chunked':
114
            kwargs['headers']['Content-Length'] = len(body) if body else 0
115
        if body:
116
            kwargs['body'] = body
117
            kwargs['headers']['Content-Type'] = 'application/octet-stream'
118
        #print '****', method, full_path, kwargs
119
        try:
120
            conn.request(method, full_path, **kwargs)
121
        except socket.error, e:
122
            raise Fault(status=503)
123
            
124
        resp = conn.getresponse()
125
        headers = dict(resp.getheaders())
126
        
127
        if self.verbose:
128
            print '%d %s' % (resp.status, resp.reason)
129
            for key, val in headers.items():
130
                print '%s: %s' % (key.capitalize(), val)
131
            print
132
        
133
        length = resp.getheader('Content-length', None)
134
        data = resp.read(length)
135
        if self.debug:
136
            print data
137
            print
138
        
139
        if int(resp.status) in ERROR_CODES.keys():
140
            raise Fault(data, int(resp.status))
141
        
142
        #print '*',  resp.status, headers, data
143
        return resp.status, headers, data
144
    
145
    def delete(self, path, format='text'):
146
        return self.req('DELETE', path, format=format)
147
    
148
    def get(self, path, format='text', headers=None, params=None):
149
        return self.req('GET', path, headers=headers, format=format,
150
                        params=params)
151
    
152
    def head(self, path, format='text', params=None):
153
        return self.req('HEAD', path, format=format, params=params)
154
    
155
    def post(self, path, body=None, format='text', headers=None):
156
        return self.req('POST', path, body, headers=headers, format=format)
157
    
158
    def put(self, path, body=None, format='text', headers=None):
159
        return self.req('PUT', path, body, headers=headers, format=format)
160
    
161
    def _list(self, path, detail=False, params=None, headers=None):
162
        format = 'json' if detail else 'text'
163
        status, headers, data = self.get(path, format=format, headers=headers,
164
                                         params=params)
165
        if detail:
166
            data = json.loads(data)
167
        else:
168
            data = data.strip().split('\n')
169
        return data
170
    
171
    def _get_metadata(self, path, prefix=None, params=None):
172
        status, headers, data = self.head(path, params=params)
173
        prefixlen = len(prefix) if prefix else 0
174
        meta = {}
175
        for key, val in headers.items():
176
            if prefix and not key.startswith(prefix):
177
                continue
178
            elif prefix and key.startswith(prefix):
179
                key = key[prefixlen:]
180
            meta[key] = val
181
        return meta
182
    
183
    def _update_metadata(self, path, entity, **meta):
184
        """
185
         adds new and updates the values of previously set metadata
186
        """
187
        for key, val in meta.items():
188
            meta.pop(key)
189
            meta['X-%s-Meta-%s' %(entity.capitalize(), key.capitalize())] = val
190
        prev_meta = self._get_metadata(path)
191
        prev_meta.update(meta)
192
        headers = {}
193
        for key, val in prev_meta.items():
194
            headers[key.capitalize()] = val
195
        self.post(path, headers=headers)
196
    
197
    def _delete_metadata(self, path, entity, meta=[]):
198
        """
199
        delete previously set metadata
200
        """
201
        prev_meta = self._get_metadata(path)
202
        headers = {}
203
        for key, val in prev_meta.items():
204
            if key.split('-')[-1] in meta:
205
                continue
206
            http_key = key.capitalize()
207
            headers[http_key] = val
208
        self.post(path, headers=headers)
209
    
210
    # Storage Account Services
211
    
212
    def list_containers(self, detail=False, params=None, headers=None):
213
        return self._list('', detail, params, headers)
214
    
215
    def account_metadata(self, restricted=False, until=None):
216
        prefix = 'x-account-meta-' if restricted else None
217
        params = {'until':until} if until else None
218
        return self._get_metadata('', prefix, params=params)
219
    
220
    def update_account_metadata(self, **meta):
221
        self._update_metadata('', 'account', **meta)
222
        
223
    def delete_account_metadata(self, meta=[]):
224
        self._delete_metadata('', 'account', meta)
225
    
226
    # Storage Container Services
227
    
228
    def list_objects(self, container, detail=False, params=None, headers=None):
229
        return self._list('/' + container, detail, params, headers)
230
    
231
    def create_container(self, container, headers=None):
232
        status, header, data = self.put('/' + container, headers=headers)
233
        if status == 202:
234
            return False
235
        elif status != 201:
236
            raise Fault(data, int(status))
237
        return True
238
    
239
    def delete_container(self, container):
240
        self.delete('/' + container)
241
    
242
    def retrieve_container_metadata(self, container, restricted=False,
243
                                    until=None):
244
        prefix = 'x-container-meta-' if restricted else None
245
        params = {'until':until} if until else None
246
        return self._get_metadata('/%s' % container, prefix, params=params)
247
    
248
    def update_container_metadata(self, container, **meta):
249
        self._update_metadata('/' + container, 'container', **meta)
250
        
251
    def delete_container_metadata(self, container, meta=[]):
252
        path = '/%s' % (container)
253
        self._delete_metadata(path, 'container', meta)
254
    
255
    # Storage Object Services
256
    
257
    def retrieve_object(self, container, object, detail=False, headers=None,
258
                        version=None):
259
        path = '/%s/%s' % (container, object)
260
        format = 'json' if detail else 'text'
261
        params = {'version':version} if version else None 
262
        status, headers, data = self.get(path, format, headers, params)
263
        return data
264
    
265
    def create_directory_marker(self, container, object):
266
        if not object:
267
            raise Fault('Directory markers have to be nested in a container')
268
        h = {'Content-Type':'application/directory'}
269
        self.create_object(container, object, f=None, headers=h)
270
    
271
    def create_object(self, container, object, f=stdin, chunked=False,
272
                      blocksize=1024, headers=None):
273
        """
274
        creates an object
275
        if f is None then creates a zero length object
276
        if f is stdin or chunked is set then performs chunked transfer 
277
        """
278
        path = '/%s/%s' % (container, object)
279
        if not chunked and f != stdin:
280
            data = f.read() if f else None
281
            return self.put(path, data, headers=headers)
282
        else:
283
            return self._chunked_transfer(path, 'PUT', f, headers=headers,
284
                                   blocksize=1024)
285
    
286
    def update_object(self, container, object, f=stdin, chunked=False,
287
                      blocksize=1024, headers=None):
288
        if not f:
289
            return
290
        path = '/%s/%s' % (container, object)
291
        if not chunked and f != stdin:
292
            data = f.read()
293
            self.post(path, data, headers=headers)
294
        else:
295
            self._chunked_transfer(path, 'POST', f, headers=headers,
296
                                   blocksize=1024)
297
    
298
    def _change_obj_location(self, src_container, src_object, dst_container,
299
                             dst_object, remove=False, headers=None):
300
        path = '/%s/%s' % (dst_container, dst_object)
301
        if not headers:
302
            headers = {}
303
        if remove:
304
            headers['X-Move-From'] = '/%s/%s' % (src_container, src_object)
305
        else:
306
            headers['X-Copy-From'] = '/%s/%s' % (src_container, src_object)
307
        headers['Content-Length'] = 0
308
        self.put(path, headers=headers)
309
    
310
    def copy_object(self, src_container, src_object, dst_container,
311
                             dst_object, headers=None):
312
        self._change_obj_location(src_container, src_object,
313
                                   dst_container, dst_object,
314
                                   headers=headers)
315
    
316
    def move_object(self, src_container, src_object, dst_container,
317
                             dst_object, headers=None):
318
        self._change_obj_location(src_container, src_object,
319
                                   dst_container, dst_object, True, headers)
320
    
321
    def delete_object(self, container, object):
322
        self.delete('/%s/%s' % (container, object))
323
    
324
    def retrieve_object_metadata(self, container, object, restricted=False,
325
                                 version=None):
326
        path = '/%s/%s' % (container, object)
327
        prefix = 'x-object-meta-' if restricted else None
328
        params = {'version':version} if version else None
329
        return self._get_metadata(path, prefix, params=params)
330
    
331
    def update_object_metadata(self, container, object, **meta):
332
        path = '/%s/%s' % (container, object)
333
        self._update_metadata(path, 'object', **meta)
334
    
335
    def delete_object_metadata(self, container, object, meta=[]):
336
        path = '/%s/%s' % (container, object)
337
        self._delete_metadata(path, 'object', meta)
338
    
339
    def trash_object(self, container, object):
340
        """
341
        trashes an object
342
        actually resets all object metadata with trash = true 
343
        """
344
        path = '/%s/%s' % (container, object)
345
        meta = {'trash':'true'}
346
        self._update_metadata(path, 'object', **meta)
347
    
348
    def restore_object(self, container, object):
349
        """
350
        restores a trashed object
351
        actualy just resets all object metadata except trash
352
        """
353
        self.delete_object_metadata(container, object, ['trash'])
354