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.compat import parse_http_date_safe
\r
15 from pithos.api.faults import Fault, NotModified, BadRequest, Unauthorized, ItemNotFound, Conflict, LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity
\r
16 from pithos.api.util import get_meta, get_range, api_method
\r
17 from pithos.backends.dummy import BackEnd
\r
23 from settings import PROJECT_PATH
\r
24 STORAGE_PATH = os.path.join(PROJECT_PATH, 'data')
\r
26 logger = logging.getLogger(__name__)
\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'] = '0000'
\r
43 response['X-Storage-Url'] = os.path.join(request.build_absolute_uri(), 'demo')
\r
46 def account_demux(request, v_account):
\r
47 if request.method == 'HEAD':
\r
48 return account_meta(request, v_account)
\r
49 elif request.method == 'GET':
\r
50 return container_list(request, v_account)
\r
51 elif request.method == 'POST':
\r
52 return account_update(request, v_account)
\r
54 return method_not_allowed(request)
\r
56 def container_demux(request, v_account, v_container):
\r
57 if request.method == 'HEAD':
\r
58 return container_meta(request, v_account, v_container)
\r
59 elif request.method == 'GET':
\r
60 return object_list(request, v_account, v_container)
\r
61 elif request.method == 'PUT':
\r
62 return container_create(request, v_account, v_container)
\r
63 elif request.method == 'POST':
\r
64 return container_update(request, v_account, v_container)
\r
65 elif request.method == 'DELETE':
\r
66 return container_delete(request, v_account, v_container)
\r
68 return method_not_allowed(request)
\r
70 def object_demux(request, v_account, v_container, v_object):
\r
71 if request.method == 'HEAD':
\r
72 return object_meta(request, v_account, v_container, v_object)
\r
73 elif request.method == 'GET':
\r
74 return object_read(request, v_account, v_container, v_object)
\r
75 elif request.method == 'PUT':
\r
76 return object_write(request, v_account, v_container, v_object)
\r
77 elif request.method == 'COPY':
\r
78 return object_copy(request, v_account, v_container, v_object)
\r
79 elif request.method == 'POST':
\r
80 return object_update(request, v_account, v_container, v_object)
\r
81 elif request.method == 'DELETE':
\r
82 return object_delete(request, v_account, v_container, v_object)
\r
84 return method_not_allowed(request)
\r
87 def account_meta(request, v_account):
\r
88 # Normal Response Codes: 204
\r
89 # Error Response Codes: serviceUnavailable (503),
\r
90 # unauthorized (401),
\r
93 be = BackEnd(STORAGE_PATH)
\r
95 info = be.get_account_meta(request.user)
\r
97 info = {'count': 0, 'bytes': 0}
\r
99 response = HttpResponse(status = 204)
\r
100 response['X-Account-Container-Count'] = info['count']
\r
101 response['X-Account-Bytes-Used'] = info['bytes']
\r
102 for k in [x for x in info.keys() if x.startswith('X-Account-Meta-')]:
\r
103 response[k.encode('utf-8')] = info[k].encode('utf-8')
\r
107 @api_method('POST')
\r
108 def account_update(request, v_account):
\r
109 # Normal Response Codes: 202
\r
110 # Error Response Codes: serviceUnavailable (503),
\r
111 # itemNotFound (404),
\r
112 # unauthorized (401),
\r
115 meta = get_meta(request, 'X-Account-Meta-')
\r
117 be = BackEnd(STORAGE_PATH)
\r
118 be.update_account_meta(request.user, meta)
\r
120 return HttpResponse(status = 202)
\r
122 @api_method('GET', format_allowed = True)
\r
123 def container_list(request, v_account):
\r
124 # Normal Response Codes: 200, 204
\r
125 # Error Response Codes: serviceUnavailable (503),
\r
126 # itemNotFound (404),
\r
127 # unauthorized (401),
\r
130 marker = request.GET.get('marker')
\r
131 limit = request.GET.get('limit')
\r
138 be = BackEnd(STORAGE_PATH)
\r
140 containers = be.list_containers(request.user, marker, limit)
\r
144 if request.serialization == 'text':
\r
145 if len(containers) == 0:
\r
146 # The cloudfiles python bindings expect 200 if json/xml.
\r
147 return HttpResponse(status = 204)
\r
148 return HttpResponse('\n'.join(containers), status = 200)
\r
150 # TODO: Do this with a backend parameter?
\r
152 containers = [be.get_container_meta(request.user, x) for x in containers]
\r
154 raise ItemNotFound()
\r
155 if request.serialization == 'xml':
\r
156 data = render_to_string('containers.xml', {'account': request.user, 'containers': containers})
\r
157 elif request.serialization == 'json':
\r
158 data = json.dumps(containers)
\r
159 return HttpResponse(data, status = 200)
\r
161 @api_method('HEAD')
\r
162 def container_meta(request, v_account, v_container):
\r
163 # Normal Response Codes: 204
\r
164 # Error Response Codes: serviceUnavailable (503),
\r
165 # itemNotFound (404),
\r
166 # unauthorized (401),
\r
169 be = BackEnd(STORAGE_PATH)
\r
171 info = be.get_container_meta(request.user, v_container)
\r
173 raise ItemNotFound()
\r
175 response = HttpResponse(status = 204)
\r
176 response['X-Container-Object-Count'] = info['count']
\r
177 response['X-Container-Bytes-Used'] = info['bytes']
\r
178 for k in [x for x in info.keys() if x.startswith('X-Container-Meta-')]:
\r
179 response[k.encode('utf-8')] = info[k].encode('utf-8')
\r
184 def container_create(request, v_account, v_container):
\r
185 # Normal Response Codes: 201, 202
\r
186 # Error Response Codes: serviceUnavailable (503),
\r
187 # itemNotFound (404),
\r
188 # unauthorized (401),
\r
191 meta = get_meta(request, 'X-Container-Meta-')
\r
193 be = BackEnd(STORAGE_PATH)
\r
195 be.create_container(request.user, v_container)
\r
201 be.update_container_meta(request.user, v_container, meta)
\r
203 return HttpResponse(status = ret)
\r
205 @api_method('POST')
\r
206 def container_update(request, v_account, v_container):
\r
207 # Normal Response Codes: 202
\r
208 # Error Response Codes: serviceUnavailable (503),
\r
209 # itemNotFound (404),
\r
210 # unauthorized (401),
\r
213 meta = get_meta(request, 'X-Container-Meta-')
\r
215 be = BackEnd(STORAGE_PATH)
\r
217 be.update_container_meta(request.user, v_container, meta)
\r
219 raise ItemNotFound()
\r
221 return HttpResponse(status = 202)
\r
223 @api_method('DELETE')
\r
224 def container_delete(request, v_account, v_container):
\r
225 # Normal Response Codes: 204
\r
226 # Error Response Codes: serviceUnavailable (503),
\r
228 # itemNotFound (404),
\r
229 # unauthorized (401),
\r
232 be = BackEnd(STORAGE_PATH)
\r
234 be.delete_container(request.user, v_container)
\r
236 raise ItemNotFound()
\r
239 return HttpResponse(status = 204)
\r
241 @api_method('GET', format_allowed = True)
\r
242 def object_list(request, v_account, v_container):
\r
243 # Normal Response Codes: 200, 204
\r
244 # Error Response Codes: serviceUnavailable (503),
\r
245 # itemNotFound (404),
\r
246 # unauthorized (401),
\r
249 path = request.GET.get('path')
\r
250 prefix = request.GET.get('prefix')
\r
251 delimiter = request.GET.get('delimiter')
\r
253 # TODO: Check if the cloudfiles python bindings expect the results with the prefix.
\r
254 # Path overrides prefix and delimiter.
\r
259 if prefix and delimiter:
\r
260 prefix = prefix + delimiter
\r
264 marker = request.GET.get('marker')
\r
265 limit = request.GET.get('limit')
\r
272 be = BackEnd(STORAGE_PATH)
\r
274 objects = be.list_objects(request.user, v_container, prefix, delimiter, marker, limit)
\r
276 raise ItemNotFound()
\r
278 if request.serialization == 'text':
\r
279 if len(objects) == 0:
\r
280 # The cloudfiles python bindings expect 200 if json/xml.
\r
281 return HttpResponse(status = 204)
\r
282 return HttpResponse('\n'.join(objects), status = 200)
\r
284 # TODO: Do this with a backend parameter?
\r
286 objects = [be.get_object_meta(request.user, v_container, x) for x in objects]
\r
288 raise ItemNotFound()
\r
291 if x.has_key('last_modified'):
\r
292 x['last_modified'] = datetime.datetime.fromtimestamp(x['last_modified']).isoformat()
\r
293 if request.serialization == 'xml':
\r
294 data = render_to_string('objects.xml', {'container': v_container, 'objects': objects})
\r
295 elif request.serialization == 'json':
\r
296 data = json.dumps(objects)
\r
297 return HttpResponse(data, status = 200)
\r
299 @api_method('HEAD')
\r
300 def object_meta(request, v_account, v_container, v_object):
\r
301 # Normal Response Codes: 204
\r
302 # Error Response Codes: serviceUnavailable (503),
\r
303 # itemNotFound (404),
\r
304 # unauthorized (401),
\r
307 be = BackEnd(STORAGE_PATH)
\r
309 info = be.get_object_meta(request.user, v_container, v_object)
\r
311 raise ItemNotFound()
\r
313 response = HttpResponse(status = 204)
\r
314 response['ETag'] = info['hash']
\r
315 response['Content-Length'] = info['bytes']
\r
316 response['Content-Type'] = info['content_type']
\r
317 response['Last-Modified'] = http_date(info['last_modified'])
\r
318 for k in [x for x in info.keys() if x.startswith('X-Object-Meta-')]:
\r
319 response[k.encode('utf-8')] = info[k].encode('utf-8')
\r
324 def object_read(request, v_account, v_container, v_object):
\r
325 # Normal Response Codes: 200, 206
\r
326 # Error Response Codes: serviceUnavailable (503),
\r
327 # rangeNotSatisfiable (416),
\r
328 # preconditionFailed (412),
\r
329 # itemNotFound (404),
\r
330 # unauthorized (401),
\r
331 # badRequest (400),
\r
332 # notModified (304)
\r
334 be = BackEnd(STORAGE_PATH)
\r
336 info = be.get_object_meta(request.user, v_container, v_object)
\r
338 raise ItemNotFound()
\r
340 # TODO: Check if the cloudfiles python bindings expect hash/content_type/last_modified on range requests.
\r
341 response = HttpResponse()
\r
342 response['ETag'] = info['hash']
\r
343 response['Content-Type'] = info['content_type']
\r
344 response['Last-Modified'] = http_date(info['last_modified'])
\r
347 range = get_range(request)
\r
348 if range is not None:
\r
349 offset, length = range
\r
351 if offset + length > info['bytes']:
\r
352 raise RangeNotSatisfiable()
\r
354 if offset > info['bytes']:
\r
355 raise RangeNotSatisfiable()
\r
359 response['Content-Length'] = length
\r
360 response.status_code = 206
\r
365 response['Content-Length'] = info['bytes']
\r
366 response.status_code = 200
\r
368 # Conditions (according to RFC2616 must be evaluated at the end).
\r
369 # TODO: Check etag/date conditions.
\r
370 if_match = request.META.get('HTTP_IF_MATCH')
\r
371 if if_match is not None and if_match != '*':
\r
372 if info['hash'] not in parse_etags(if_match):
\r
373 raise PreconditionFailed()
\r
375 if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
\r
376 if if_none_match is not None:
\r
377 if if_none_match == '*' or info['hash'] in parse_etags(if_none_match):
\r
378 raise NotModified()
\r
380 if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
\r
381 if if_modified_since is not None:
\r
382 if_modified_since = parse_http_date_safe(if_modified_since)
\r
383 if if_modified_since is not None and info['last_modified'] <= if_modified_since:
\r
384 raise NotModified()
\r
386 if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
\r
387 if if_unmodified_since is not None:
\r
388 if_unmodified_since = parse_http_date_safe(if_unmodified_since)
\r
389 if if_unmodified_since is not None and info['last_modified'] > if_unmodified_since:
\r
390 raise PreconditionFailed()
\r
393 response.content = be.get_object(request.user, v_container, v_object, offset, length)
\r
395 raise ItemNotFound()
\r
400 def object_write(request, v_account, v_container, v_object):
\r
401 # Normal Response Codes: 201
\r
402 # Error Response Codes: serviceUnavailable (503),
\r
403 # unprocessableEntity (422),
\r
404 # lengthRequired (411),
\r
405 # itemNotFound (404),
\r
406 # unauthorized (401),
\r
409 be = BackEnd(STORAGE_PATH)
\r
411 copy_from = request.META.get('HTTP_X_COPY_FROM')
\r
413 parts = copy_from.split('/')
\r
414 if len(parts) < 3 or parts[0] != '':
\r
415 raise BadRequest('Bad X-Copy-From path.')
\r
416 copy_container = parts[1]
\r
417 copy_name = '/'.join(parts[2:])
\r
420 info = be.get_object_meta(request.user, copy_container, copy_name)
\r
422 raise ItemNotFound()
\r
424 content_length = request.META.get('CONTENT_LENGTH')
\r
425 content_type = request.META.get('CONTENT_TYPE')
\r
426 # TODO: Why is this required? Copy this ammount?
\r
427 if not content_length:
\r
428 raise LengthRequired()
\r
430 info['content_type'] = content_type
\r
432 meta = get_meta(request, 'X-Object-Meta-')
\r
436 be.copy_object(request.user, copy_container, copy_name, v_container, v_object)
\r
437 be.update_object_meta(request.user, v_container, v_object, info)
\r
439 raise ItemNotFound()
\r
441 response = HttpResponse(status = 201)
\r
443 content_length = request.META.get('CONTENT_LENGTH')
\r
444 content_type = request.META.get('CONTENT_TYPE')
\r
445 if not content_length or not content_type:
\r
446 raise LengthRequired()
\r
448 info = {'content_type': content_type}
\r
449 meta = get_meta(request, 'X-Object-Meta-')
\r
452 data = request.raw_post_data
\r
454 be.update_object(request.user, v_container, v_object, data)
\r
455 be.update_object_meta(request.user, v_container, v_object, info)
\r
457 raise ItemNotFound()
\r
459 # TODO: Check before update?
\r
460 info = be.get_object_meta(request.user, v_container, v_object)
\r
461 etag = request.META.get('HTTP_ETAG')
\r
463 etag = parse_etags(etag)[0] # TODO: Unescape properly.
\r
464 if etag != info['hash']:
\r
465 be.delete_object(request.user, v_container, v_object)
\r
466 raise UnprocessableEntity()
\r
468 response = HttpResponse(status = 201)
\r
469 response['ETag'] = info['hash']
\r
473 @api_method('COPY')
\r
474 def object_copy(request, v_account, v_container, v_object):
\r
475 # Normal Response Codes: 201
\r
476 # Error Response Codes: serviceUnavailable (503),
\r
477 # itemNotFound (404),
\r
478 # unauthorized (401),
\r
481 destination = request.META.get('HTTP_DESTINATION')
\r
482 if not destination:
\r
483 raise BadRequest('Missing Destination.');
\r
485 parts = destination.split('/')
\r
486 if len(parts) < 3 or parts[0] != '':
\r
487 raise BadRequest('Bad Destination path.')
\r
488 dest_container = parts[1]
\r
489 dest_name = '/'.join(parts[2:])
\r
491 be = BackEnd(STORAGE_PATH)
\r
493 info = be.get_object_meta(request.user, v_container, v_object)
\r
495 raise ItemNotFound()
\r
497 content_type = request.META.get('CONTENT_TYPE')
\r
499 info['content_type'] = content_type
\r
500 meta = get_meta(request, 'X-Object-Meta-')
\r
504 be.copy_object(request.user, v_container, v_object, dest_container, dest_name)
\r
505 be.update_object_meta(request.user, dest_container, dest_name, info)
\r
507 raise ItemNotFound()
\r
509 response = HttpResponse(status = 201)
\r
511 @api_method('POST')
\r
512 def object_update(request, v_account, v_container, v_object):
\r
513 # Normal Response Codes: 202
\r
514 # Error Response Codes: serviceUnavailable (503),
\r
515 # itemNotFound (404),
\r
516 # unauthorized (401),
\r
519 meta = get_meta(request, 'X-Object-Meta-')
\r
521 be = BackEnd(STORAGE_PATH)
\r
523 be.update_object_meta(request.user, v_container, v_object, meta)
\r
525 raise ItemNotFound()
\r
527 return HttpResponse(status = 202)
\r
529 @api_method('DELETE')
\r
530 def object_delete(request, v_account, v_container, v_object):
\r
531 # Normal Response Codes: 204
\r
532 # Error Response Codes: serviceUnavailable (503),
\r
533 # itemNotFound (404),
\r
534 # unauthorized (401),
\r
537 be = BackEnd(STORAGE_PATH)
\r
539 be.delete_object(request.user, v_container, v_object)
\r
541 raise ItemNotFound()
\r
542 return HttpResponse(status = 204)
\r
545 def method_not_allowed(request):
\r
546 raise BadRequest('Method not allowed.')
\r