2 # Copyright (c) 2011 Greek Research and Technology Network
\r
5 from django.http import HttpResponse
\r
6 from django.template.loader import render_to_string
\r
7 from django.utils import simplejson as json
\r
8 from django.utils.http import http_date, parse_etags
\r
11 from django.utils.http import parse_http_date_safe
\r
13 from pithos.api.util import parse_http_date_safe
\r
15 from pithos.api.faults import Fault, NotModified, BadRequest, Unauthorized, ItemNotFound, LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity
\r
16 from pithos.api.util import get_meta, get_range, api_method
\r
18 from settings import PROJECT_PATH
\r
20 STORAGE_PATH = path.join(PROJECT_PATH, 'data')
\r
22 from pithos.backends.dummy import BackEnd
\r
26 logging.basicConfig(level=logging.DEBUG)
\r
29 def authenticate(request):
\r
30 # Normal Response Codes: 204
\r
31 # Error Response Codes: serviceUnavailable (503),
\r
32 # unauthorized (401),
\r
35 x_auth_user = request.META.get('HTTP_X_AUTH_USER')
\r
36 x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
\r
38 if not x_auth_user or not x_auth_key:
\r
39 raise BadRequest('Missing auth user or key.')
\r
41 response = HttpResponse(status = 204)
\r
42 response['X-Auth-Token'] = 'eaaafd18-0fed-4b3a-81b4-663c99ec1cbb'
\r
43 # TODO: Must support X-Storage-Url to be compatible.
\r
44 response['X-Storage-Url'] = 'http://127.0.0.1:8000/v1/asdf'
\r
47 def account_demux(request, v_account):
\r
48 if request.method == 'HEAD':
\r
49 return account_meta(request, v_account)
\r
50 elif request.method == 'GET':
\r
51 return container_list(request, v_account)
\r
52 elif request.method == 'POST':
\r
53 return account_update(request, v_account)
\r
55 return method_not_allowed(request)
\r
57 def container_demux(request, v_account, v_container):
\r
58 if request.method == 'HEAD':
\r
59 return container_meta(request, v_account, v_container)
\r
60 elif request.method == 'GET':
\r
61 return object_list(request, v_account, v_container)
\r
62 elif request.method == 'PUT':
\r
63 return container_create(request, v_account, v_container)
\r
64 elif request.method == 'POST':
\r
65 return container_update(request, v_account, v_container)
\r
66 elif request.method == 'DELETE':
\r
67 return container_delete(request, v_account, v_container)
\r
69 return method_not_allowed(request)
\r
71 def object_demux(request, v_account, v_container, v_object):
\r
72 if request.method == 'HEAD':
\r
73 return object_meta(request, v_account, v_container, v_object)
\r
74 elif request.method == 'GET':
\r
75 return object_read(request, v_account, v_container, v_object)
\r
76 elif request.method == 'PUT':
\r
77 return object_write(request, v_account, v_container, v_object)
\r
78 elif request.method == 'COPY':
\r
79 return object_copy(request, v_account, v_container, v_object)
\r
80 elif request.method == 'POST':
\r
81 return object_update(request, v_account, v_container, v_object)
\r
82 elif request.method == 'DELETE':
\r
83 return object_delete(request, v_account, v_container, v_object)
\r
85 return method_not_allowed(request)
\r
88 def account_meta(request, v_account):
\r
89 # Normal Response Codes: 204
\r
90 # Error Response Codes: serviceUnavailable (503),
\r
91 # unauthorized (401),
\r
94 be = BackEnd(STORAGE_PATH)
\r
96 info = be.get_account_meta(request.user)
\r
98 info = {'count': 0, 'bytes': 0}
\r
100 response = HttpResponse(status = 204)
\r
101 response['X-Account-Container-Count'] = info['count']
\r
102 response['X-Account-Bytes-Used'] = info['bytes']
\r
103 for k in [x for x in info.keys() if x.startswith('X-Account-Meta-')]:
\r
104 response[k] = info[k]
\r
108 @api_method('POST')
\r
109 def account_update(request, v_account):
\r
110 # Normal Response Codes: 202
\r
111 # Error Response Codes: serviceUnavailable (503),
\r
112 # itemNotFound (404),
\r
113 # unauthorized (401),
\r
116 meta = get_meta(request, 'X-Account-Meta-')
\r
118 be = BackEnd(STORAGE_PATH)
\r
119 be.update_account_meta(request.user, meta)
\r
121 return HttpResponse(status = 202)
\r
123 @api_method('GET', format_allowed = True)
\r
124 def container_list(request, v_account):
\r
125 # Normal Response Codes: 200, 204
\r
126 # Error Response Codes: serviceUnavailable (503),
\r
127 # itemNotFound (404),
\r
128 # unauthorized (401),
\r
131 marker = request.GET.get('marker')
\r
132 limit = request.GET.get('limit')
\r
139 be = BackEnd(STORAGE_PATH)
\r
141 containers = be.list_containers(request.user, marker, limit)
\r
144 # TODO: The cloudfiles python bindings expect 200 if json/xml.
\r
145 if len(containers) == 0:
\r
146 return HttpResponse(status = 204)
\r
148 if request.serialization == 'text':
\r
149 return HttpResponse('\n'.join(containers), status = 200)
\r
151 # TODO: Do this with a backend parameter?
\r
153 containers = [be.get_container_meta(request.user, x) for x in containers]
\r
155 raise ItemNotFound()
\r
156 # TODO: Format dates.
\r
157 if request.serialization == 'xml':
\r
158 data = render_to_string('containers.xml', {'account': request.user, 'containers': containers})
\r
159 elif request.serialization == 'json':
\r
160 data = json.dumps(containers)
\r
161 return HttpResponse(data, status = 200)
\r
163 @api_method('HEAD')
\r
164 def container_meta(request, v_account, v_container):
\r
165 # Normal Response Codes: 204
\r
166 # Error Response Codes: serviceUnavailable (503),
\r
167 # itemNotFound (404),
\r
168 # unauthorized (401),
\r
171 be = BackEnd(STORAGE_PATH)
\r
173 info = be.get_container_meta(request.user, v_container)
\r
175 raise ItemNotFound()
\r
177 response = HttpResponse(status = 204)
\r
178 response['X-Container-Object-Count'] = info['count']
\r
179 response['X-Container-Bytes-Used'] = info['bytes']
\r
180 for k in [x for x in info.keys() if x.startswith('X-Container-Meta-')]:
\r
181 response[k] = info[k]
\r
186 def container_create(request, v_account, v_container):
\r
187 # Normal Response Codes: 201, 202
\r
188 # Error Response Codes: serviceUnavailable (503),
\r
189 # itemNotFound (404),
\r
190 # unauthorized (401),
\r
193 meta = get_meta(request, 'X-Container-Meta-')
\r
195 be = BackEnd(STORAGE_PATH)
\r
197 be.create_container(request.user, v_container)
\r
203 be.update_container_meta(request.user, v_container, meta)
\r
205 return HttpResponse(status = ret)
\r
207 @api_method('POST')
\r
208 def container_update(request, v_account, v_container):
\r
209 # Normal Response Codes: 202
\r
210 # Error Response Codes: serviceUnavailable (503),
\r
211 # itemNotFound (404),
\r
212 # unauthorized (401),
\r
215 meta = get_meta(request, 'X-Container-Meta-')
\r
217 be = BackEnd(STORAGE_PATH)
\r
219 be.update_container_meta(request.user, v_container, meta)
\r
221 raise ItemNotFound()
\r
223 return HttpResponse(status = 202)
\r
225 @api_method('DELETE')
\r
226 def container_delete(request, v_account, v_container):
\r
227 # Normal Response Codes: 204
\r
228 # Error Response Codes: serviceUnavailable (503),
\r
229 # itemNotFound (404),
\r
230 # unauthorized (401),
\r
233 be = BackEnd(STORAGE_PATH)
\r
235 info = be.get_container_meta(request.user, v_container)
\r
237 raise ItemNotFound()
\r
239 if info['count'] > 0:
\r
240 return HttpResponse(status = 409)
\r
242 # TODO: Handle both exceptions.
\r
244 be.delete_container(request.user, v_container)
\r
246 raise ItemNotFound()
\r
247 return HttpResponse(status = 204)
\r
249 @api_method('GET', format_allowed = True)
\r
250 def object_list(request, v_account, v_container):
\r
251 # Normal Response Codes: 200, 204
\r
252 # Error Response Codes: serviceUnavailable (503),
\r
253 # itemNotFound (404),
\r
254 # unauthorized (401),
\r
257 path = request.GET.get('path')
\r
258 prefix = request.GET.get('prefix')
\r
259 delimiter = request.GET.get('delimiter')
\r
261 # Path overrides prefix and delimiter.
\r
266 if prefix and delimiter:
\r
267 prefix = prefix + delimiter
\r
271 marker = request.GET.get('marker')
\r
272 limit = request.GET.get('limit')
\r
279 be = BackEnd(STORAGE_PATH)
\r
281 objects = be.list_objects(request.user, v_container, prefix, delimiter, marker, limit)
\r
283 raise ItemNotFound()
\r
284 # TODO: The cloudfiles python bindings expect 200 if json/xml.
\r
285 if len(objects) == 0:
\r
286 return HttpResponse(status = 204)
\r
288 if request.serialization == 'text':
\r
289 return HttpResponse('\n'.join(objects), status = 200)
\r
291 # TODO: Do this with a backend parameter?
\r
293 objects = [be.get_object_meta(request.user, v_container, x) for x in objects]
\r
295 raise ItemNotFound()
\r
296 # TODO: Format dates.
\r
297 if request.serialization == 'xml':
\r
298 data = render_to_string('objects.xml', {'container': v_container, 'objects': objects})
\r
299 elif request.serialization == 'json':
\r
300 data = json.dumps(objects)
\r
301 return HttpResponse(data, status = 200)
\r
303 @api_method('HEAD')
\r
304 def object_meta(request, v_account, v_container, v_object):
\r
305 # Normal Response Codes: 204
\r
306 # Error Response Codes: serviceUnavailable (503),
\r
307 # itemNotFound (404),
\r
308 # unauthorized (401),
\r
311 be = BackEnd(STORAGE_PATH)
\r
313 info = be.get_object_meta(request.user, v_container, v_object)
\r
315 raise ItemNotFound()
\r
317 response = HttpResponse(status = 204)
\r
318 response['ETag'] = info['hash']
\r
319 response['Content-Length'] = info['bytes']
\r
320 response['Content-Type'] = info['content_type']
\r
321 response['Last-Modified'] = http_date(info['last_modified'])
\r
322 for k in [x for x in info.keys() if x.startswith('X-Object-Meta-')]:
\r
323 response[k] = info[k]
\r
328 def object_read(request, v_account, v_container, v_object):
\r
329 # Normal Response Codes: 200, 206
\r
330 # Error Response Codes: serviceUnavailable (503),
\r
331 # rangeNotSatisfiable (416),
\r
332 # preconditionFailed (412),
\r
333 # itemNotFound (404),
\r
334 # unauthorized (401),
\r
335 # badRequest (400),
\r
336 # notModified (304)
\r
338 be = BackEnd(STORAGE_PATH)
\r
340 info = be.get_object_meta(request.user, v_container, v_object)
\r
342 raise ItemNotFound()
\r
344 # TODO: Check if the cloudfiles python bindings expect hash/content_type/last_modified on range requests.
\r
345 response = HttpResponse()
\r
346 response['ETag'] = info['hash']
\r
347 response['Content-Type'] = info['content_type']
\r
348 response['Last-Modified'] = http_date(info['last_modified'])
\r
351 range = get_range(request)
\r
352 if range is not None:
\r
353 offset, length = range
\r
355 if offset + length > info['bytes']:
\r
356 raise RangeNotSatisfiable()
\r
358 if offset > info['bytes']:
\r
359 raise RangeNotSatisfiable()
\r
363 response['Content-Length'] = length
\r
364 response.status_code = 206
\r
369 response['Content-Length'] = info['bytes']
\r
370 response.status_code = 200
\r
372 # Conditions (according to RFC2616 must be evaluated at the end).
\r
373 # TODO: Check etag/date conditions.
\r
374 if_match = request.META.get('HTTP_IF_MATCH')
\r
375 if if_match is not None and if_match != '*':
\r
376 if info['hash'] not in parse_etags(if_match):
\r
377 raise PreconditionFailed()
\r
379 if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
\r
380 if if_none_match is not None:
\r
381 if if_none_match == '*' or info['hash'] in parse_etags(if_none_match):
\r
382 raise NotModified()
\r
384 if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
\r
385 if if_modified_since is not None:
\r
386 if_modified_since = parse_http_date_safe(if_modified_since)
\r
387 if if_modified_since is not None and info['last_modified'] <= if_modified_since:
\r
388 raise NotModified()
\r
390 if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
\r
391 if if_unmodified_since is not None:
\r
392 if_unmodified_since = parse_http_date_safe(if_unmodified_since)
\r
393 if if_unmodified_since is not None and info['last_modified'] > if_unmodified_since:
\r
394 raise PreconditionFailed()
\r
397 response.content = be.get_object(request.user, v_container, v_object, offset, length)
\r
399 raise ItemNotFound()
\r
404 def object_write(request, v_account, v_container, v_object):
\r
405 # Normal Response Codes: 201
\r
406 # Error Response Codes: serviceUnavailable (503),
\r
407 # unprocessableEntity (422),
\r
408 # lengthRequired (411),
\r
409 # itemNotFound (404),
\r
410 # unauthorized (401),
\r
413 be = BackEnd(STORAGE_PATH)
\r
415 copy_from = request.META.get('HTTP_X_COPY_FROM')
\r
417 parts = copy_from.split('/')
\r
418 if len(parts) < 3 or parts[0] != '':
\r
419 raise BadRequest('Bad X-Copy-From path.')
\r
420 copy_container = parts[1]
\r
421 copy_name = '/'.join(parts[2:])
\r
424 info = be.get_object_meta(request.user, copy_container, copy_name)
\r
426 raise ItemNotFound()
\r
428 content_length = request.META.get('CONTENT_LENGTH')
\r
429 content_type = request.META.get('CONTENT_TYPE')
\r
430 # TODO: Why is this required? Copy this ammount?
\r
431 if not content_length:
\r
432 raise LengthRequired()
\r
434 info['content_type'] = content_type
\r
436 meta = get_meta(request, 'X-Object-Meta-')
\r
440 be.copy_object(request.user, copy_container, copy_name, v_container, v_object)
\r
441 be.update_object_meta(request.user, v_container, v_object, info)
\r
443 raise ItemNotFound()
\r
445 response = HttpResponse(status = 201)
\r
447 content_length = request.META.get('CONTENT_LENGTH')
\r
448 content_type = request.META.get('CONTENT_TYPE')
\r
449 if not content_length or not content_type:
\r
450 raise LengthRequired()
\r
452 info = {'content_type': content_type}
\r
453 meta = get_meta(request, 'X-Object-Meta-')
\r
456 data = request.raw_post_data
\r
458 be.update_object(request.user, v_container, v_object, data)
\r
459 be.update_object_meta(request.user, v_container, v_object, info)
\r
461 raise ItemNotFound()
\r
463 # TODO: Check before update?
\r
464 info = be.get_object_meta(request.user, v_container, v_object)
\r
465 etag = request.META.get('HTTP_ETAG')
\r
467 etag = parse_etags(etag)[0] # TODO: Unescape properly.
\r
468 if etag != info['hash']:
\r
469 be.delete_object(request.user, v_container, v_object)
\r
470 raise UnprocessableEntity()
\r
472 response = HttpResponse(status = 201)
\r
473 response['ETag'] = info['hash']
\r
477 @api_method('COPY')
\r
478 def object_copy(request, v_account, v_container, v_object):
\r
479 # Normal Response Codes: 201
\r
480 # Error Response Codes: serviceUnavailable (503),
\r
481 # itemNotFound (404),
\r
482 # unauthorized (401),
\r
485 destination = request.META.get('HTTP_DESTINATION')
\r
486 if not destination:
\r
487 raise BadRequest('Missing Destination.');
\r
489 parts = destination.split('/')
\r
490 if len(parts) < 3 or parts[0] != '':
\r
491 raise BadRequest('Bad Destination path.')
\r
492 dest_container = parts[1]
\r
493 dest_name = '/'.join(parts[2:])
\r
495 be = BackEnd(STORAGE_PATH)
\r
497 info = be.get_object_meta(request.user, v_container, v_object)
\r
499 raise ItemNotFound()
\r
501 content_type = request.META.get('CONTENT_TYPE')
\r
503 info['content_type'] = content_type
\r
504 meta = get_meta(request, 'X-Object-Meta-')
\r
508 be.copy_object(request.user, v_container, v_object, dest_container, dest_name)
\r
509 be.update_object_meta(request.user, dest_container, dest_name, info)
\r
511 raise ItemNotFound()
\r
513 response = HttpResponse(status = 201)
\r
515 @api_method('POST')
\r
516 def object_update(request, v_account, v_container, v_object):
\r
517 # Normal Response Codes: 202
\r
518 # Error Response Codes: serviceUnavailable (503),
\r
519 # itemNotFound (404),
\r
520 # unauthorized (401),
\r
523 return HttpResponse(status = 202)
\r
525 @api_method('DELETE')
\r
526 def object_delete(request, v_account, v_container, v_object):
\r
527 # Normal Response Codes: 204
\r
528 # Error Response Codes: serviceUnavailable (503),
\r
529 # itemNotFound (404),
\r
530 # unauthorized (401),
\r
533 be = BackEnd(STORAGE_PATH)
\r
535 be.delete_object(request.user, v_container, v_object)
\r
537 raise ItemNotFound()
\r
538 return HttpResponse(status = 204)
\r
541 def method_not_allowed(request):
\r
542 raise BadRequest('Method not allowed.')
\r