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
10 from pithos.api.compat import parse_http_date_safe
\r
12 from pithos.api.faults import Fault, NotModified, BadRequest, Unauthorized, ItemNotFound, Conflict, LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity
\r
13 from pithos.api.util import get_meta, get_range, api_method
\r
14 from pithos.backends.dummy import BackEnd
\r
20 from settings import PROJECT_PATH
\r
21 STORAGE_PATH = os.path.join(PROJECT_PATH, 'data')
\r
23 logger = logging.getLogger(__name__)
\r
26 def authenticate(request):
\r
27 # Normal Response Codes: 204
\r
28 # Error Response Codes: serviceUnavailable (503),
\r
29 # unauthorized (401),
\r
32 x_auth_user = request.META.get('HTTP_X_AUTH_USER')
\r
33 x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
\r
35 if not x_auth_user or not x_auth_key:
\r
36 raise BadRequest('Missing auth user or key.')
\r
38 response = HttpResponse(status = 204)
\r
39 response['X-Auth-Token'] = '0000'
\r
40 response['X-Storage-Url'] = os.path.join(request.build_absolute_uri(), 'demo')
\r
43 def account_demux(request, v_account):
\r
44 if request.method == 'HEAD':
\r
45 return account_meta(request, v_account)
\r
46 elif request.method == 'GET':
\r
47 return container_list(request, v_account)
\r
48 elif request.method == 'POST':
\r
49 return account_update(request, v_account)
\r
51 return method_not_allowed(request)
\r
53 def container_demux(request, v_account, v_container):
\r
54 if request.method == 'HEAD':
\r
55 return container_meta(request, v_account, v_container)
\r
56 elif request.method == 'GET':
\r
57 return object_list(request, v_account, v_container)
\r
58 elif request.method == 'PUT':
\r
59 return container_create(request, v_account, v_container)
\r
60 elif request.method == 'POST':
\r
61 return container_update(request, v_account, v_container)
\r
62 elif request.method == 'DELETE':
\r
63 return container_delete(request, v_account, v_container)
\r
65 return method_not_allowed(request)
\r
67 def object_demux(request, v_account, v_container, v_object):
\r
68 if request.method == 'HEAD':
\r
69 return object_meta(request, v_account, v_container, v_object)
\r
70 elif request.method == 'GET':
\r
71 return object_read(request, v_account, v_container, v_object)
\r
72 elif request.method == 'PUT':
\r
73 return object_write(request, v_account, v_container, v_object)
\r
74 elif request.method == 'COPY':
\r
75 return object_copy(request, v_account, v_container, v_object)
\r
76 elif request.method == 'POST':
\r
77 return object_update(request, v_account, v_container, v_object)
\r
78 elif request.method == 'DELETE':
\r
79 return object_delete(request, v_account, v_container, v_object)
\r
81 return method_not_allowed(request)
\r
84 def account_meta(request, v_account):
\r
85 # Normal Response Codes: 204
\r
86 # Error Response Codes: serviceUnavailable (503),
\r
87 # unauthorized (401),
\r
90 be = BackEnd(STORAGE_PATH)
\r
92 info = be.get_account_meta(request.user)
\r
94 info = {'count': 0, 'bytes': 0}
\r
96 response = HttpResponse(status = 204)
\r
97 response['X-Account-Container-Count'] = info['count']
\r
98 response['X-Account-Bytes-Used'] = info['bytes']
\r
99 for k in [x for x in info.keys() if x.startswith('X-Account-Meta-')]:
\r
100 response[k.encode('utf-8')] = info[k].encode('utf-8')
\r
104 @api_method('POST')
\r
105 def account_update(request, v_account):
\r
106 # Normal Response Codes: 202
\r
107 # Error Response Codes: serviceUnavailable (503),
\r
108 # itemNotFound (404),
\r
109 # unauthorized (401),
\r
112 meta = get_meta(request, 'X-Account-Meta-')
\r
114 be = BackEnd(STORAGE_PATH)
\r
115 be.update_account_meta(request.user, meta)
\r
117 return HttpResponse(status = 202)
\r
119 @api_method('GET', format_allowed = True)
\r
120 def container_list(request, v_account):
\r
121 # Normal Response Codes: 200, 204
\r
122 # Error Response Codes: serviceUnavailable (503),
\r
123 # itemNotFound (404),
\r
124 # unauthorized (401),
\r
127 marker = request.GET.get('marker')
\r
128 limit = request.GET.get('limit')
\r
135 be = BackEnd(STORAGE_PATH)
\r
137 containers = be.list_containers(request.user, marker, limit)
\r
141 if request.serialization == 'text':
\r
142 if len(containers) == 0:
\r
143 # The cloudfiles python bindings expect 200 if json/xml.
\r
144 return HttpResponse(status = 204)
\r
145 return HttpResponse('\n'.join(containers), status = 200)
\r
147 # TODO: Do this with a backend parameter?
\r
149 containers = [be.get_container_meta(request.user, x) for x in containers]
\r
151 raise ItemNotFound()
\r
152 if request.serialization == 'xml':
\r
153 data = render_to_string('containers.xml', {'account': request.user, 'containers': containers})
\r
154 elif request.serialization == 'json':
\r
155 data = json.dumps(containers)
\r
156 return HttpResponse(data, status = 200)
\r
158 @api_method('HEAD')
\r
159 def container_meta(request, v_account, v_container):
\r
160 # Normal Response Codes: 204
\r
161 # Error Response Codes: serviceUnavailable (503),
\r
162 # itemNotFound (404),
\r
163 # unauthorized (401),
\r
166 be = BackEnd(STORAGE_PATH)
\r
168 info = be.get_container_meta(request.user, v_container)
\r
170 raise ItemNotFound()
\r
172 response = HttpResponse(status = 204)
\r
173 response['X-Container-Object-Count'] = info['count']
\r
174 response['X-Container-Bytes-Used'] = info['bytes']
\r
175 for k in [x for x in info.keys() if x.startswith('X-Container-Meta-')]:
\r
176 response[k.encode('utf-8')] = info[k].encode('utf-8')
\r
181 def container_create(request, v_account, v_container):
\r
182 # Normal Response Codes: 201, 202
\r
183 # Error Response Codes: serviceUnavailable (503),
\r
184 # itemNotFound (404),
\r
185 # unauthorized (401),
\r
188 meta = get_meta(request, 'X-Container-Meta-')
\r
190 be = BackEnd(STORAGE_PATH)
\r
192 be.create_container(request.user, v_container)
\r
198 be.update_container_meta(request.user, v_container, meta)
\r
200 return HttpResponse(status = ret)
\r
202 @api_method('POST')
\r
203 def container_update(request, v_account, v_container):
\r
204 # Normal Response Codes: 202
\r
205 # Error Response Codes: serviceUnavailable (503),
\r
206 # itemNotFound (404),
\r
207 # unauthorized (401),
\r
210 meta = get_meta(request, 'X-Container-Meta-')
\r
212 be = BackEnd(STORAGE_PATH)
\r
214 be.update_container_meta(request.user, v_container, meta)
\r
216 raise ItemNotFound()
\r
218 return HttpResponse(status = 202)
\r
220 @api_method('DELETE')
\r
221 def container_delete(request, v_account, v_container):
\r
222 # Normal Response Codes: 204
\r
223 # Error Response Codes: serviceUnavailable (503),
\r
225 # itemNotFound (404),
\r
226 # unauthorized (401),
\r
229 be = BackEnd(STORAGE_PATH)
\r
231 be.delete_container(request.user, v_container)
\r
233 raise ItemNotFound()
\r
236 return HttpResponse(status = 204)
\r
238 @api_method('GET', format_allowed = True)
\r
239 def object_list(request, v_account, v_container):
\r
240 # Normal Response Codes: 200, 204
\r
241 # Error Response Codes: serviceUnavailable (503),
\r
242 # itemNotFound (404),
\r
243 # unauthorized (401),
\r
246 path = request.GET.get('path')
\r
247 prefix = request.GET.get('prefix')
\r
248 delimiter = request.GET.get('delimiter')
\r
250 # TODO: Check if the cloudfiles python bindings expect the results with the prefix.
\r
251 # Path overrides prefix and delimiter.
\r
256 if prefix and delimiter:
\r
257 prefix = prefix + delimiter
\r
261 marker = request.GET.get('marker')
\r
262 limit = request.GET.get('limit')
\r
269 be = BackEnd(STORAGE_PATH)
\r
271 objects = be.list_objects(request.user, v_container, prefix, delimiter, marker, limit)
\r
273 raise ItemNotFound()
\r
275 if request.serialization == 'text':
\r
276 if len(objects) == 0:
\r
277 # The cloudfiles python bindings expect 200 if json/xml.
\r
278 return HttpResponse(status = 204)
\r
279 return HttpResponse('\n'.join(objects), status = 200)
\r
281 # TODO: Do this with a backend parameter?
\r
283 objects = [be.get_object_meta(request.user, v_container, x) for x in objects]
\r
285 raise ItemNotFound()
\r
288 if x.has_key('last_modified'):
\r
289 x['last_modified'] = datetime.datetime.fromtimestamp(x['last_modified']).isoformat()
\r
290 if request.serialization == 'xml':
\r
291 data = render_to_string('objects.xml', {'container': v_container, 'objects': objects})
\r
292 elif request.serialization == 'json':
\r
293 data = json.dumps(objects)
\r
294 return HttpResponse(data, status = 200)
\r
296 @api_method('HEAD')
\r
297 def object_meta(request, v_account, v_container, v_object):
\r
298 # Normal Response Codes: 204
\r
299 # Error Response Codes: serviceUnavailable (503),
\r
300 # itemNotFound (404),
\r
301 # unauthorized (401),
\r
304 be = BackEnd(STORAGE_PATH)
\r
306 info = be.get_object_meta(request.user, v_container, v_object)
\r
308 raise ItemNotFound()
\r
310 response = HttpResponse(status = 204)
\r
311 response['ETag'] = info['hash']
\r
312 response['Content-Length'] = info['bytes']
\r
313 response['Content-Type'] = info['content_type']
\r
314 response['Last-Modified'] = http_date(info['last_modified'])
\r
315 for k in [x for x in info.keys() if x.startswith('X-Object-Meta-')]:
\r
316 response[k.encode('utf-8')] = info[k].encode('utf-8')
\r
321 def object_read(request, v_account, v_container, v_object):
\r
322 # Normal Response Codes: 200, 206
\r
323 # Error Response Codes: serviceUnavailable (503),
\r
324 # rangeNotSatisfiable (416),
\r
325 # preconditionFailed (412),
\r
326 # itemNotFound (404),
\r
327 # unauthorized (401),
\r
328 # badRequest (400),
\r
329 # notModified (304)
\r
331 be = BackEnd(STORAGE_PATH)
\r
333 info = be.get_object_meta(request.user, v_container, v_object)
\r
335 raise ItemNotFound()
\r
337 # TODO: Check if the cloudfiles python bindings expect hash/content_type/last_modified on range requests.
\r
338 response = HttpResponse()
\r
339 response['ETag'] = info['hash']
\r
340 response['Content-Type'] = info['content_type']
\r
341 response['Last-Modified'] = http_date(info['last_modified'])
\r
344 range = get_range(request)
\r
345 if range is not None:
\r
346 offset, length = range
\r
348 if offset + length > info['bytes']:
\r
349 raise RangeNotSatisfiable()
\r
351 if offset > info['bytes']:
\r
352 raise RangeNotSatisfiable()
\r
356 response['Content-Length'] = length
\r
357 response.status_code = 206
\r
362 response['Content-Length'] = info['bytes']
\r
363 response.status_code = 200
\r
365 # Conditions (according to RFC2616 must be evaluated at the end).
\r
366 # TODO: Check etag/date conditions.
\r
367 if_match = request.META.get('HTTP_IF_MATCH')
\r
368 if if_match is not None and if_match != '*':
\r
369 if info['hash'] not in parse_etags(if_match):
\r
370 raise PreconditionFailed()
\r
372 if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
\r
373 if if_none_match is not None:
\r
374 if if_none_match == '*' or info['hash'] in parse_etags(if_none_match):
\r
375 raise NotModified()
\r
377 if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
\r
378 if if_modified_since is not None:
\r
379 if_modified_since = parse_http_date_safe(if_modified_since)
\r
380 if if_modified_since is not None and info['last_modified'] <= if_modified_since:
\r
381 raise NotModified()
\r
383 if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
\r
384 if if_unmodified_since is not None:
\r
385 if_unmodified_since = parse_http_date_safe(if_unmodified_since)
\r
386 if if_unmodified_since is not None and info['last_modified'] > if_unmodified_since:
\r
387 raise PreconditionFailed()
\r
390 response.content = be.get_object(request.user, v_container, v_object, offset, length)
\r
392 raise ItemNotFound()
\r
397 def object_write(request, v_account, v_container, v_object):
\r
398 # Normal Response Codes: 201
\r
399 # Error Response Codes: serviceUnavailable (503),
\r
400 # unprocessableEntity (422),
\r
401 # lengthRequired (411),
\r
402 # itemNotFound (404),
\r
403 # unauthorized (401),
\r
406 be = BackEnd(STORAGE_PATH)
\r
408 copy_from = request.META.get('HTTP_X_COPY_FROM')
\r
410 parts = copy_from.split('/')
\r
411 if len(parts) < 3 or parts[0] != '':
\r
412 raise BadRequest('Bad X-Copy-From path.')
\r
413 copy_container = parts[1]
\r
414 copy_name = '/'.join(parts[2:])
\r
417 info = be.get_object_meta(request.user, copy_container, copy_name)
\r
419 raise ItemNotFound()
\r
421 content_length = request.META.get('CONTENT_LENGTH')
\r
422 content_type = request.META.get('CONTENT_TYPE')
\r
423 # TODO: Why is this required? Copy this ammount?
\r
424 if not content_length:
\r
425 raise LengthRequired()
\r
427 info['content_type'] = content_type
\r
429 meta = get_meta(request, 'X-Object-Meta-')
\r
433 be.copy_object(request.user, copy_container, copy_name, v_container, v_object)
\r
434 be.update_object_meta(request.user, v_container, v_object, info)
\r
436 raise ItemNotFound()
\r
438 response = HttpResponse(status = 201)
\r
440 content_length = request.META.get('CONTENT_LENGTH')
\r
441 content_type = request.META.get('CONTENT_TYPE')
\r
442 if not content_length or not content_type:
\r
443 raise LengthRequired()
\r
445 info = {'content_type': content_type}
\r
446 meta = get_meta(request, 'X-Object-Meta-')
\r
449 data = request.raw_post_data
\r
451 be.update_object(request.user, v_container, v_object, data)
\r
452 be.update_object_meta(request.user, v_container, v_object, info)
\r
454 raise ItemNotFound()
\r
456 # TODO: Check before update?
\r
457 info = be.get_object_meta(request.user, v_container, v_object)
\r
458 etag = request.META.get('HTTP_ETAG')
\r
460 etag = parse_etags(etag)[0] # TODO: Unescape properly.
\r
461 if etag != info['hash']:
\r
462 be.delete_object(request.user, v_container, v_object)
\r
463 raise UnprocessableEntity()
\r
465 response = HttpResponse(status = 201)
\r
466 response['ETag'] = info['hash']
\r
470 @api_method('COPY')
\r
471 def object_copy(request, v_account, v_container, v_object):
\r
472 # Normal Response Codes: 201
\r
473 # Error Response Codes: serviceUnavailable (503),
\r
474 # itemNotFound (404),
\r
475 # unauthorized (401),
\r
478 destination = request.META.get('HTTP_DESTINATION')
\r
479 if not destination:
\r
480 raise BadRequest('Missing Destination.');
\r
482 parts = destination.split('/')
\r
483 if len(parts) < 3 or parts[0] != '':
\r
484 raise BadRequest('Bad Destination path.')
\r
485 dest_container = parts[1]
\r
486 dest_name = '/'.join(parts[2:])
\r
488 be = BackEnd(STORAGE_PATH)
\r
490 info = be.get_object_meta(request.user, v_container, v_object)
\r
492 raise ItemNotFound()
\r
494 content_type = request.META.get('CONTENT_TYPE')
\r
496 info['content_type'] = content_type
\r
497 meta = get_meta(request, 'X-Object-Meta-')
\r
501 be.copy_object(request.user, v_container, v_object, dest_container, dest_name)
\r
502 be.update_object_meta(request.user, dest_container, dest_name, info)
\r
504 raise ItemNotFound()
\r
506 response = HttpResponse(status = 201)
\r
508 @api_method('POST')
\r
509 def object_update(request, v_account, v_container, v_object):
\r
510 # Normal Response Codes: 202
\r
511 # Error Response Codes: serviceUnavailable (503),
\r
512 # itemNotFound (404),
\r
513 # unauthorized (401),
\r
516 meta = get_meta(request, 'X-Object-Meta-')
\r
518 be = BackEnd(STORAGE_PATH)
\r
520 be.update_object_meta(request.user, v_container, v_object, meta)
\r
522 raise ItemNotFound()
\r
524 return HttpResponse(status = 202)
\r
526 @api_method('DELETE')
\r
527 def object_delete(request, v_account, v_container, v_object):
\r
528 # Normal Response Codes: 204
\r
529 # Error Response Codes: serviceUnavailable (503),
\r
530 # itemNotFound (404),
\r
531 # unauthorized (401),
\r
534 be = BackEnd(STORAGE_PATH)
\r
536 be.delete_object(request.user, v_container, v_object)
\r
538 raise ItemNotFound()
\r
539 return HttpResponse(status = 204)
\r
542 def method_not_allowed(request):
\r
543 raise BadRequest('Method not allowed.')
\r