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.
38 from django.conf import settings
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
43 from xml.dom import minidom
45 from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, ItemNotFound, Conflict,
46 LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity)
47 from pithos.api.util import (format_header_key, printable_header_dict, get_account_headers,
48 put_account_headers, get_container_headers, put_container_headers, get_object_headers, put_object_headers,
49 update_manifest_meta, update_sharing_meta, update_public_meta, validate_modification_preconditions,
50 validate_matching_preconditions, split_container_object_string, copy_or_move_object,
51 get_int_parameter, get_content_length, get_content_range, raw_input_socket,
52 socket_read_iterator, object_data_response, put_object_block, hashmap_hash, api_method)
53 from pithos.backends import backend
54 from pithos.backends.base import NotAllowedError
57 logger = logging.getLogger(__name__)
60 def top_demux(request):
61 if request.method == 'GET':
62 return authenticate(request)
64 return method_not_allowed(request)
66 def account_demux(request, v_account):
67 if request.method == 'HEAD':
68 return account_meta(request, v_account)
69 elif request.method == 'POST':
70 return account_update(request, v_account)
71 elif request.method == 'GET':
72 return container_list(request, v_account)
74 return method_not_allowed(request)
76 def container_demux(request, v_account, v_container):
77 if request.method == 'HEAD':
78 return container_meta(request, v_account, v_container)
79 elif request.method == 'PUT':
80 return container_create(request, v_account, v_container)
81 elif request.method == 'POST':
82 return container_update(request, v_account, v_container)
83 elif request.method == 'DELETE':
84 return container_delete(request, v_account, v_container)
85 elif request.method == 'GET':
86 return object_list(request, v_account, v_container)
88 return method_not_allowed(request)
90 def object_demux(request, v_account, v_container, v_object):
91 if request.method == 'HEAD':
92 return object_meta(request, v_account, v_container, v_object)
93 elif request.method == 'GET':
94 return object_read(request, v_account, v_container, v_object)
95 elif request.method == 'PUT':
96 return object_write(request, v_account, v_container, v_object)
97 elif request.method == 'COPY':
98 return object_copy(request, v_account, v_container, v_object)
99 elif request.method == 'MOVE':
100 return object_move(request, v_account, v_container, v_object)
101 elif request.method == 'POST':
102 if request.META.get('CONTENT_TYPE', '').startswith('multipart/form-data'):
103 return object_write_form(request, v_account, v_container, v_object)
104 return object_update(request, v_account, v_container, v_object)
105 elif request.method == 'DELETE':
106 return object_delete(request, v_account, v_container, v_object)
108 return method_not_allowed(request)
111 def authenticate(request):
112 # Normal Response Codes: 204
113 # Error Response Codes: serviceUnavailable (503),
114 # unauthorized (401),
117 x_auth_user = request.META.get('HTTP_X_AUTH_USER')
118 x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
119 if not x_auth_user or not x_auth_key:
120 raise BadRequest('Missing X-Auth-User or X-Auth-Key header')
121 response = HttpResponse(status=204)
122 inv_auth_tokens = dict((v, k) for k, v in settings.AUTH_TOKENS.items())
123 response['X-Auth-Token'] = inv_auth_tokens.get(x_auth_user, '0000')
124 response['X-Storage-Url'] = os.path.join(request.build_absolute_uri(),
129 def account_meta(request, v_account):
130 # Normal Response Codes: 204
131 # Error Response Codes: serviceUnavailable (503),
132 # unauthorized (401),
135 until = get_int_parameter(request.GET.get('until'))
137 meta = backend.get_account_meta(request.user, v_account, until)
138 groups = backend.get_account_groups(request.user, v_account)
139 except NotAllowedError:
140 raise Unauthorized('Access denied')
142 response = HttpResponse(status=204)
143 put_account_headers(response, meta, groups)
147 def account_update(request, v_account):
148 # Normal Response Codes: 202
149 # Error Response Codes: serviceUnavailable (503),
150 # unauthorized (401),
153 meta, groups = get_account_headers(request)
155 if 'update' in request.GET:
159 backend.update_account_groups(request.user, v_account, groups, replace)
160 except NotAllowedError:
161 raise Unauthorized('Access denied')
163 raise BadRequest('Invalid groups header')
165 backend.update_account_meta(request.user, v_account, meta, replace)
166 except NotAllowedError:
167 raise Unauthorized('Access denied')
168 return HttpResponse(status=202)
170 @api_method('GET', format_allowed=True)
171 def container_list(request, v_account):
172 # Normal Response Codes: 200, 204
173 # Error Response Codes: serviceUnavailable (503),
174 # itemNotFound (404),
175 # unauthorized (401),
178 until = get_int_parameter(request.GET.get('until'))
180 meta = backend.get_account_meta(request.user, v_account, until)
181 groups = backend.get_account_groups(request.user, v_account)
182 except NotAllowedError:
183 raise Unauthorized('Access denied')
185 validate_modification_preconditions(request, meta)
187 response = HttpResponse()
188 put_account_headers(response, meta, groups)
190 marker = request.GET.get('marker')
191 limit = request.GET.get('limit')
201 containers = backend.list_containers(request.user, v_account, marker, limit, until)
202 except NotAllowedError:
203 raise Unauthorized('Access denied')
207 if request.serialization == 'text':
208 if len(containers) == 0:
209 # The cloudfiles python bindings expect 200 if json/xml.
210 response.status_code = 204
212 response.status_code = 200
213 response.content = '\n'.join([x[0] for x in containers]) + '\n'
220 meta = backend.get_container_meta(request.user, v_account, x[0], until)
221 policy = backend.get_container_policy(request.user, v_account, x[0])
222 except NotAllowedError:
223 raise Unauthorized('Access denied')
227 for k, v in policy.iteritems():
228 meta['X-Container-Policy-' + k] = v
229 container_meta.append(printable_header_dict(meta))
230 if request.serialization == 'xml':
231 data = render_to_string('containers.xml', {'account': v_account, 'containers': container_meta})
232 elif request.serialization == 'json':
233 data = json.dumps(container_meta)
234 response.status_code = 200
235 response.content = data
239 def container_meta(request, v_account, v_container):
240 # Normal Response Codes: 204
241 # Error Response Codes: serviceUnavailable (503),
242 # itemNotFound (404),
243 # unauthorized (401),
246 until = get_int_parameter(request.GET.get('until'))
248 meta = backend.get_container_meta(request.user, v_account, v_container, until)
249 meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
250 policy = backend.get_container_policy(request.user, v_account, v_container)
251 except NotAllowedError:
252 raise Unauthorized('Access denied')
254 raise ItemNotFound('Container does not exist')
256 response = HttpResponse(status=204)
257 put_container_headers(response, meta, policy)
261 def container_create(request, v_account, v_container):
262 # Normal Response Codes: 201, 202
263 # Error Response Codes: serviceUnavailable (503),
264 # itemNotFound (404),
265 # unauthorized (401),
268 meta, policy = get_container_headers(request)
271 backend.put_container(request.user, v_account, v_container, policy)
273 except NotAllowedError:
274 raise Unauthorized('Access denied')
280 backend.update_container_meta(request.user, v_account, v_container, meta, replace=True)
281 except NotAllowedError:
282 raise Unauthorized('Access denied')
284 raise ItemNotFound('Container does not exist')
286 return HttpResponse(status=ret)
289 def container_update(request, v_account, v_container):
290 # Normal Response Codes: 202
291 # Error Response Codes: serviceUnavailable (503),
292 # itemNotFound (404),
293 # unauthorized (401),
296 meta, policy = get_container_headers(request)
298 if 'update' in request.GET:
302 backend.update_container_policy(request.user, v_account, v_container, policy, replace)
303 except NotAllowedError:
304 raise Unauthorized('Access denied')
306 raise ItemNotFound('Container does not exist')
308 raise BadRequest('Invalid policy header')
310 backend.update_container_meta(request.user, v_account, v_container, meta, replace)
311 except NotAllowedError:
312 raise Unauthorized('Access denied')
314 raise ItemNotFound('Container does not exist')
315 return HttpResponse(status=202)
317 @api_method('DELETE')
318 def container_delete(request, v_account, v_container):
319 # Normal Response Codes: 204
320 # Error Response Codes: serviceUnavailable (503),
322 # itemNotFound (404),
323 # unauthorized (401),
326 until = get_int_parameter(request.GET.get('until'))
328 backend.delete_container(request.user, v_account, v_container, until)
329 except NotAllowedError:
330 raise Unauthorized('Access denied')
332 raise ItemNotFound('Container does not exist')
334 raise Conflict('Container is not empty')
335 return HttpResponse(status=204)
337 @api_method('GET', format_allowed=True)
338 def object_list(request, v_account, v_container):
339 # Normal Response Codes: 200, 204
340 # Error Response Codes: serviceUnavailable (503),
341 # itemNotFound (404),
342 # unauthorized (401),
345 until = get_int_parameter(request.GET.get('until'))
347 meta = backend.get_container_meta(request.user, v_account, v_container, until)
348 meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
349 policy = backend.get_container_policy(request.user, v_account, v_container)
350 except NotAllowedError:
351 raise Unauthorized('Access denied')
353 raise ItemNotFound('Container does not exist')
355 validate_modification_preconditions(request, meta)
357 response = HttpResponse()
358 put_container_headers(response, meta, policy)
360 path = request.GET.get('path')
361 prefix = request.GET.get('prefix')
362 delimiter = request.GET.get('delimiter')
364 # Path overrides prefix and delimiter.
372 if prefix and delimiter:
373 prefix = prefix + delimiter
376 prefix = prefix.lstrip('/')
378 marker = request.GET.get('marker')
379 limit = request.GET.get('limit')
388 keys = request.GET.get('meta')
390 keys = keys.split(',')
391 keys = [format_header_key('X-Object-Meta-' + x.strip()) for x in keys if x.strip() != '']
396 objects = backend.list_objects(request.user, v_account, v_container, prefix, delimiter, marker, limit, virtual, keys, until)
397 except NotAllowedError:
398 raise Unauthorized('Access denied')
400 raise ItemNotFound('Container does not exist')
402 if request.serialization == 'text':
403 if len(objects) == 0:
404 # The cloudfiles python bindings expect 200 if json/xml.
405 response.status_code = 204
407 response.status_code = 200
408 response.content = '\n'.join([x[0] for x in objects]) + '\n'
414 # Virtual objects/directories.
415 object_meta.append({'subdir': x[0]})
418 meta = backend.get_object_meta(request.user, v_account, v_container, x[0], x[1])
420 permissions = backend.get_object_permissions(request.user, v_account, v_container, x[0])
421 public = backend.get_object_public(request.user, v_account, v_container, x[0])
425 except NotAllowedError:
426 raise Unauthorized('Access denied')
430 update_sharing_meta(permissions, v_account, v_container, x[0], meta)
431 update_public_meta(public, meta)
432 object_meta.append(printable_header_dict(meta))
433 if request.serialization == 'xml':
434 data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
435 elif request.serialization == 'json':
436 data = json.dumps(object_meta)
437 response.status_code = 200
438 response.content = data
442 def object_meta(request, v_account, v_container, v_object):
443 # Normal Response Codes: 204
444 # Error Response Codes: serviceUnavailable (503),
445 # itemNotFound (404),
446 # unauthorized (401),
449 version = request.GET.get('version')
451 meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
453 permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
454 public = backend.get_object_public(request.user, v_account, v_container, v_object)
458 except NotAllowedError:
459 raise Unauthorized('Access denied')
461 raise ItemNotFound('Object does not exist')
463 raise ItemNotFound('Version does not exist')
465 update_manifest_meta(request, v_account, meta)
466 update_sharing_meta(permissions, v_account, v_container, v_object, meta)
467 update_public_meta(public, meta)
469 response = HttpResponse(status=200)
470 put_object_headers(response, meta)
473 @api_method('GET', format_allowed=True)
474 def object_read(request, v_account, v_container, v_object):
475 # Normal Response Codes: 200, 206
476 # Error Response Codes: serviceUnavailable (503),
477 # rangeNotSatisfiable (416),
478 # preconditionFailed (412),
479 # itemNotFound (404),
480 # unauthorized (401),
484 version = request.GET.get('version')
486 # Reply with the version list. Do this first, as the object may be deleted.
487 if version == 'list':
488 if request.serialization == 'text':
489 raise BadRequest('No format specified for version list.')
492 v = backend.list_versions(request.user, v_account, v_container, v_object)
493 except NotAllowedError:
494 raise Unauthorized('Access denied')
496 if request.serialization == 'xml':
497 d['object'] = v_object
498 data = render_to_string('versions.xml', d)
499 elif request.serialization == 'json':
502 response = HttpResponse(data, status=200)
503 response['Content-Length'] = len(data)
507 meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
509 permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
510 public = backend.get_object_public(request.user, v_account, v_container, v_object)
514 except NotAllowedError:
515 raise Unauthorized('Access denied')
517 raise ItemNotFound('Object does not exist')
519 raise ItemNotFound('Version does not exist')
521 update_manifest_meta(request, v_account, meta)
522 update_sharing_meta(permissions, v_account, v_container, v_object, meta)
523 update_public_meta(public, meta)
525 # Evaluate conditions.
526 validate_modification_preconditions(request, meta)
528 validate_matching_preconditions(request, meta)
530 response = HttpResponse(status=304)
531 response['ETag'] = meta['hash']
536 if 'X-Object-Manifest' in meta:
538 src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
539 objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
540 except NotAllowedError:
541 raise Unauthorized('Access denied')
543 raise BadRequest('Invalid X-Object-Manifest header')
545 raise ItemNotFound('Container does not exist')
549 s, h = backend.get_object_hashmap(request.user, v_account, src_container, x[0], x[1])
552 except NotAllowedError:
553 raise Unauthorized('Access denied')
555 raise ItemNotFound('Object does not exist')
557 raise ItemNotFound('Version does not exist')
560 s, h = backend.get_object_hashmap(request.user, v_account, v_container, v_object, version)
563 except NotAllowedError:
564 raise Unauthorized('Access denied')
566 raise ItemNotFound('Object does not exist')
568 raise ItemNotFound('Version does not exist')
570 # Reply with the hashmap.
571 if request.serialization != 'text':
573 hashmap = sum(hashmaps, [])
574 d = {'block_size': backend.block_size, 'block_hash': backend.hash_algorithm, 'bytes': size, 'hashes': hashmap}
575 if request.serialization == 'xml':
576 d['object'] = v_object
577 data = render_to_string('hashes.xml', d)
578 elif request.serialization == 'json':
581 response = HttpResponse(data, status=200)
582 put_object_headers(response, meta)
583 response['Content-Length'] = len(data)
586 return object_data_response(request, sizes, hashmaps, meta)
588 @api_method('PUT', format_allowed=True)
589 def object_write(request, v_account, v_container, v_object):
590 # Normal Response Codes: 201
591 # Error Response Codes: serviceUnavailable (503),
592 # unprocessableEntity (422),
593 # lengthRequired (411),
595 # itemNotFound (404),
596 # unauthorized (401),
599 if not request.GET.get('format'):
600 request.serialization = 'text'
602 copy_from = request.META.get('HTTP_X_COPY_FROM')
603 move_from = request.META.get('HTTP_X_MOVE_FROM')
604 if copy_from or move_from:
605 content_length = get_content_length(request) # Required by the API.
609 src_container, src_name = split_container_object_string(move_from)
611 raise BadRequest('Invalid X-Move-From header')
612 copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=True)
615 src_container, src_name = split_container_object_string(copy_from)
617 raise BadRequest('Invalid X-Copy-From header')
618 copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=False)
619 return HttpResponse(status=201)
621 meta, permissions, public = get_object_headers(request)
623 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
624 content_length = get_content_length(request)
625 # Should be BadRequest, but API says otherwise.
626 if 'Content-Type' not in meta:
627 raise LengthRequired('Missing Content-Type header')
629 if request.serialization != 'text':
631 sock = raw_input_socket(request)
632 for block in socket_read_iterator(sock, content_length, backend.block_size):
633 data = '%s%s' % (data, block)
635 if request.serialization == 'json':
637 if not hasattr(d, '__getitem__'):
638 raise BadRequest('Invalid data formating')
640 hashmap = d['hashes']
643 raise BadRequest('Invalid data formatting')
644 elif request.serialization == 'xml':
646 xml = minidom.parseString(data)
647 obj = xml.getElementsByTagName('object')[0]
648 size = obj.attributes['bytes'].value
650 hashes = xml.getElementsByTagName('hash')
653 hashmap.append(hash.firstChild.data)
655 raise BadRequest('Invalid data formatting')
657 meta.update({'hash': hashmap_hash(hashmap)}) # Update ETag.
662 sock = raw_input_socket(request)
663 for data in socket_read_iterator(sock, content_length, backend.block_size):
664 # TODO: Raise 408 (Request Timeout) if this takes too long.
665 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
667 hashmap.append(backend.put_block(data))
670 meta['hash'] = md5.hexdigest().lower()
671 etag = request.META.get('HTTP_ETAG')
672 if etag and parse_etags(etag)[0].lower() != meta['hash']:
673 raise UnprocessableEntity('Object ETag does not match')
676 backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True, permissions)
677 except NotAllowedError:
678 raise Unauthorized('Access denied')
679 except IndexError, e:
680 raise Conflict(json.dumps(e.data))
682 raise ItemNotFound('Container does not exist')
684 raise BadRequest('Invalid sharing header')
685 except AttributeError, e:
686 raise Conflict(json.dumps(e.data))
687 if public is not None:
689 backend.update_object_public(request.user, v_account, v_container, v_object, public)
690 except NotAllowedError:
691 raise Unauthorized('Access denied')
693 raise ItemNotFound('Object does not exist')
695 response = HttpResponse(status=201)
696 response['ETag'] = meta['hash']
700 def object_write_form(request, v_account, v_container, v_object):
701 # Normal Response Codes: 201
702 # Error Response Codes: serviceUnavailable (503),
703 # itemNotFound (404),
704 # unauthorized (401),
707 if not request.FILES.has_key('X-Object-Data'):
708 raise BadRequest('Missing X-Object-Data field')
709 file = request.FILES['X-Object-Data']
712 meta['Content-Type'] = file.content_type
717 for data in file.chunks(backend.block_size):
719 hashmap.append(backend.put_block(data))
722 meta['hash'] = md5.hexdigest().lower()
725 backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True)
726 except NotAllowedError:
727 raise Unauthorized('Access denied')
729 raise ItemNotFound('Container does not exist')
731 response = HttpResponse(status=201)
732 response['ETag'] = meta['hash']
736 def object_copy(request, v_account, v_container, v_object):
737 # Normal Response Codes: 201
738 # Error Response Codes: serviceUnavailable (503),
739 # itemNotFound (404),
740 # unauthorized (401),
743 dest_path = request.META.get('HTTP_DESTINATION')
745 raise BadRequest('Missing Destination header')
747 dest_container, dest_name = split_container_object_string(dest_path)
749 raise BadRequest('Invalid Destination header')
750 copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=False)
751 return HttpResponse(status=201)
754 def object_move(request, v_account, v_container, v_object):
755 # Normal Response Codes: 201
756 # Error Response Codes: serviceUnavailable (503),
757 # itemNotFound (404),
758 # unauthorized (401),
761 dest_path = request.META.get('HTTP_DESTINATION')
763 raise BadRequest('Missing Destination header')
765 dest_container, dest_name = split_container_object_string(dest_path)
767 raise BadRequest('Invalid Destination header')
768 copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=True)
769 return HttpResponse(status=201)
772 def object_update(request, v_account, v_container, v_object):
773 # Normal Response Codes: 202, 204
774 # Error Response Codes: serviceUnavailable (503),
776 # itemNotFound (404),
777 # unauthorized (401),
779 meta, permissions, public = get_object_headers(request)
780 content_type = meta.get('Content-Type')
782 del(meta['Content-Type']) # Do not allow changing the Content-Type.
785 prev_meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
786 except NotAllowedError:
787 raise Unauthorized('Access denied')
789 raise ItemNotFound('Object does not exist')
790 # If replacing, keep previous values of 'Content-Type' and 'hash'.
792 if 'update' in request.GET:
795 for k in ('Content-Type', 'hash'):
797 meta[k] = prev_meta[k]
799 # A Content-Type or X-Source-Object header indicates data updates.
800 src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
801 if (not content_type or content_type != 'application/octet-stream') and not src_object:
802 # Do permissions first, as it may fail easier.
803 if permissions is not None:
805 backend.update_object_permissions(request.user, v_account, v_container, v_object, permissions)
806 except NotAllowedError:
807 raise Unauthorized('Access denied')
809 raise ItemNotFound('Object does not exist')
811 raise BadRequest('Invalid sharing header')
812 except AttributeError, e:
813 raise Conflict(json.dumps(e.data))
814 if public is not None:
816 backend.update_object_public(request.user, v_account, v_container, v_object, public)
817 except NotAllowedError:
818 raise Unauthorized('Access denied')
820 raise ItemNotFound('Object does not exist')
822 backend.update_object_meta(request.user, v_account, v_container, v_object, meta, replace)
823 except NotAllowedError:
824 raise Unauthorized('Access denied')
826 raise ItemNotFound('Object does not exist')
827 return HttpResponse(status=202)
829 # Single range update. Range must be in Content-Range.
830 # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
831 # (with the addition that '*' is allowed for the range - will append).
832 content_range = request.META.get('HTTP_CONTENT_RANGE')
833 if not content_range:
834 raise BadRequest('Missing Content-Range header')
835 ranges = get_content_range(request)
837 raise RangeNotSatisfiable('Invalid Content-Range header')
840 size, hashmap = backend.get_object_hashmap(request.user, v_account, v_container, v_object)
841 except NotAllowedError:
842 raise Unauthorized('Access denied')
844 raise ItemNotFound('Object does not exist')
846 offset, length, total = ranges
850 raise RangeNotSatisfiable('Supplied offset is beyond object limits')
852 src_container, src_name = split_container_object_string(src_object)
853 src_version = request.META.get('HTTP_X_SOURCE_VERSION')
855 src_size, src_hashmap = backend.get_object_hashmap(request.user, v_account, src_container, src_name, src_version)
856 except NotAllowedError:
857 raise Unauthorized('Access denied')
859 raise ItemNotFound('Source object does not exist')
863 elif length > src_size:
864 raise BadRequest('Object length is smaller than range length')
866 # Require either a Content-Length, or 'chunked' Transfer-Encoding.
868 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
869 content_length = get_content_length(request)
872 length = content_length
874 if content_length == -1:
875 # TODO: Get up to length bytes in chunks.
876 length = content_length
877 elif length != content_length:
878 raise BadRequest('Content length does not match range length')
879 if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
880 raise RangeNotSatisfiable('Supplied range will change provided object limits')
882 dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
883 if dest_bytes is not None:
884 dest_bytes = get_int_parameter(dest_bytes)
885 if dest_bytes is None:
886 raise BadRequest('Invalid X-Object-Bytes header')
889 if offset % backend.block_size == 0:
890 # Update the hashes only.
893 bi = int(offset / backend.block_size)
894 bl = min(length, backend.block_size)
895 if bi < len(hashmap):
896 if bl == backend.block_size:
897 hashmap[bi] = src_hashmap[sbi]
899 data = backend.get_block(src_hashmap[sbi])
900 hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], 0)
902 hashmap.append(src_hashmap[sbi])
910 data += backend.get_block(src_hashmap[sbi])
911 if length < backend.block_size:
913 bytes = put_object_block(hashmap, data, offset)
919 sock = raw_input_socket(request)
921 for d in socket_read_iterator(sock, length, backend.block_size):
922 # TODO: Raise 408 (Request Timeout) if this takes too long.
923 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
925 bytes = put_object_block(hashmap, data, offset)
929 put_object_block(hashmap, data, offset)
933 if dest_bytes is not None and dest_bytes < size:
935 hashmap = hashmap[:(int((size - 1) / backend.block_size) + 1)]
936 meta.update({'hash': hashmap_hash(hashmap)}) # Update ETag.
938 backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, replace, permissions)
939 except NotAllowedError:
940 raise Unauthorized('Access denied')
942 raise ItemNotFound('Container does not exist')
944 raise BadRequest('Invalid sharing header')
945 except AttributeError, e:
946 raise Conflict(json.dumps(e.data))
947 if public is not None:
949 backend.update_object_public(request.user, v_account, v_container, v_object, public)
950 except NotAllowedError:
951 raise Unauthorized('Access denied')
953 raise ItemNotFound('Object does not exist')
955 response = HttpResponse(status=204)
956 response['ETag'] = meta['hash']
959 @api_method('DELETE')
960 def object_delete(request, v_account, v_container, v_object):
961 # Normal Response Codes: 204
962 # Error Response Codes: serviceUnavailable (503),
963 # itemNotFound (404),
964 # unauthorized (401),
967 until = get_int_parameter(request.GET.get('until'))
969 backend.delete_object(request.user, v_account, v_container, v_object, until)
970 except NotAllowedError:
971 raise Unauthorized('Access denied')
973 raise ItemNotFound('Object does not exist')
974 return HttpResponse(status=204)
977 def method_not_allowed(request):
978 raise BadRequest('Method not allowed')