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.
37 from django.conf import settings
38 from django.http import HttpResponse
39 from django.template.loader import render_to_string
40 from django.utils import simplejson as json
41 from django.utils.http import parse_etags
42 from django.utils.encoding import smart_unicode, smart_str
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 (rename_meta_key, 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, socket_read_iterator,
52 object_data_response, put_object_block, hashmap_hash, api_method, json_encode_decimal)
53 from pithos.backends import connect_backend
54 from pithos.backends.base import NotAllowedError
57 logger = logging.getLogger(__name__)
60 def top_demux(request):
61 if request.method == 'GET':
63 return account_list(request)
64 return authenticate(request)
66 return method_not_allowed(request)
68 def account_demux(request, v_account):
69 if request.method == 'HEAD':
70 return account_meta(request, v_account)
71 elif request.method == 'POST':
72 return account_update(request, v_account)
73 elif request.method == 'GET':
74 return container_list(request, v_account)
76 return method_not_allowed(request)
78 def container_demux(request, v_account, v_container):
79 if request.method == 'HEAD':
80 return container_meta(request, v_account, v_container)
81 elif request.method == 'PUT':
82 return container_create(request, v_account, v_container)
83 elif request.method == 'POST':
84 return container_update(request, v_account, v_container)
85 elif request.method == 'DELETE':
86 return container_delete(request, v_account, v_container)
87 elif request.method == 'GET':
88 return object_list(request, v_account, v_container)
90 return method_not_allowed(request)
92 def object_demux(request, v_account, v_container, v_object):
93 if request.method == 'HEAD':
94 return object_meta(request, v_account, v_container, v_object)
95 elif request.method == 'GET':
96 return object_read(request, v_account, v_container, v_object)
97 elif request.method == 'PUT':
98 return object_write(request, v_account, v_container, v_object)
99 elif request.method == 'COPY':
100 return object_copy(request, v_account, v_container, v_object)
101 elif request.method == 'MOVE':
102 return object_move(request, v_account, v_container, v_object)
103 elif request.method == 'POST':
104 if request.META.get('CONTENT_TYPE', '').startswith('multipart/form-data'):
105 return object_write_form(request, v_account, v_container, v_object)
106 return object_update(request, v_account, v_container, v_object)
107 elif request.method == 'DELETE':
108 return object_delete(request, v_account, v_container, v_object)
110 return method_not_allowed(request)
113 def authenticate(request):
114 # Normal Response Codes: 204
115 # Error Response Codes: serviceUnavailable (503),
116 # unauthorized (401),
119 x_auth_user = request.META.get('HTTP_X_AUTH_USER')
120 x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
121 if not x_auth_user or not x_auth_key:
122 raise BadRequest('Missing X-Auth-User or X-Auth-Key header')
123 response = HttpResponse(status=204)
125 uri = request.build_absolute_uri()
127 uri = uri[:uri.find('?')]
129 response['X-Auth-Token'] = x_auth_key
130 response['X-Storage-Url'] = uri + (uri.endswith('/') and '' or '/') + x_auth_user
133 @api_method('GET', format_allowed=True)
134 def account_list(request):
135 # Normal Response Codes: 200, 204
136 # Error Response Codes: serviceUnavailable (503),
139 response = HttpResponse()
141 marker = request.GET.get('marker')
142 limit = get_int_parameter(request.GET.get('limit'))
146 accounts = request.backend.list_accounts(request.user, marker, limit)
148 if request.serialization == 'text':
149 if len(accounts) == 0:
150 # The cloudfiles python bindings expect 200 if json/xml.
151 response.status_code = 204
153 response.status_code = 200
154 response.content = '\n'.join(accounts) + '\n'
160 meta = request.backend.get_account_meta(request.user, x)
161 groups = request.backend.get_account_groups(request.user, x)
162 except NotAllowedError:
163 raise Unauthorized('Access denied')
165 rename_meta_key(meta, 'modified', 'last_modified')
166 rename_meta_key(meta, 'until_timestamp', 'x_account_until_timestamp')
167 for k, v in groups.iteritems():
168 meta['X-Container-Group-' + k] = ','.join(v)
169 account_meta.append(printable_header_dict(meta))
170 if request.serialization == 'xml':
171 data = render_to_string('accounts.xml', {'accounts': account_meta})
172 elif request.serialization == 'json':
173 data = json.dumps(account_meta)
174 response.status_code = 200
175 response.content = data
179 def account_meta(request, v_account):
180 # Normal Response Codes: 204
181 # Error Response Codes: serviceUnavailable (503),
182 # unauthorized (401),
185 until = get_int_parameter(request.GET.get('until'))
187 meta = request.backend.get_account_meta(request.user, v_account, until)
188 groups = request.backend.get_account_groups(request.user, v_account)
189 except NotAllowedError:
190 raise Unauthorized('Access denied')
192 validate_modification_preconditions(request, meta)
194 response = HttpResponse(status=204)
195 put_account_headers(response, request.quota, meta, groups)
199 def account_update(request, v_account):
200 # Normal Response Codes: 202
201 # Error Response Codes: serviceUnavailable (503),
202 # unauthorized (401),
205 meta, groups = get_account_headers(request)
207 if 'update' in request.GET:
211 request.backend.update_account_groups(request.user, v_account,
213 except NotAllowedError:
214 raise Unauthorized('Access denied')
216 raise BadRequest('Invalid groups header')
219 request.backend.update_account_meta(request.user, v_account, meta,
221 except NotAllowedError:
222 raise Unauthorized('Access denied')
223 return HttpResponse(status=202)
225 @api_method('GET', format_allowed=True)
226 def container_list(request, v_account):
227 # Normal Response Codes: 200, 204
228 # Error Response Codes: serviceUnavailable (503),
229 # itemNotFound (404),
230 # unauthorized (401),
233 until = get_int_parameter(request.GET.get('until'))
235 meta = request.backend.get_account_meta(request.user, v_account, until)
236 groups = request.backend.get_account_groups(request.user, v_account)
237 except NotAllowedError:
238 raise Unauthorized('Access denied')
240 validate_modification_preconditions(request, meta)
242 response = HttpResponse()
243 put_account_headers(response, request.quota, meta, groups)
245 marker = request.GET.get('marker')
246 limit = get_int_parameter(request.GET.get('limit'))
251 if 'shared' in request.GET:
255 containers = request.backend.list_containers(request.user, v_account,
256 marker, limit, shared, until)
257 except NotAllowedError:
258 raise Unauthorized('Access denied')
262 if request.serialization == 'text':
263 if len(containers) == 0:
264 # The cloudfiles python bindings expect 200 if json/xml.
265 response.status_code = 204
267 response.status_code = 200
268 response.content = '\n'.join(containers) + '\n'
274 meta = request.backend.get_container_meta(request.user, v_account,
276 policy = request.backend.get_container_policy(request.user,
278 except NotAllowedError:
279 raise Unauthorized('Access denied')
283 rename_meta_key(meta, 'modified', 'last_modified')
284 rename_meta_key(meta, 'until_timestamp', 'x_container_until_timestamp')
285 for k, v in policy.iteritems():
286 meta['X-Container-Policy-' + k] = v
287 container_meta.append(printable_header_dict(meta))
288 if request.serialization == 'xml':
289 data = render_to_string('containers.xml', {'account': v_account, 'containers': container_meta})
290 elif request.serialization == 'json':
291 data = json.dumps(container_meta)
292 response.status_code = 200
293 response.content = data
297 def container_meta(request, v_account, v_container):
298 # Normal Response Codes: 204
299 # Error Response Codes: serviceUnavailable (503),
300 # itemNotFound (404),
301 # unauthorized (401),
304 until = get_int_parameter(request.GET.get('until'))
306 meta = request.backend.get_container_meta(request.user, v_account,
308 meta['object_meta'] = request.backend.list_object_meta(request.user,
309 v_account, v_container, until)
310 policy = request.backend.get_container_policy(request.user, v_account,
312 except NotAllowedError:
313 raise Unauthorized('Access denied')
315 raise ItemNotFound('Container does not exist')
317 validate_modification_preconditions(request, meta)
319 response = HttpResponse(status=204)
320 put_container_headers(request, response, meta, policy)
324 def container_create(request, v_account, v_container):
325 # Normal Response Codes: 201, 202
326 # Error Response Codes: serviceUnavailable (503),
327 # itemNotFound (404),
328 # unauthorized (401),
331 meta, policy = get_container_headers(request)
333 if policy and int(policy.get('quota', 0)) > request.quota:
334 policy['quota'] = request.quota
336 raise BadRequest('Invalid quota header')
339 request.backend.put_container(request.user, v_account, v_container, policy)
341 except NotAllowedError:
342 raise Unauthorized('Access denied')
344 raise BadRequest('Invalid policy header')
348 if ret == 202 and policy:
350 request.backend.update_container_policy(request.user, v_account,
351 v_container, policy, replace=False)
352 except NotAllowedError:
353 raise Unauthorized('Access denied')
355 raise ItemNotFound('Container does not exist')
357 raise BadRequest('Invalid policy header')
360 request.backend.update_container_meta(request.user, v_account,
361 v_container, meta, replace=False)
362 except NotAllowedError:
363 raise Unauthorized('Access denied')
365 raise ItemNotFound('Container does not exist')
367 return HttpResponse(status=ret)
370 def container_update(request, v_account, v_container):
371 # Normal Response Codes: 202
372 # Error Response Codes: serviceUnavailable (503),
373 # itemNotFound (404),
374 # unauthorized (401),
377 meta, policy = get_container_headers(request)
379 if 'update' in request.GET:
383 if int(policy.get('quota', 0)) > request.quota:
384 policy['quota'] = request.quota
386 raise BadRequest('Invalid quota header')
388 request.backend.update_container_policy(request.user, v_account,
389 v_container, policy, replace)
390 except NotAllowedError:
391 raise Unauthorized('Access denied')
393 raise ItemNotFound('Container does not exist')
395 raise BadRequest('Invalid policy header')
398 request.backend.update_container_meta(request.user, v_account,
399 v_container, meta, replace)
400 except NotAllowedError:
401 raise Unauthorized('Access denied')
403 raise ItemNotFound('Container does not exist')
406 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
407 content_length = get_int_parameter(request.META.get('CONTENT_LENGTH', 0))
408 content_type = request.META.get('CONTENT_TYPE')
410 if content_type and content_type == 'application/octet-stream' and content_length != 0:
411 for data in socket_read_iterator(request, content_length,
412 request.backend.block_size):
413 # TODO: Raise 408 (Request Timeout) if this takes too long.
414 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
415 hashmap.append(request.backend.put_block(data))
417 response = HttpResponse(status=202)
419 response.content = '\n'.join(hashmap) + '\n'
422 @api_method('DELETE')
423 def container_delete(request, v_account, v_container):
424 # Normal Response Codes: 204
425 # Error Response Codes: serviceUnavailable (503),
427 # itemNotFound (404),
428 # unauthorized (401),
431 until = get_int_parameter(request.GET.get('until'))
433 request.backend.delete_container(request.user, v_account, v_container,
435 except NotAllowedError:
436 raise Unauthorized('Access denied')
438 raise ItemNotFound('Container does not exist')
440 raise Conflict('Container is not empty')
441 return HttpResponse(status=204)
443 @api_method('GET', format_allowed=True)
444 def object_list(request, v_account, v_container):
445 # Normal Response Codes: 200, 204
446 # Error Response Codes: serviceUnavailable (503),
447 # itemNotFound (404),
448 # unauthorized (401),
451 until = get_int_parameter(request.GET.get('until'))
453 meta = request.backend.get_container_meta(request.user, v_account,
455 meta['object_meta'] = request.backend.list_object_meta(request.user,
456 v_account, v_container, until)
457 policy = request.backend.get_container_policy(request.user, v_account,
459 except NotAllowedError:
460 raise Unauthorized('Access denied')
462 raise ItemNotFound('Container does not exist')
464 validate_modification_preconditions(request, meta)
466 response = HttpResponse()
467 put_container_headers(request, response, meta, policy)
469 path = request.GET.get('path')
470 prefix = request.GET.get('prefix')
471 delimiter = request.GET.get('delimiter')
473 # Path overrides prefix and delimiter.
481 if prefix and delimiter:
482 prefix = prefix + delimiter
485 prefix = prefix.lstrip('/')
487 marker = request.GET.get('marker')
488 limit = get_int_parameter(request.GET.get('limit'))
492 keys = request.GET.get('meta')
494 keys = keys.split(',')
495 l = [smart_str(x) for x in keys if x.strip() != '']
496 keys = [format_header_key('X-Object-Meta-' + x.strip()) for x in l]
501 if 'shared' in request.GET:
505 objects = request.backend.list_objects(request.user, v_account,
506 v_container, prefix, delimiter, marker,
507 limit, virtual, keys, shared, until)
508 except NotAllowedError:
509 raise Unauthorized('Access denied')
511 raise ItemNotFound('Container does not exist')
513 if request.serialization == 'text':
514 if len(objects) == 0:
515 # The cloudfiles python bindings expect 200 if json/xml.
516 response.status_code = 204
518 response.status_code = 200
519 response.content = '\n'.join([x[0] for x in objects]) + '\n'
525 # Virtual objects/directories.
526 object_meta.append({'subdir': x[0]})
529 meta = request.backend.get_object_meta(request.user, v_account,
530 v_container, x[0], x[1])
532 permissions = request.backend.get_object_permissions(
533 request.user, v_account, v_container, x[0])
534 public = request.backend.get_object_public(request.user,
535 v_account, v_container, x[0])
539 except NotAllowedError:
540 raise Unauthorized('Access denied')
544 rename_meta_key(meta, 'modified', 'last_modified')
545 rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
546 rename_meta_key(meta, 'version', 'x_object_version')
547 rename_meta_key(meta, 'version_timestamp', 'x_object_version_timestamp')
548 update_sharing_meta(request, permissions, v_account, v_container, x[0], meta)
549 update_public_meta(public, meta)
550 object_meta.append(printable_header_dict(meta))
551 if request.serialization == 'xml':
552 data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
553 elif request.serialization == 'json':
554 data = json.dumps(object_meta, default=json_encode_decimal)
555 response.status_code = 200
556 response.content = data
560 def object_meta(request, v_account, v_container, v_object):
561 # Normal Response Codes: 204
562 # Error Response Codes: serviceUnavailable (503),
563 # itemNotFound (404),
564 # unauthorized (401),
567 version = request.GET.get('version')
569 meta = request.backend.get_object_meta(request.user, v_account,
570 v_container, v_object, version)
572 permissions = request.backend.get_object_permissions(request.user,
573 v_account, v_container, v_object)
574 public = request.backend.get_object_public(request.user, v_account,
575 v_container, v_object)
579 except NotAllowedError:
580 raise Unauthorized('Access denied')
582 raise ItemNotFound('Object does not exist')
584 raise ItemNotFound('Version does not exist')
586 update_manifest_meta(request, v_account, meta)
587 update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
588 update_public_meta(public, meta)
590 # Evaluate conditions.
591 validate_modification_preconditions(request, meta)
593 validate_matching_preconditions(request, meta)
595 response = HttpResponse(status=304)
596 response['ETag'] = meta['hash']
599 response = HttpResponse(status=200)
600 put_object_headers(response, meta)
603 @api_method('GET', format_allowed=True)
604 def object_read(request, v_account, v_container, v_object):
605 # Normal Response Codes: 200, 206
606 # Error Response Codes: serviceUnavailable (503),
607 # rangeNotSatisfiable (416),
608 # preconditionFailed (412),
609 # itemNotFound (404),
610 # unauthorized (401),
614 version = request.GET.get('version')
616 # Reply with the version list. Do this first, as the object may be deleted.
617 if version == 'list':
618 if request.serialization == 'text':
619 raise BadRequest('No format specified for version list.')
622 v = request.backend.list_versions(request.user, v_account,
623 v_container, v_object)
624 except NotAllowedError:
625 raise Unauthorized('Access denied')
627 if request.serialization == 'xml':
628 d['object'] = v_object
629 data = render_to_string('versions.xml', d)
630 elif request.serialization == 'json':
631 data = json.dumps(d, default=json_encode_decimal)
633 response = HttpResponse(data, status=200)
634 response['Content-Length'] = len(data)
638 meta = request.backend.get_object_meta(request.user, v_account,
639 v_container, v_object, version)
641 permissions = request.backend.get_object_permissions(request.user,
642 v_account, v_container, v_object)
643 public = request.backend.get_object_public(request.user, v_account,
644 v_container, v_object)
648 except NotAllowedError:
649 raise Unauthorized('Access denied')
651 raise ItemNotFound('Object does not exist')
653 raise ItemNotFound('Version does not exist')
655 update_manifest_meta(request, v_account, meta)
656 update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
657 update_public_meta(public, meta)
659 # Evaluate conditions.
660 validate_modification_preconditions(request, meta)
662 validate_matching_preconditions(request, meta)
664 response = HttpResponse(status=304)
665 response['ETag'] = meta['hash']
670 if 'X-Object-Manifest' in meta:
672 src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
673 objects = request.backend.list_objects(request.user, v_account,
674 src_container, prefix=src_name, virtual=False)
675 except NotAllowedError:
676 raise Unauthorized('Access denied')
678 raise BadRequest('Invalid X-Object-Manifest header')
680 raise ItemNotFound('Container does not exist')
684 s, h = request.backend.get_object_hashmap(request.user,
685 v_account, src_container, x[0], x[1])
688 except NotAllowedError:
689 raise Unauthorized('Access denied')
691 raise ItemNotFound('Object does not exist')
693 raise ItemNotFound('Version does not exist')
696 s, h = request.backend.get_object_hashmap(request.user, v_account,
697 v_container, v_object, version)
700 except NotAllowedError:
701 raise Unauthorized('Access denied')
703 raise ItemNotFound('Object does not exist')
705 raise ItemNotFound('Version does not exist')
707 # Reply with the hashmap.
708 if 'hashmap' in request.GET and request.serialization != 'text':
710 hashmap = sum(hashmaps, [])
712 'block_size': request.backend.block_size,
713 'block_hash': request.backend.hash_algorithm,
716 if request.serialization == 'xml':
717 d['object'] = v_object
718 data = render_to_string('hashes.xml', d)
719 elif request.serialization == 'json':
722 response = HttpResponse(data, status=200)
723 put_object_headers(response, meta)
724 response['Content-Length'] = len(data)
727 request.serialization = 'text' # Unset.
728 return object_data_response(request, sizes, hashmaps, meta)
730 @api_method('PUT', format_allowed=True)
731 def object_write(request, v_account, v_container, v_object):
732 # Normal Response Codes: 201
733 # Error Response Codes: serviceUnavailable (503),
734 # unprocessableEntity (422),
735 # lengthRequired (411),
737 # itemNotFound (404),
738 # unauthorized (401),
741 # Evaluate conditions.
742 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
744 meta = request.backend.get_object_meta(request.user, v_account,
745 v_container, v_object)
746 except NotAllowedError:
747 raise Unauthorized('Access denied')
750 validate_matching_preconditions(request, meta)
752 copy_from = smart_unicode(request.META.get('HTTP_X_COPY_FROM'), strings_only=True)
753 move_from = smart_unicode(request.META.get('HTTP_X_MOVE_FROM'), strings_only=True)
754 if copy_from or move_from:
755 content_length = get_content_length(request) # Required by the API.
757 src_account = smart_unicode(request.META.get('HTTP_X_SOURCE_ACCOUNT'), strings_only=True)
759 src_account = request.user
762 src_container, src_name = split_container_object_string(move_from)
764 raise BadRequest('Invalid X-Move-From header')
765 version_id = copy_or_move_object(request, src_account, src_container, src_name,
766 v_account, v_container, v_object, move=True)
769 src_container, src_name = split_container_object_string(copy_from)
771 raise BadRequest('Invalid X-Copy-From header')
772 version_id = copy_or_move_object(request, src_account, src_container, src_name,
773 v_account, v_container, v_object, move=False)
774 response = HttpResponse(status=201)
775 response['X-Object-Version'] = version_id
778 meta, permissions, public = get_object_headers(request)
780 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
781 content_length = get_content_length(request)
782 # Should be BadRequest, but API says otherwise.
783 if 'Content-Type' not in meta:
784 raise LengthRequired('Missing Content-Type header')
786 if 'hashmap' in request.GET:
787 if request.serialization not in ('json', 'xml'):
788 raise BadRequest('Invalid hashmap format')
791 for block in socket_read_iterator(request, content_length,
792 request.backend.block_size):
793 data = '%s%s' % (data, block)
795 if request.serialization == 'json':
797 if not hasattr(d, '__getitem__'):
798 raise BadRequest('Invalid data formating')
800 hashmap = d['hashes']
801 size = int(d['bytes'])
803 raise BadRequest('Invalid data formatting')
804 elif request.serialization == 'xml':
806 xml = minidom.parseString(data)
807 obj = xml.getElementsByTagName('object')[0]
808 size = int(obj.attributes['bytes'].value)
810 hashes = xml.getElementsByTagName('hash')
813 hashmap.append(hash.firstChild.data)
815 raise BadRequest('Invalid data formatting')
817 meta.update({'hash': hashmap_hash(request, hashmap)}) # Update ETag.
822 for data in socket_read_iterator(request, content_length,
823 request.backend.block_size):
824 # TODO: Raise 408 (Request Timeout) if this takes too long.
825 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
827 hashmap.append(request.backend.put_block(data))
830 meta['hash'] = md5.hexdigest().lower()
831 etag = request.META.get('HTTP_ETAG')
832 if etag and parse_etags(etag)[0].lower() != meta['hash']:
833 raise UnprocessableEntity('Object ETag does not match')
836 version_id = request.backend.update_object_hashmap(request.user,
837 v_account, v_container, v_object, size, hashmap, meta,
839 except NotAllowedError:
840 raise Unauthorized('Access denied')
841 except IndexError, e:
842 raise Conflict('\n'.join(e.data) + '\n')
844 raise ItemNotFound('Container does not exist')
846 raise BadRequest('Invalid sharing header')
847 except AttributeError, e:
848 raise Conflict('\n'.join(e.data) + '\n')
849 if public is not None:
851 request.backend.update_object_public(request.user, v_account,
852 v_container, v_object, public)
853 except NotAllowedError:
854 raise Unauthorized('Access denied')
856 raise ItemNotFound('Object does not exist')
858 response = HttpResponse(status=201)
859 response['ETag'] = meta['hash']
860 response['X-Object-Version'] = version_id
864 def object_write_form(request, v_account, v_container, v_object):
865 # Normal Response Codes: 201
866 # Error Response Codes: serviceUnavailable (503),
867 # itemNotFound (404),
868 # unauthorized (401),
871 if not request.FILES.has_key('X-Object-Data'):
872 raise BadRequest('Missing X-Object-Data field')
873 file = request.FILES['X-Object-Data']
876 meta['Content-Type'] = file.content_type
881 for data in file.chunks(request.backend.block_size):
883 hashmap.append(request.backend.put_block(data))
886 meta['hash'] = md5.hexdigest().lower()
889 version_id = request.backend.update_object_hashmap(request.user,
890 v_account, v_container, v_object, size, hashmap, meta, True)
891 except NotAllowedError:
892 raise Unauthorized('Access denied')
894 raise ItemNotFound('Container does not exist')
896 response = HttpResponse(status=201)
897 response['ETag'] = meta['hash']
898 response['X-Object-Version'] = version_id
902 def object_copy(request, v_account, v_container, v_object):
903 # Normal Response Codes: 201
904 # Error Response Codes: serviceUnavailable (503),
905 # itemNotFound (404),
906 # unauthorized (401),
909 dest_account = smart_unicode(request.META.get('HTTP_DESTINATION_ACCOUNT'), strings_only=True)
911 dest_account = request.user
912 dest_path = smart_unicode(request.META.get('HTTP_DESTINATION'), strings_only=True)
914 raise BadRequest('Missing Destination header')
916 dest_container, dest_name = split_container_object_string(dest_path)
918 raise BadRequest('Invalid Destination header')
920 # Evaluate conditions.
921 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
922 src_version = request.META.get('HTTP_X_SOURCE_VERSION')
924 meta = request.backend.get_object_meta(request.user, v_account,
925 v_container, v_object, src_version)
926 except NotAllowedError:
927 raise Unauthorized('Access denied')
928 except (NameError, IndexError):
929 raise ItemNotFound('Container or object does not exist')
930 validate_matching_preconditions(request, meta)
932 version_id = copy_or_move_object(request, v_account, v_container, v_object,
933 dest_account, dest_container, dest_name, move=False)
934 response = HttpResponse(status=201)
935 response['X-Object-Version'] = version_id
939 def object_move(request, v_account, v_container, v_object):
940 # Normal Response Codes: 201
941 # Error Response Codes: serviceUnavailable (503),
942 # itemNotFound (404),
943 # unauthorized (401),
946 dest_account = smart_unicode(request.META.get('HTTP_DESTINATION_ACCOUNT'), strings_only=True)
948 dest_account = request.user
949 dest_path = smart_unicode(request.META.get('HTTP_DESTINATION'), strings_only=True)
951 raise BadRequest('Missing Destination header')
953 dest_container, dest_name = split_container_object_string(dest_path)
955 raise BadRequest('Invalid Destination header')
957 # Evaluate conditions.
958 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
960 meta = request.backend.get_object_meta(request.user, v_account,
961 v_container, v_object)
962 except NotAllowedError:
963 raise Unauthorized('Access denied')
965 raise ItemNotFound('Container or object does not exist')
966 validate_matching_preconditions(request, meta)
968 version_id = copy_or_move_object(request, v_account, v_container, v_object,
969 dest_account, dest_container, dest_name, move=True)
970 response = HttpResponse(status=201)
971 response['X-Object-Version'] = version_id
975 def object_update(request, v_account, v_container, v_object):
976 # Normal Response Codes: 202, 204
977 # Error Response Codes: serviceUnavailable (503),
979 # itemNotFound (404),
980 # unauthorized (401),
982 meta, permissions, public = get_object_headers(request)
983 content_type = meta.get('Content-Type')
985 del(meta['Content-Type']) # Do not allow changing the Content-Type.
988 prev_meta = request.backend.get_object_meta(request.user, v_account,
989 v_container, v_object)
990 except NotAllowedError:
991 raise Unauthorized('Access denied')
993 raise ItemNotFound('Object does not exist')
995 # Evaluate conditions.
996 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
997 validate_matching_preconditions(request, prev_meta)
999 # If replacing, keep previous values of 'Content-Type' and 'hash'.
1001 if 'update' in request.GET:
1004 for k in ('Content-Type', 'hash'):
1006 meta[k] = prev_meta[k]
1008 # A Content-Type or X-Source-Object header indicates data updates.
1009 src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
1010 if (not content_type or content_type != 'application/octet-stream') and not src_object:
1011 response = HttpResponse(status=202)
1013 # Do permissions first, as it may fail easier.
1014 if permissions is not None:
1016 request.backend.update_object_permissions(request.user,
1017 v_account, v_container, v_object, permissions)
1018 except NotAllowedError:
1019 raise Unauthorized('Access denied')
1021 raise ItemNotFound('Object does not exist')
1023 raise BadRequest('Invalid sharing header')
1024 except AttributeError, e:
1025 raise Conflict('\n'.join(e.data) + '\n')
1026 if public is not None:
1028 request.backend.update_object_public(request.user, v_account,
1029 v_container, v_object, public)
1030 except NotAllowedError:
1031 raise Unauthorized('Access denied')
1033 raise ItemNotFound('Object does not exist')
1036 version_id = request.backend.update_object_meta(request.user,
1037 v_account, v_container, v_object, meta, replace)
1038 except NotAllowedError:
1039 raise Unauthorized('Access denied')
1041 raise ItemNotFound('Object does not exist')
1042 response['X-Object-Version'] = version_id
1046 # Single range update. Range must be in Content-Range.
1047 # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
1048 # (with the addition that '*' is allowed for the range - will append).
1049 content_range = request.META.get('HTTP_CONTENT_RANGE')
1050 if not content_range:
1051 raise BadRequest('Missing Content-Range header')
1052 ranges = get_content_range(request)
1054 raise RangeNotSatisfiable('Invalid Content-Range header')
1057 size, hashmap = request.backend.get_object_hashmap(request.user,
1058 v_account, v_container, v_object)
1059 except NotAllowedError:
1060 raise Unauthorized('Access denied')
1062 raise ItemNotFound('Object does not exist')
1064 offset, length, total = ranges
1068 raise RangeNotSatisfiable('Supplied offset is beyond object limits')
1070 src_account = smart_unicode(request.META.get('HTTP_X_SOURCE_ACCOUNT'), strings_only=True)
1072 src_account = request.user
1073 src_container, src_name = split_container_object_string(src_object)
1074 src_container = smart_unicode(src_container, strings_only=True)
1075 src_name = smart_unicode(src_name, strings_only=True)
1076 src_version = request.META.get('HTTP_X_SOURCE_VERSION')
1078 src_size, src_hashmap = request.backend.get_object_hashmap(request.user,
1079 src_account, src_container, src_name, src_version)
1080 except NotAllowedError:
1081 raise Unauthorized('Access denied')
1083 raise ItemNotFound('Source object does not exist')
1087 elif length > src_size:
1088 raise BadRequest('Object length is smaller than range length')
1090 # Require either a Content-Length, or 'chunked' Transfer-Encoding.
1092 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
1093 content_length = get_content_length(request)
1096 length = content_length
1098 if content_length == -1:
1099 # TODO: Get up to length bytes in chunks.
1100 length = content_length
1101 elif length != content_length:
1102 raise BadRequest('Content length does not match range length')
1103 if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
1104 raise RangeNotSatisfiable('Supplied range will change provided object limits')
1106 dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
1107 if dest_bytes is not None:
1108 dest_bytes = get_int_parameter(dest_bytes)
1109 if dest_bytes is None:
1110 raise BadRequest('Invalid X-Object-Bytes header')
1113 if offset % request.backend.block_size == 0:
1114 # Update the hashes only.
1117 bi = int(offset / request.backend.block_size)
1118 bl = min(length, request.backend.block_size)
1119 if bi < len(hashmap):
1120 if bl == request.backend.block_size:
1121 hashmap[bi] = src_hashmap[sbi]
1123 data = request.backend.get_block(src_hashmap[sbi])
1124 hashmap[bi] = request.backend.update_block(hashmap[bi],
1127 hashmap.append(src_hashmap[sbi])
1135 data += request.backend.get_block(src_hashmap[sbi])
1136 if length < request.backend.block_size:
1137 data = data[:length]
1138 bytes = put_object_block(request, hashmap, data, offset)
1145 for d in socket_read_iterator(request, length,
1146 request.backend.block_size):
1147 # TODO: Raise 408 (Request Timeout) if this takes too long.
1148 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
1150 bytes = put_object_block(request, hashmap, data, offset)
1154 put_object_block(request, hashmap, data, offset)
1158 if dest_bytes is not None and dest_bytes < size:
1160 hashmap = hashmap[:(int((size - 1) / request.backend.block_size) + 1)]
1161 meta.update({'hash': hashmap_hash(request, hashmap)}) # Update ETag.
1163 version_id = request.backend.update_object_hashmap(request.user,
1164 v_account, v_container, v_object, size, hashmap, meta,
1165 replace, permissions)
1166 except NotAllowedError:
1167 raise Unauthorized('Access denied')
1169 raise ItemNotFound('Container does not exist')
1171 raise BadRequest('Invalid sharing header')
1172 except AttributeError, e:
1173 raise Conflict('\n'.join(e.data) + '\n')
1174 if public is not None:
1176 request.backend.update_object_public(request.user, v_account,
1177 v_container, v_object, public)
1178 except NotAllowedError:
1179 raise Unauthorized('Access denied')
1181 raise ItemNotFound('Object does not exist')
1183 response = HttpResponse(status=204)
1184 response['ETag'] = meta['hash']
1185 response['X-Object-Version'] = version_id
1188 @api_method('DELETE')
1189 def object_delete(request, v_account, v_container, v_object):
1190 # Normal Response Codes: 204
1191 # Error Response Codes: serviceUnavailable (503),
1192 # itemNotFound (404),
1193 # unauthorized (401),
1196 until = get_int_parameter(request.GET.get('until'))
1198 request.backend.delete_object(request.user, v_account, v_container,
1200 except NotAllowedError:
1201 raise Unauthorized('Access denied')
1203 raise ItemNotFound('Object does not exist')
1204 return HttpResponse(status=204)
1207 def method_not_allowed(request):
1208 raise BadRequest('Method not allowed')