1 # Copyright 2011 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
11 # 2. Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following
13 # disclaimer in the documentation and/or other materials
14 # provided with the distribution.
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
39 from django.http import HttpResponse
40 from django.template.loader import render_to_string
41 from django.utils import simplejson as json
42 from django.utils.http import parse_etags
44 from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, ItemNotFound, Conflict,
45 LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity)
46 from pithos.api.util import (format_meta_key, printable_meta_dict, get_account_meta,
47 put_account_meta, get_container_meta, put_container_meta, get_object_meta, put_object_meta,
48 validate_modification_preconditions, validate_matching_preconditions, copy_or_move_object,
49 get_version, get_content_length, get_range, get_content_range, raw_input_socket,
50 socket_read_iterator, ObjectWrapper, hashmap_hash, api_method)
51 from pithos.backends import backend
54 logger = logging.getLogger(__name__)
57 def top_demux(request):
58 if request.method == 'GET':
59 return authenticate(request)
61 return method_not_allowed(request)
63 def account_demux(request, v_account):
64 if request.method == 'HEAD':
65 return account_meta(request, v_account)
66 elif request.method == 'GET':
67 return container_list(request, v_account)
68 elif request.method == 'POST':
69 return account_update(request, v_account)
71 return method_not_allowed(request)
73 def container_demux(request, v_account, v_container):
74 if request.method == 'HEAD':
75 return container_meta(request, v_account, v_container)
76 elif request.method == 'GET':
77 return object_list(request, v_account, v_container)
78 elif request.method == 'PUT':
79 return container_create(request, v_account, v_container)
80 elif request.method == 'POST':
81 return container_update(request, v_account, v_container)
82 elif request.method == 'DELETE':
83 return container_delete(request, v_account, v_container)
85 return method_not_allowed(request)
87 def object_demux(request, v_account, v_container, v_object):
88 if request.method == 'HEAD':
89 return object_meta(request, v_account, v_container, v_object)
90 elif request.method == 'GET':
91 return object_read(request, v_account, v_container, v_object)
92 elif request.method == 'PUT':
93 return object_write(request, v_account, v_container, v_object)
94 elif request.method == 'COPY':
95 return object_copy(request, v_account, v_container, v_object)
96 elif request.method == 'MOVE':
97 return object_move(request, v_account, v_container, v_object)
98 elif request.method == 'POST':
99 return object_update(request, v_account, v_container, v_object)
100 elif request.method == 'DELETE':
101 return object_delete(request, v_account, v_container, v_object)
103 return method_not_allowed(request)
106 def authenticate(request):
107 # Normal Response Codes: 204
108 # Error Response Codes: serviceUnavailable (503),
109 # unauthorized (401),
112 x_auth_user = request.META.get('HTTP_X_AUTH_USER')
113 x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
114 if not x_auth_user or not x_auth_key:
115 raise BadRequest('Missing X-Auth-User or X-Auth-Key header')
116 response = HttpResponse(status=204)
117 response['X-Auth-Token'] = '0000'
118 response['X-Storage-Url'] = os.path.join(request.build_absolute_uri(), 'demo')
122 def account_meta(request, v_account):
123 # Normal Response Codes: 204
124 # Error Response Codes: serviceUnavailable (503),
125 # unauthorized (401),
128 meta = backend.get_account_meta(request.user)
130 response = HttpResponse(status=204)
131 put_account_meta(response, meta)
135 def account_update(request, v_account):
136 # Normal Response Codes: 202
137 # Error Response Codes: serviceUnavailable (503),
138 # unauthorized (401),
141 meta = get_account_meta(request)
142 backend.update_account_meta(request.user, meta, replace=True)
143 return HttpResponse(status=202)
145 @api_method('GET', format_allowed=True)
146 def container_list(request, v_account):
147 # Normal Response Codes: 200, 204
148 # Error Response Codes: serviceUnavailable (503),
149 # itemNotFound (404),
150 # unauthorized (401),
153 meta = backend.get_account_meta(request.user)
155 validate_modification_preconditions(request, meta)
157 response = HttpResponse()
158 put_account_meta(response, meta)
160 marker = request.GET.get('marker')
161 limit = request.GET.get('limit')
171 containers = [x[0] for x in backend.list_containers(request.user, marker, limit)]
175 if request.serialization == 'text':
176 if len(containers) == 0:
177 # The cloudfiles python bindings expect 200 if json/xml.
178 response.status_code = 204
180 response.status_code = 200
181 response.content = '\n'.join(containers) + '\n'
187 meta = backend.get_container_meta(request.user, x)
190 container_meta.append(printable_meta_dict(meta))
191 if request.serialization == 'xml':
192 data = render_to_string('containers.xml', {'account': request.user, 'containers': container_meta})
193 elif request.serialization == 'json':
194 data = json.dumps(container_meta)
195 response.status_code = 200
196 response.content = data
200 def container_meta(request, v_account, v_container):
201 # Normal Response Codes: 204
202 # Error Response Codes: serviceUnavailable (503),
203 # itemNotFound (404),
204 # unauthorized (401),
208 meta = backend.get_container_meta(request.user, v_container)
209 meta['object_meta'] = backend.list_object_meta(request.user, v_container)
211 raise ItemNotFound('Container does not exist')
213 response = HttpResponse(status=204)
214 put_container_meta(response, meta)
218 def container_create(request, v_account, v_container):
219 # Normal Response Codes: 201, 202
220 # Error Response Codes: serviceUnavailable (503),
221 # itemNotFound (404),
222 # unauthorized (401),
225 meta = get_container_meta(request)
228 backend.put_container(request.user, v_container)
234 backend.update_container_meta(request.user, v_container, meta, replace=True)
236 return HttpResponse(status=ret)
239 def container_update(request, v_account, v_container):
240 # Normal Response Codes: 202
241 # Error Response Codes: serviceUnavailable (503),
242 # itemNotFound (404),
243 # unauthorized (401),
246 meta = get_container_meta(request)
248 backend.update_container_meta(request.user, v_container, meta, replace=True)
250 raise ItemNotFound('Container does not exist')
251 return HttpResponse(status=202)
253 @api_method('DELETE')
254 def container_delete(request, v_account, v_container):
255 # Normal Response Codes: 204
256 # Error Response Codes: serviceUnavailable (503),
258 # itemNotFound (404),
259 # unauthorized (401),
263 backend.delete_container(request.user, v_container)
265 raise ItemNotFound('Container does not exist')
267 raise Conflict('Container is not empty')
268 return HttpResponse(status=204)
270 @api_method('GET', format_allowed=True)
271 def object_list(request, v_account, v_container):
272 # Normal Response Codes: 200, 204
273 # Error Response Codes: serviceUnavailable (503),
274 # itemNotFound (404),
275 # unauthorized (401),
279 meta = backend.get_container_meta(request.user, v_container)
280 meta['object_meta'] = backend.list_object_meta(request.user, v_container)
282 raise ItemNotFound('Container does not exist')
284 validate_modification_preconditions(request, meta)
286 response = HttpResponse()
287 put_container_meta(response, meta)
289 path = request.GET.get('path')
290 prefix = request.GET.get('prefix')
291 delimiter = request.GET.get('delimiter')
293 # Path overrides prefix and delimiter.
301 if prefix and delimiter:
302 prefix = prefix + delimiter
305 prefix = prefix.lstrip('/')
307 marker = request.GET.get('marker')
308 limit = request.GET.get('limit')
317 keys = request.GET.get('meta')
319 keys = keys.split(',')
320 keys = [format_meta_key('X-Object-Meta-' + x.strip()) for x in keys if x.strip() != '']
325 objects = [x[0] for x in backend.list_objects(request.user, v_container, prefix, delimiter, marker, limit, virtual, keys)]
327 raise ItemNotFound('Container does not exist')
329 if request.serialization == 'text':
330 if len(objects) == 0:
331 # The cloudfiles python bindings expect 200 if json/xml.
332 response.status_code = 204
334 response.status_code = 200
335 response.content = '\n'.join(objects) + '\n'
341 meta = backend.get_object_meta(request.user, v_container, x)
343 # Virtual objects/directories.
344 if virtual and delimiter and x.endswith(delimiter):
345 object_meta.append({'subdir': x})
347 object_meta.append(printable_meta_dict(meta))
348 if request.serialization == 'xml':
349 data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
350 elif request.serialization == 'json':
351 data = json.dumps(object_meta)
352 response.status_code = 200
353 response.content = data
357 def object_meta(request, v_account, v_container, v_object):
358 # Normal Response Codes: 204
359 # Error Response Codes: serviceUnavailable (503),
360 # itemNotFound (404),
361 # unauthorized (401),
364 version = get_version(request)
366 meta = backend.get_object_meta(request.user, v_container, v_object, version)
368 raise ItemNotFound('Object does not exist')
370 raise ItemNotFound('Version does not exist')
372 response = HttpResponse(status=204)
373 put_object_meta(response, meta)
376 @api_method('GET', format_allowed=True)
377 def object_read(request, v_account, v_container, v_object):
378 # Normal Response Codes: 200, 206
379 # Error Response Codes: serviceUnavailable (503),
380 # rangeNotSatisfiable (416),
381 # preconditionFailed (412),
382 # itemNotFound (404),
383 # unauthorized (401),
387 version = get_version(request)
389 meta = backend.get_object_meta(request.user, v_container, v_object, version)
391 raise ItemNotFound('Object does not exist')
393 raise ItemNotFound('Version does not exist')
395 # Evaluate conditions.
396 validate_modification_preconditions(request, meta)
398 validate_matching_preconditions(request, meta)
400 response = HttpResponse(status=304)
401 response['ETag'] = meta['hash']
405 size, hashmap = backend.get_object_hashmap(request.user, v_container, v_object, version)
407 raise ItemNotFound('Object does not exist')
409 raise ItemNotFound('Version does not exist')
411 # Reply with the hashmap.
412 if request.serialization != 'text':
413 d = {'block_size': backend.block_size, 'block_hash': backend.hash_algorithm, 'bytes': size, 'hashes': hashmap}
414 if request.serialization == 'xml':
415 d['object'] = v_object
416 data = render_to_string('hashes.xml', d)
417 elif request.serialization == 'json':
420 response = HttpResponse(data, status=200)
421 put_object_meta(response, meta)
422 response['Content-Length'] = len(data)
426 ranges = get_range(request, size)
431 check = [True for offset, length in ranges if
432 length <= 0 or length > size or
433 offset < 0 or offset >= size or
434 offset + length > size]
436 raise RangeNotSatisfiable('Requested range exceeds object limits')
439 if ret == 206 and len(ranges) > 1:
440 boundary = uuid.uuid4().hex
443 wrapper = ObjectWrapper(request.user, v_container, v_object, ranges, size, hashmap, boundary)
444 response = HttpResponse(wrapper, status=ret)
445 put_object_meta(response, meta)
448 offset, length = ranges[0]
449 response['Content-Length'] = length # Update with the correct length.
450 response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
452 del(response['Content-Length'])
453 response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
457 def object_write(request, v_account, v_container, v_object):
458 # Normal Response Codes: 201
459 # Error Response Codes: serviceUnavailable (503),
460 # unprocessableEntity (422),
461 # lengthRequired (411),
462 # itemNotFound (404),
463 # unauthorized (401),
466 copy_from = request.META.get('HTTP_X_COPY_FROM')
467 move_from = request.META.get('HTTP_X_MOVE_FROM')
468 if copy_from or move_from:
469 # TODO: Why is this required? Copy this ammount?
470 content_length = get_content_length(request)
473 copy_or_move_object(request, move_from, (v_container, v_object), move=True)
475 copy_or_move_object(request, copy_from, (v_container, v_object), move=False)
476 return HttpResponse(status=201)
478 meta = get_object_meta(request)
480 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
481 content_length = get_content_length(request)
482 # Should be BadRequest, but API says otherwise.
483 if 'Content-Type' not in meta:
484 raise LengthRequired('Missing Content-Type header')
489 sock = raw_input_socket(request)
490 for data in socket_read_iterator(sock, content_length, backend.block_size):
491 # TODO: Raise 408 (Request Timeout) if this takes too long.
492 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
494 hashmap.append(backend.put_block(data))
497 meta['hash'] = md5.hexdigest().lower()
498 etag = request.META.get('HTTP_ETAG')
499 if etag and parse_etags(etag)[0].lower() != meta['hash']:
500 raise UnprocessableEntity('Object ETag does not match')
503 # TODO: Update metadata with the hashmap.
504 backend.update_object_hashmap(request.user, v_container, v_object, size, hashmap)
506 raise ItemNotFound('Container does not exist')
508 backend.update_object_meta(request.user, v_container, v_object, meta, replace=True)
510 raise ItemNotFound('Object does not exist')
512 response = HttpResponse(status=201)
513 response['ETag'] = meta['hash']
517 def object_copy(request, v_account, v_container, v_object):
518 # Normal Response Codes: 201
519 # Error Response Codes: serviceUnavailable (503),
520 # itemNotFound (404),
521 # unauthorized (401),
524 dest_path = request.META.get('HTTP_DESTINATION')
526 raise BadRequest('Missing Destination header')
527 copy_or_move_object(request, (v_container, v_object), dest_path, move=False)
528 return HttpResponse(status=201)
531 def object_move(request, v_account, v_container, v_object):
532 # Normal Response Codes: 201
533 # Error Response Codes: serviceUnavailable (503),
534 # itemNotFound (404),
535 # unauthorized (401),
538 dest_path = request.META.get('HTTP_DESTINATION')
540 raise BadRequest('Missing Destination header')
541 copy_or_move_object(request, (v_container, v_object), dest_path, move=True)
542 return HttpResponse(status=201)
545 def object_update(request, v_account, v_container, v_object):
546 # Normal Response Codes: 202, 204
547 # Error Response Codes: serviceUnavailable (503),
548 # itemNotFound (404),
549 # unauthorized (401),
552 meta = get_object_meta(request)
553 content_type = meta.get('Content-Type')
555 del(meta['Content-Type']) # Do not allow changing the Content-Type.
558 prev_meta = backend.get_object_meta(request.user, v_container, v_object)
560 raise ItemNotFound('Object does not exist')
562 # Handle metadata changes.
564 # Keep previous values of 'Content-Type' and 'hash'.
565 for k in ('Content-Type', 'hash'):
567 meta[k] = prev_meta[k]
569 backend.update_object_meta(request.user, v_container, v_object, meta, replace=True)
571 raise ItemNotFound('Object does not exist')
573 # A Content-Type or Content-Range header may indicate data updates.
574 if content_type and content_type.startswith('multipart/byteranges'):
575 # TODO: Support multiple update ranges.
576 return HttpResponse(status=202)
577 # Single range update. Range must be in Content-Range.
578 # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
579 # (with the addition that '*' is allowed for the range - will append).
580 if content_type and content_type != 'application/octet-stream':
581 return HttpResponse(status=202)
582 content_range = request.META.get('HTTP_CONTENT_RANGE')
583 if not content_range:
584 return HttpResponse(status=202)
585 ranges = get_content_range(request)
587 return HttpResponse(status=202)
588 # Require either a Content-Length, or 'chunked' Transfer-Encoding.
590 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
591 content_length = get_content_length(request)
594 size, hashmap = backend.get_object_hashmap(request.user, v_container, v_object)
596 raise ItemNotFound('Object does not exist')
598 offset, length, total = ranges
601 if length is None or content_length == -1:
602 length = content_length # Nevermind the error.
603 elif length != content_length:
604 raise BadRequest('Content length does not match range length')
605 if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
606 raise RangeNotSatisfiable('Supplied range will change provided object limits')
608 sock = raw_input_socket(request)
610 for d in socket_read_iterator(sock, length, backend.block_size):
611 # TODO: Raise 408 (Request Timeout) if this takes too long.
612 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
614 bi = int(offset / backend.block_size)
615 bo = offset % backend.block_size
616 bl = min(len(data), backend.block_size - bo)
618 h = backend.update_block(hashmap[bi], data[:bl], bo)
619 if bi < len(hashmap):
625 bi = int(offset / backend.block_size)
627 h = backend.update_block(hashmap[bi], data)
628 if bi < len(hashmap):
636 # TODO: Update metadata with the hashmap.
637 backend.update_object_hashmap(request.user, v_container, v_object, size, hashmap)
639 raise ItemNotFound('Container does not exist')
643 meta['hash'] = hashmap_hash(hashmap)
645 backend.update_object_meta(request.user, v_container, v_object, meta)
647 raise ItemNotFound('Object does not exist')
649 response = HttpResponse(status=204)
650 response['ETag'] = meta['hash']
653 @api_method('DELETE')
654 def object_delete(request, v_account, v_container, v_object):
655 # Normal Response Codes: 204
656 # Error Response Codes: serviceUnavailable (503),
657 # itemNotFound (404),
658 # unauthorized (401),
662 backend.delete_object(request.user, v_container, v_object)
664 raise ItemNotFound('Object does not exist')
665 return HttpResponse(status=204)
668 def method_not_allowed(request):
669 raise BadRequest('Method not allowed')