client support from COPY/MOVE from specific version
[pithos] / pithos / lib / client.py
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, headers=None):
274         path = '/%s/%s' % (dst_container, dst_object)
275         if not headers:
276             headers = {}
277         if remove:
278             headers['X-Move-From'] = '/%s/%s' % (src_container, src_object)
279         else:
280             headers['X-Copy-From'] = '/%s/%s' % (src_container, src_object)
281         headers['Content-Length'] = 0
282         self.put(path, headers=headers)
283
284     def copy_object(self, src_container, src_object, dst_container,
285                              dst_object, headers=None):
286         self._change_obj_location(src_container, src_object,
287                                    dst_container, dst_object, headers)
288
289     def move_object(self, src_container, src_object, dst_container,
290                              dst_object, headers=None):
291         self._change_obj_location(src_container, src_object,
292                                    dst_container, dst_object, True, headers)
293
294     def delete_object(self, container, object):
295         self.delete('/%s/%s' % (container, object))
296
297     def retrieve_object_metadata(self, container, object, restricted=False,
298                                  version=None):
299         path = '/%s/%s' % (container, object)
300         prefix = restricted and 'x-object-meta-' or None
301         params = version and {'version':version} or None
302         return self._get_metadata(path, prefix, params=params)
303
304     def update_object_metadata(self, container, object, **meta):
305         path = '/%s/%s' % (container, object)
306         self._set_metadata(path, 'object', **meta)