Statistics
| Branch: | Tag: | Revision:

root / pithos / lib / client.py @ 8fe01d72

History | View | Annotate | Download (10.7 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
        data = resp.read()
85
        if self.debug:
86
            print data
87
            print
88
        
89
        if data:
90
            assert data[-1] == '\n'
91
        #remove trailing enter
92
        data = data and data[:-1] or data
93
        
94
        if int(resp.status) in ERROR_CODES.keys():
95
            raise Fault(data, int(resp.status))
96
        
97
        return resp.status, headers, data
98

    
99
    def req(self, method, path, body=None, headers=None, format='text',
100
            params=None):
101
        full_path = '/%s/%s%s?format=%s' % (self.api, self.account, path,
102
                                            format)
103
        if params:
104
            for k,v in params.items():
105
                if v:
106
                    full_path = '%s&%s=%s' %(full_path, k, v)
107
        conn = HTTPConnection(self.host)
108
        
109
        #encode whitespace
110
        full_path = full_path.replace(' ', '%20')
111
        
112
        kwargs = {}
113
        kwargs['headers'] = headers or {}
114
        if not headers or \
115
        'Transfer-Encoding' not in headers \
116
        or headers['Transfer-Encoding'] != 'chunked':
117
            kwargs['headers']['Content-Length'] = len(body) if body else 0
118
        if body:
119
            kwargs['body'] = body
120
            kwargs['headers']['Content-Type'] = 'application/octet-stream'
121
        #print '****', method, full_path, kwargs
122
        try:
123
            conn.request(method, full_path, **kwargs)
124
        except socket.error, e:
125
            raise Fault(status=503)
126
            
127
        resp = conn.getresponse()
128
        headers = dict(resp.getheaders())
129
        
130
        if self.verbose:
131
            print '%d %s' % (resp.status, resp.reason)
132
            for key, val in headers.items():
133
                print '%s: %s' % (key.capitalize(), val)
134
            print
135
        
136
        data = resp.read()
137
        if self.debug:
138
            print data
139
            print
140
        
141
        if data:
142
            assert data[-1] == '\n'
143
        #remove trailing enter
144
        data = data and data[:-1] or data
145
        
146
        if int(resp.status) in ERROR_CODES.keys():
147
            raise Fault(data, int(resp.status))
148
        
149
        #print '*',  resp.status, headers, data
150
        return resp.status, headers, data
151

    
152
    def delete(self, path, format='text'):
153
        return self.req('DELETE', path, format=format)
154

    
155
    def get(self, path, format='text', headers=None, params=None):
156
        return self.req('GET', path, headers=headers, format=format,
157
                        params=params)
158

    
159
    def head(self, path, format='text', params=None):
160
        return self.req('HEAD', path, format=format, params=params)
161

    
162
    def post(self, path, body=None, format='text', headers=None):
163
        return self.req('POST', path, body, headers=headers, format=format)
164

    
165
    def put(self, path, body=None, format='text', headers=None):
166
        return self.req('PUT', path, body, headers=headers, format=format)
167

    
168
    def _list(self, path, detail=False, params=None, headers=None):
169
        format = 'json' if detail else 'text'
170
        status, headers, data = self.get(path, format=format, headers=headers,
171
                                         params=params)
172
        if detail:
173
            data = json.loads(data)
174
        else:
175
            data = data.strip().split('\n')
176
        return data
177

    
178
    def _get_metadata(self, path, prefix=None, params=None):
179
        status, headers, data = self.head(path, params=params)
180
        prefixlen = prefix and len(prefix) or 0
181
        meta = {}
182
        for key, val in headers.items():
183
            if prefix and not key.startswith(prefix):
184
                continue
185
            elif prefix and key.startswith(prefix):
186
                key = key[prefixlen:]
187
            meta[key] = val
188
        return meta
189

    
190
    def _set_metadata(self, path, entity, **meta):
191
        headers = {}
192
        for key, val in meta.items():
193
            http_key = 'X-%s-Meta-%s' %(entity.capitalize(), key.capitalize())
194
            headers[http_key] = val
195
        self.post(path, headers=headers)
196

    
197
    # Storage Account Services
198

    
199
    def list_containers(self, detail=False, params=None, headers=None):
200
        return self._list('', detail, params, headers)
201

    
202
    def account_metadata(self, restricted=False, until=None):
203
        prefix = restricted and 'x-account-meta-' or None
204
        params = until and {'until':until} or None
205
        return self._get_metadata('', prefix, params=params)
206

    
207
    def update_account_metadata(self, **meta):
208
        self._set_metadata('', 'account', **meta)
209

    
210
    # Storage Container Services
211

    
212
    def list_objects(self, container, detail=False, params=None, headers=None):
213
        return self._list('/' + container, detail, params, headers)
214

    
215
    def create_container(self, container, headers=None):
216
        status, header, data = self.put('/' + container, headers=headers)
217
        if status == 202:
218
            return False
219
        elif status != 201:
220
            raise Fault(data, int(status))
221
        return True
222

    
223
    def delete_container(self, container):
224
        self.delete('/' + container)
225

    
226
    def retrieve_container_metadata(self, container, restricted=False,
227
                                    until=None):
228
        prefix = restricted and 'x-container-meta-' or None
229
        params = until and {'until':until} or None
230
        return self._get_metadata('/%s' % container, prefix, params=params)
231

    
232
    def update_container_metadata(self, container, **meta):
233
        self._set_metadata('/' + container, 'container', **meta)
234

    
235
    # Storage Object Services
236

    
237
    def retrieve_object(self, container, object, detail=False, headers=None,
238
                        version=None):
239
        path = '/%s/%s' % (container, object)
240
        format = 'json' if detail else 'text'
241
        params = version and {'version':version} or None 
242
        status, headers, data = self.get(path, format, headers, params)
243
        return data
244

    
245
    def create_object(self, container, object, f=stdin, chunked=False,
246
                      blocksize=1024, headers=None):
247
        """
248
        creates an object
249
        if f is None then creates a zero length object
250
        if f is stdin or chunked is set then performs chunked transfer 
251
        """
252
        path = '/%s/%s' % (container, object)
253
        if not chunked and f != stdin:
254
            data = f and f.read() or None
255
            return self.put(path, data, headers=headers)
256
        else:
257
            return self._chunked_transfer(path, 'PUT', f, headers=headers,
258
                                   blocksize=1024)
259

    
260
    def update_object(self, container, object, f=stdin, chunked=False,
261
                      blocksize=1024, headers=None):
262
        if not f:
263
            return
264
        path = '/%s/%s' % (container, object)
265
        if not chunked and f != stdin:
266
            data = f.read()
267
            self.post(path, data, headers=headers)
268
        else:
269
            self._chunked_transfer(path, 'POST', f, headers=headers,
270
                                   blocksize=1024)
271

    
272
    def _change_obj_location(self, src_container, src_object, dst_container,
273
                             dst_object, remove=False):
274
        path = '/%s/%s' % (dst_container, dst_object)
275
        headers = {}
276
        if remove:
277
            headers['X-Move-From'] = '/%s/%s' % (src_container, src_object)
278
        else:
279
            headers['X-Copy-From'] = '/%s/%s' % (src_container, src_object)
280
        headers['Content-Length'] = 0
281
        self.put(path, headers=headers)
282

    
283
    def copy_object(self, src_container, src_object, dst_container,
284
                             dst_object):
285
        self._change_obj_location(src_container, src_object,
286
                                   dst_container, dst_object)
287

    
288
    def move_object(self, src_container, src_object, dst_container,
289
                             dst_object):
290
        self._change_obj_location(src_container, src_object,
291
                                   dst_container, dst_object, True)
292

    
293
    def delete_object(self, container, object):
294
        self.delete('/%s/%s' % (container, object))
295

    
296
    def retrieve_object_metadata(self, container, object, restricted=False,
297
                                 version=None):
298
        path = '/%s/%s' % (container, object)
299
        prefix = restricted and 'x-object-meta-' or None
300
        params = version and {'version':version} or None
301
        return self._get_metadata(path, prefix, params=params)
302

    
303
    def update_object_metadata(self, container, object, **meta):
304
        path = '/%s/%s' % (container, object)
305
        self._set_metadata(path, 'object', **meta)