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, Forbidden, ItemNotFound, Conflict,
46 LengthRequired, PreconditionFailed, RequestEntityTooLarge, 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.base import NotAllowedError, QuotaError
56 logger = logging.getLogger(__name__)
59 def top_demux(request):
60 if request.method == 'GET':
61 if getattr(request, 'user', None) is not None:
62 return account_list(request)
63 return authenticate(request)
65 return method_not_allowed(request)
67 def account_demux(request, v_account):
68 if request.method == 'HEAD':
69 return account_meta(request, v_account)
70 elif request.method == 'POST':
71 return account_update(request, v_account)
72 elif request.method == 'GET':
73 return container_list(request, v_account)
75 return method_not_allowed(request)
77 def container_demux(request, v_account, v_container):
78 if request.method == 'HEAD':
79 return container_meta(request, v_account, v_container)
80 elif request.method == 'PUT':
81 return container_create(request, v_account, v_container)
82 elif request.method == 'POST':
83 return container_update(request, v_account, v_container)
84 elif request.method == 'DELETE':
85 return container_delete(request, v_account, v_container)
86 elif request.method == 'GET':
87 return object_list(request, v_account, v_container)
89 return method_not_allowed(request)
91 def object_demux(request, v_account, v_container, v_object):
92 if request.method == 'HEAD':
93 return object_meta(request, v_account, v_container, v_object)
94 elif request.method == 'GET':
95 return object_read(request, v_account, v_container, v_object)
96 elif request.method == 'PUT':
97 return object_write(request, v_account, v_container, v_object)
98 elif request.method == 'COPY':
99 return object_copy(request, v_account, v_container, v_object)
100 elif request.method == 'MOVE':
101 return object_move(request, v_account, v_container, v_object)
102 elif request.method == 'POST':
103 if request.META.get('CONTENT_TYPE', '').startswith('multipart/form-data'):
104 return object_write_form(request, v_account, v_container, v_object)
105 return object_update(request, v_account, v_container, v_object)
106 elif request.method == 'DELETE':
107 return object_delete(request, v_account, v_container, v_object)
109 return method_not_allowed(request)
111 @api_method('GET', user_required=False)
112 def authenticate(request):
113 # Normal Response Codes: 204
114 # Error Response Codes: serviceUnavailable (503),
118 x_auth_user = request.META.get('HTTP_X_AUTH_USER')
119 x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
120 if not x_auth_user or not x_auth_key:
121 raise BadRequest('Missing X-Auth-User or X-Auth-Key header')
122 response = HttpResponse(status=204)
124 uri = request.build_absolute_uri()
126 uri = uri[:uri.find('?')]
128 response['X-Auth-Token'] = x_auth_key
129 response['X-Storage-Url'] = uri + (uri.endswith('/') and '' or '/') + x_auth_user
132 @api_method('GET', format_allowed=True)
133 def account_list(request):
134 # Normal Response Codes: 200, 204
135 # Error Response Codes: serviceUnavailable (503),
138 response = HttpResponse()
140 marker = request.GET.get('marker')
141 limit = get_int_parameter(request.GET.get('limit'))
145 accounts = request.backend.list_accounts(request.user_uniq, marker, limit)
147 if request.serialization == 'text':
148 if len(accounts) == 0:
149 # The cloudfiles python bindings expect 200 if json/xml.
150 response.status_code = 204
152 response.status_code = 200
153 response.content = '\n'.join(accounts) + '\n'
159 meta = request.backend.get_account_meta(request.user_uniq, x)
160 groups = request.backend.get_account_groups(request.user_uniq, x)
161 except NotAllowedError:
162 raise Forbidden('Not allowed')
164 rename_meta_key(meta, 'modified', 'last_modified')
165 rename_meta_key(meta, 'until_timestamp', 'x_account_until_timestamp')
166 for k, v in groups.iteritems():
167 meta['X-Container-Group-' + k] = ','.join(v)
168 account_meta.append(printable_header_dict(meta))
169 if request.serialization == 'xml':
170 data = render_to_string('accounts.xml', {'accounts': account_meta})
171 elif request.serialization == 'json':
172 data = json.dumps(account_meta)
173 response.status_code = 200
174 response.content = data
178 def account_meta(request, v_account):
179 # Normal Response Codes: 204
180 # Error Response Codes: serviceUnavailable (503),
184 until = get_int_parameter(request.GET.get('until'))
186 meta = request.backend.get_account_meta(request.user_uniq, v_account, until)
187 groups = request.backend.get_account_groups(request.user_uniq, v_account)
188 policy = request.backend.get_account_policy(request.user_uniq, v_account)
189 except NotAllowedError:
190 raise Forbidden('Not allowed')
192 validate_modification_preconditions(request, meta)
194 response = HttpResponse(status=204)
195 put_account_headers(response, meta, groups, policy)
199 def account_update(request, v_account):
200 # Normal Response Codes: 202
201 # Error Response Codes: serviceUnavailable (503),
205 meta, groups = get_account_headers(request)
207 if 'update' in request.GET:
211 request.backend.update_account_groups(request.user_uniq, v_account,
213 except NotAllowedError:
214 raise Forbidden('Not allowed')
216 raise BadRequest('Invalid groups header')
219 request.backend.update_account_meta(request.user_uniq, v_account, meta,
221 except NotAllowedError:
222 raise Forbidden('Not allowed')
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),
233 until = get_int_parameter(request.GET.get('until'))
235 meta = request.backend.get_account_meta(request.user_uniq, v_account, until)
236 groups = request.backend.get_account_groups(request.user_uniq, v_account)
237 policy = request.backend.get_account_policy(request.user_uniq, v_account)
238 except NotAllowedError:
239 raise Forbidden('Not allowed')
241 validate_modification_preconditions(request, meta)
243 response = HttpResponse()
244 put_account_headers(response, meta, groups, policy)
246 marker = request.GET.get('marker')
247 limit = get_int_parameter(request.GET.get('limit'))
252 if 'shared' in request.GET:
256 containers = request.backend.list_containers(request.user_uniq, v_account,
257 marker, limit, shared, until)
258 except NotAllowedError:
259 raise Forbidden('Not allowed')
263 if request.serialization == 'text':
264 if len(containers) == 0:
265 # The cloudfiles python bindings expect 200 if json/xml.
266 response.status_code = 204
268 response.status_code = 200
269 response.content = '\n'.join(containers) + '\n'
275 meta = request.backend.get_container_meta(request.user_uniq, v_account,
277 policy = request.backend.get_container_policy(request.user_uniq,
279 except NotAllowedError:
280 raise Forbidden('Not allowed')
284 rename_meta_key(meta, 'modified', 'last_modified')
285 rename_meta_key(meta, 'until_timestamp', 'x_container_until_timestamp')
286 for k, v in policy.iteritems():
287 meta['X-Container-Policy-' + k] = v
288 container_meta.append(printable_header_dict(meta))
289 if request.serialization == 'xml':
290 data = render_to_string('containers.xml', {'account': v_account, 'containers': container_meta})
291 elif request.serialization == 'json':
292 data = json.dumps(container_meta)
293 response.status_code = 200
294 response.content = data
298 def container_meta(request, v_account, v_container):
299 # Normal Response Codes: 204
300 # Error Response Codes: serviceUnavailable (503),
301 # itemNotFound (404),
305 until = get_int_parameter(request.GET.get('until'))
307 meta = request.backend.get_container_meta(request.user_uniq, v_account,
309 meta['object_meta'] = request.backend.list_object_meta(request.user_uniq,
310 v_account, v_container, until)
311 policy = request.backend.get_container_policy(request.user_uniq, v_account,
313 except NotAllowedError:
314 raise Forbidden('Not allowed')
316 raise ItemNotFound('Container does not exist')
318 validate_modification_preconditions(request, meta)
320 response = HttpResponse(status=204)
321 put_container_headers(request, response, meta, policy)
325 def container_create(request, v_account, v_container):
326 # Normal Response Codes: 201, 202
327 # Error Response Codes: serviceUnavailable (503),
328 # itemNotFound (404),
332 meta, policy = get_container_headers(request)
335 request.backend.put_container(request.user_uniq, v_account, v_container, policy)
337 except NotAllowedError:
338 raise Forbidden('Not allowed')
340 raise BadRequest('Invalid policy header')
344 if ret == 202 and policy:
346 request.backend.update_container_policy(request.user_uniq, v_account,
347 v_container, policy, replace=False)
348 except NotAllowedError:
349 raise Forbidden('Not allowed')
351 raise ItemNotFound('Container does not exist')
353 raise BadRequest('Invalid policy header')
356 request.backend.update_container_meta(request.user_uniq, v_account,
357 v_container, meta, replace=False)
358 except NotAllowedError:
359 raise Forbidden('Not allowed')
361 raise ItemNotFound('Container does not exist')
363 return HttpResponse(status=ret)
366 def container_update(request, v_account, v_container):
367 # Normal Response Codes: 202
368 # Error Response Codes: serviceUnavailable (503),
369 # itemNotFound (404),
373 meta, policy = get_container_headers(request)
375 if 'update' in request.GET:
379 request.backend.update_container_policy(request.user_uniq, v_account,
380 v_container, policy, replace)
381 except NotAllowedError:
382 raise Forbidden('Not allowed')
384 raise ItemNotFound('Container does not exist')
386 raise BadRequest('Invalid policy header')
389 request.backend.update_container_meta(request.user_uniq, v_account,
390 v_container, meta, replace)
391 except NotAllowedError:
392 raise Forbidden('Not allowed')
394 raise ItemNotFound('Container does not exist')
397 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
398 content_length = get_int_parameter(request.META.get('CONTENT_LENGTH', 0))
399 content_type = request.META.get('CONTENT_TYPE')
401 if content_type and content_type == 'application/octet-stream' and content_length != 0:
402 for data in socket_read_iterator(request, content_length,
403 request.backend.block_size):
404 # TODO: Raise 408 (Request Timeout) if this takes too long.
405 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
406 hashmap.append(request.backend.put_block(data))
408 response = HttpResponse(status=202)
410 response.content = '\n'.join(hashmap) + '\n'
413 @api_method('DELETE')
414 def container_delete(request, v_account, v_container):
415 # Normal Response Codes: 204
416 # Error Response Codes: serviceUnavailable (503),
418 # itemNotFound (404),
422 until = get_int_parameter(request.GET.get('until'))
424 request.backend.delete_container(request.user_uniq, v_account, v_container,
426 except NotAllowedError:
427 raise Forbidden('Not allowed')
429 raise ItemNotFound('Container does not exist')
431 raise Conflict('Container is not empty')
432 return HttpResponse(status=204)
434 @api_method('GET', format_allowed=True)
435 def object_list(request, v_account, v_container):
436 # Normal Response Codes: 200, 204
437 # Error Response Codes: serviceUnavailable (503),
438 # itemNotFound (404),
442 until = get_int_parameter(request.GET.get('until'))
444 meta = request.backend.get_container_meta(request.user_uniq, v_account,
446 meta['object_meta'] = request.backend.list_object_meta(request.user_uniq,
447 v_account, v_container, until)
448 policy = request.backend.get_container_policy(request.user_uniq, v_account,
450 except NotAllowedError:
451 raise Forbidden('Not allowed')
453 raise ItemNotFound('Container does not exist')
455 validate_modification_preconditions(request, meta)
457 response = HttpResponse()
458 put_container_headers(request, response, meta, policy)
460 path = request.GET.get('path')
461 prefix = request.GET.get('prefix')
462 delimiter = request.GET.get('delimiter')
464 # Path overrides prefix and delimiter.
472 if prefix and delimiter:
473 prefix = prefix + delimiter
476 prefix = prefix.lstrip('/')
478 marker = request.GET.get('marker')
479 limit = get_int_parameter(request.GET.get('limit'))
483 keys = request.GET.get('meta')
485 keys = keys.split(',')
486 l = [smart_str(x) for x in keys if x.strip() != '']
487 keys = [format_header_key('X-Object-Meta-' + x.strip()) for x in l]
492 if 'shared' in request.GET:
496 objects = request.backend.list_objects(request.user_uniq, v_account,
497 v_container, prefix, delimiter, marker,
498 limit, virtual, keys, shared, until)
499 except NotAllowedError:
500 raise Forbidden('Not allowed')
502 raise ItemNotFound('Container does not exist')
504 if request.serialization == 'text':
505 if len(objects) == 0:
506 # The cloudfiles python bindings expect 200 if json/xml.
507 response.status_code = 204
509 response.status_code = 200
510 response.content = '\n'.join([x[0] for x in objects]) + '\n'
516 # Virtual objects/directories.
517 object_meta.append({'subdir': x[0]})
520 meta = request.backend.get_object_meta(request.user_uniq, v_account,
521 v_container, x[0], x[1])
523 permissions = request.backend.get_object_permissions(
524 request.user_uniq, v_account, v_container, x[0])
525 public = request.backend.get_object_public(request.user_uniq,
526 v_account, v_container, x[0])
530 except NotAllowedError:
531 raise Forbidden('Not allowed')
535 rename_meta_key(meta, 'modified', 'last_modified')
536 rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
537 rename_meta_key(meta, 'version', 'x_object_version')
538 rename_meta_key(meta, 'version_timestamp', 'x_object_version_timestamp')
539 update_sharing_meta(request, permissions, v_account, v_container, x[0], meta)
540 update_public_meta(public, meta)
541 object_meta.append(printable_header_dict(meta))
542 if request.serialization == 'xml':
543 data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
544 elif request.serialization == 'json':
545 data = json.dumps(object_meta, default=json_encode_decimal)
546 response.status_code = 200
547 response.content = data
551 def object_meta(request, v_account, v_container, v_object):
552 # Normal Response Codes: 204
553 # Error Response Codes: serviceUnavailable (503),
554 # itemNotFound (404),
558 version = request.GET.get('version')
560 meta = request.backend.get_object_meta(request.user_uniq, v_account,
561 v_container, v_object, version)
563 permissions = request.backend.get_object_permissions(request.user_uniq,
564 v_account, v_container, v_object)
565 public = request.backend.get_object_public(request.user_uniq, v_account,
566 v_container, v_object)
570 except NotAllowedError:
571 raise Forbidden('Not allowed')
573 raise ItemNotFound('Object does not exist')
575 raise ItemNotFound('Version does not exist')
577 update_manifest_meta(request, v_account, meta)
578 update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
579 update_public_meta(public, meta)
581 # Evaluate conditions.
582 validate_modification_preconditions(request, meta)
584 validate_matching_preconditions(request, meta)
586 response = HttpResponse(status=304)
587 response['ETag'] = meta['hash']
590 response = HttpResponse(status=200)
591 put_object_headers(response, meta)
594 @api_method('GET', format_allowed=True)
595 def object_read(request, v_account, v_container, v_object):
596 # Normal Response Codes: 200, 206
597 # Error Response Codes: serviceUnavailable (503),
598 # rangeNotSatisfiable (416),
599 # preconditionFailed (412),
600 # itemNotFound (404),
605 version = request.GET.get('version')
607 # Reply with the version list. Do this first, as the object may be deleted.
608 if version == 'list':
609 if request.serialization == 'text':
610 raise BadRequest('No format specified for version list.')
613 v = request.backend.list_versions(request.user_uniq, v_account,
614 v_container, v_object)
615 except NotAllowedError:
616 raise Forbidden('Not allowed')
618 if request.serialization == 'xml':
619 d['object'] = v_object
620 data = render_to_string('versions.xml', d)
621 elif request.serialization == 'json':
622 data = json.dumps(d, default=json_encode_decimal)
624 response = HttpResponse(data, status=200)
625 response['Content-Length'] = len(data)
629 meta = request.backend.get_object_meta(request.user_uniq, v_account,
630 v_container, v_object, version)
632 permissions = request.backend.get_object_permissions(request.user_uniq,
633 v_account, v_container, v_object)
634 public = request.backend.get_object_public(request.user_uniq, v_account,
635 v_container, v_object)
639 except NotAllowedError:
640 raise Forbidden('Not allowed')
642 raise ItemNotFound('Object does not exist')
644 raise ItemNotFound('Version does not exist')
646 update_manifest_meta(request, v_account, meta)
647 update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
648 update_public_meta(public, meta)
650 # Evaluate conditions.
651 validate_modification_preconditions(request, meta)
653 validate_matching_preconditions(request, meta)
655 response = HttpResponse(status=304)
656 response['ETag'] = meta['hash']
661 if 'X-Object-Manifest' in meta:
663 src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
664 objects = request.backend.list_objects(request.user_uniq, v_account,
665 src_container, prefix=src_name, virtual=False)
666 except NotAllowedError:
667 raise Forbidden('Not allowed')
669 raise BadRequest('Invalid X-Object-Manifest header')
671 raise ItemNotFound('Container does not exist')
675 s, h = request.backend.get_object_hashmap(request.user_uniq,
676 v_account, src_container, x[0], x[1])
679 except NotAllowedError:
680 raise Forbidden('Not allowed')
682 raise ItemNotFound('Object does not exist')
684 raise ItemNotFound('Version does not exist')
687 s, h = request.backend.get_object_hashmap(request.user_uniq, v_account,
688 v_container, v_object, version)
691 except NotAllowedError:
692 raise Forbidden('Not allowed')
694 raise ItemNotFound('Object does not exist')
696 raise ItemNotFound('Version does not exist')
698 # Reply with the hashmap.
699 if 'hashmap' in request.GET and request.serialization != 'text':
701 hashmap = sum(hashmaps, [])
703 'block_size': request.backend.block_size,
704 'block_hash': request.backend.hash_algorithm,
707 if request.serialization == 'xml':
708 d['object'] = v_object
709 data = render_to_string('hashes.xml', d)
710 elif request.serialization == 'json':
713 response = HttpResponse(data, status=200)
714 put_object_headers(response, meta)
715 response['Content-Length'] = len(data)
718 request.serialization = 'text' # Unset.
719 return object_data_response(request, sizes, hashmaps, meta)
721 @api_method('PUT', format_allowed=True)
722 def object_write(request, v_account, v_container, v_object):
723 # Normal Response Codes: 201
724 # Error Response Codes: serviceUnavailable (503),
725 # unprocessableEntity (422),
726 # lengthRequired (411),
728 # itemNotFound (404),
732 # Evaluate conditions.
733 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
735 meta = request.backend.get_object_meta(request.user_uniq, v_account,
736 v_container, v_object)
737 except NotAllowedError:
738 raise Forbidden('Not allowed')
741 validate_matching_preconditions(request, meta)
743 copy_from = smart_unicode(request.META.get('HTTP_X_COPY_FROM'), strings_only=True)
744 move_from = smart_unicode(request.META.get('HTTP_X_MOVE_FROM'), strings_only=True)
745 if copy_from or move_from:
746 content_length = get_content_length(request) # Required by the API.
748 src_account = smart_unicode(request.META.get('HTTP_X_SOURCE_ACCOUNT'), strings_only=True)
750 src_account = request.user_uniq
753 src_container, src_name = split_container_object_string(move_from)
755 raise BadRequest('Invalid X-Move-From header')
756 version_id = copy_or_move_object(request, src_account, src_container, src_name,
757 v_account, v_container, v_object, move=True)
760 src_container, src_name = split_container_object_string(copy_from)
762 raise BadRequest('Invalid X-Copy-From header')
763 version_id = copy_or_move_object(request, src_account, src_container, src_name,
764 v_account, v_container, v_object, move=False)
765 response = HttpResponse(status=201)
766 response['X-Object-Version'] = version_id
769 meta, permissions, public = get_object_headers(request)
771 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
772 content_length = get_content_length(request)
773 # Should be BadRequest, but API says otherwise.
774 if 'Content-Type' not in meta:
775 raise LengthRequired('Missing Content-Type header')
777 if 'hashmap' in request.GET:
778 if request.serialization not in ('json', 'xml'):
779 raise BadRequest('Invalid hashmap format')
782 for block in socket_read_iterator(request, content_length,
783 request.backend.block_size):
784 data = '%s%s' % (data, block)
786 if request.serialization == 'json':
788 if not hasattr(d, '__getitem__'):
789 raise BadRequest('Invalid data formating')
791 hashmap = d['hashes']
792 size = int(d['bytes'])
794 raise BadRequest('Invalid data formatting')
795 elif request.serialization == 'xml':
797 xml = minidom.parseString(data)
798 obj = xml.getElementsByTagName('object')[0]
799 size = int(obj.attributes['bytes'].value)
801 hashes = xml.getElementsByTagName('hash')
804 hashmap.append(hash.firstChild.data)
806 raise BadRequest('Invalid data formatting')
808 meta.update({'hash': hashmap_hash(request, hashmap)}) # Update ETag.
813 for data in socket_read_iterator(request, content_length,
814 request.backend.block_size):
815 # TODO: Raise 408 (Request Timeout) if this takes too long.
816 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
818 hashmap.append(request.backend.put_block(data))
821 meta['hash'] = md5.hexdigest().lower()
822 etag = request.META.get('HTTP_ETAG')
823 if etag and parse_etags(etag)[0].lower() != meta['hash']:
824 raise UnprocessableEntity('Object ETag does not match')
827 version_id = request.backend.update_object_hashmap(request.user_uniq,
828 v_account, v_container, v_object, size, hashmap, meta,
830 except NotAllowedError:
831 raise Forbidden('Not allowed')
832 except IndexError, e:
833 raise Conflict('\n'.join(e.data) + '\n')
835 raise ItemNotFound('Container does not exist')
837 raise BadRequest('Invalid sharing header')
838 except AttributeError, e:
839 raise Conflict('\n'.join(e.data) + '\n')
841 raise RequestEntityTooLarge('Quota exceeded')
842 if public is not None:
844 request.backend.update_object_public(request.user_uniq, v_account,
845 v_container, v_object, public)
846 except NotAllowedError:
847 raise Forbidden('Not allowed')
849 raise ItemNotFound('Object does not exist')
851 response = HttpResponse(status=201)
852 response['ETag'] = meta['hash']
853 response['X-Object-Version'] = version_id
857 def object_write_form(request, v_account, v_container, v_object):
858 # Normal Response Codes: 201
859 # Error Response Codes: serviceUnavailable (503),
860 # itemNotFound (404),
864 if not request.FILES.has_key('X-Object-Data'):
865 raise BadRequest('Missing X-Object-Data field')
866 file = request.FILES['X-Object-Data']
869 meta['Content-Type'] = file.content_type
874 for data in file.chunks(request.backend.block_size):
876 hashmap.append(request.backend.put_block(data))
879 meta['hash'] = md5.hexdigest().lower()
882 version_id = request.backend.update_object_hashmap(request.user_uniq,
883 v_account, v_container, v_object, size, hashmap, meta, True)
884 except NotAllowedError:
885 raise Forbidden('Not allowed')
887 raise ItemNotFound('Container does not exist')
889 raise RequestEntityTooLarge('Quota exceeded')
891 response = HttpResponse(status=201)
892 response['ETag'] = meta['hash']
893 response['X-Object-Version'] = version_id
897 def object_copy(request, v_account, v_container, v_object):
898 # Normal Response Codes: 201
899 # Error Response Codes: serviceUnavailable (503),
900 # itemNotFound (404),
904 dest_account = smart_unicode(request.META.get('HTTP_DESTINATION_ACCOUNT'), strings_only=True)
906 dest_account = request.user_uniq
907 dest_path = smart_unicode(request.META.get('HTTP_DESTINATION'), strings_only=True)
909 raise BadRequest('Missing Destination header')
911 dest_container, dest_name = split_container_object_string(dest_path)
913 raise BadRequest('Invalid Destination header')
915 # Evaluate conditions.
916 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
917 src_version = request.META.get('HTTP_X_SOURCE_VERSION')
919 meta = request.backend.get_object_meta(request.user_uniq, v_account,
920 v_container, v_object, src_version)
921 except NotAllowedError:
922 raise Forbidden('Not allowed')
923 except (NameError, IndexError):
924 raise ItemNotFound('Container or object does not exist')
925 validate_matching_preconditions(request, meta)
927 version_id = copy_or_move_object(request, v_account, v_container, v_object,
928 dest_account, dest_container, dest_name, move=False)
929 response = HttpResponse(status=201)
930 response['X-Object-Version'] = version_id
934 def object_move(request, v_account, v_container, v_object):
935 # Normal Response Codes: 201
936 # Error Response Codes: serviceUnavailable (503),
937 # itemNotFound (404),
941 dest_account = smart_unicode(request.META.get('HTTP_DESTINATION_ACCOUNT'), strings_only=True)
943 dest_account = request.user_uniq
944 dest_path = smart_unicode(request.META.get('HTTP_DESTINATION'), strings_only=True)
946 raise BadRequest('Missing Destination header')
948 dest_container, dest_name = split_container_object_string(dest_path)
950 raise BadRequest('Invalid Destination header')
952 # Evaluate conditions.
953 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
955 meta = request.backend.get_object_meta(request.user_uniq, v_account,
956 v_container, v_object)
957 except NotAllowedError:
958 raise Forbidden('Not allowed')
960 raise ItemNotFound('Container or object does not exist')
961 validate_matching_preconditions(request, meta)
963 version_id = copy_or_move_object(request, v_account, v_container, v_object,
964 dest_account, dest_container, dest_name, move=True)
965 response = HttpResponse(status=201)
966 response['X-Object-Version'] = version_id
970 def object_update(request, v_account, v_container, v_object):
971 # Normal Response Codes: 202, 204
972 # Error Response Codes: serviceUnavailable (503),
974 # itemNotFound (404),
977 meta, permissions, public = get_object_headers(request)
978 content_type = meta.get('Content-Type')
980 del(meta['Content-Type']) # Do not allow changing the Content-Type.
983 prev_meta = request.backend.get_object_meta(request.user_uniq, v_account,
984 v_container, v_object)
985 except NotAllowedError:
986 raise Forbidden('Not allowed')
988 raise ItemNotFound('Object does not exist')
990 # Evaluate conditions.
991 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
992 validate_matching_preconditions(request, prev_meta)
994 # If replacing, keep previous values of 'Content-Type' and 'hash'.
996 if 'update' in request.GET:
999 for k in ('Content-Type', 'hash'):
1001 meta[k] = prev_meta[k]
1003 # A Content-Type or X-Source-Object header indicates data updates.
1004 src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
1005 if (not content_type or content_type != 'application/octet-stream') and not src_object:
1006 response = HttpResponse(status=202)
1008 # Do permissions first, as it may fail easier.
1009 if permissions is not None:
1011 request.backend.update_object_permissions(request.user_uniq,
1012 v_account, v_container, v_object, permissions)
1013 except NotAllowedError:
1014 raise Forbidden('Not allowed')
1016 raise ItemNotFound('Object does not exist')
1018 raise BadRequest('Invalid sharing header')
1019 except AttributeError, e:
1020 raise Conflict('\n'.join(e.data) + '\n')
1021 if public is not None:
1023 request.backend.update_object_public(request.user_uniq, v_account,
1024 v_container, v_object, public)
1025 except NotAllowedError:
1026 raise Forbidden('Not allowed')
1028 raise ItemNotFound('Object does not exist')
1031 version_id = request.backend.update_object_meta(request.user_uniq,
1032 v_account, v_container, v_object, meta, replace)
1033 except NotAllowedError:
1034 raise Forbidden('Not allowed')
1036 raise ItemNotFound('Object does not exist')
1037 response['X-Object-Version'] = version_id
1041 # Single range update. Range must be in Content-Range.
1042 # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
1043 # (with the addition that '*' is allowed for the range - will append).
1044 content_range = request.META.get('HTTP_CONTENT_RANGE')
1045 if not content_range:
1046 raise BadRequest('Missing Content-Range header')
1047 ranges = get_content_range(request)
1049 raise RangeNotSatisfiable('Invalid Content-Range header')
1052 size, hashmap = request.backend.get_object_hashmap(request.user_uniq,
1053 v_account, v_container, v_object)
1054 except NotAllowedError:
1055 raise Forbidden('Not allowed')
1057 raise ItemNotFound('Object does not exist')
1059 offset, length, total = ranges
1063 raise RangeNotSatisfiable('Supplied offset is beyond object limits')
1065 src_account = smart_unicode(request.META.get('HTTP_X_SOURCE_ACCOUNT'), strings_only=True)
1067 src_account = request.user_uniq
1068 src_container, src_name = split_container_object_string(src_object)
1069 src_container = smart_unicode(src_container, strings_only=True)
1070 src_name = smart_unicode(src_name, strings_only=True)
1071 src_version = request.META.get('HTTP_X_SOURCE_VERSION')
1073 src_size, src_hashmap = request.backend.get_object_hashmap(request.user_uniq,
1074 src_account, src_container, src_name, src_version)
1075 except NotAllowedError:
1076 raise Forbidden('Not allowed')
1078 raise ItemNotFound('Source object does not exist')
1082 elif length > src_size:
1083 raise BadRequest('Object length is smaller than range length')
1085 # Require either a Content-Length, or 'chunked' Transfer-Encoding.
1087 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
1088 content_length = get_content_length(request)
1091 length = content_length
1093 if content_length == -1:
1094 # TODO: Get up to length bytes in chunks.
1095 length = content_length
1096 elif length != content_length:
1097 raise BadRequest('Content length does not match range length')
1098 if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
1099 raise RangeNotSatisfiable('Supplied range will change provided object limits')
1101 dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
1102 if dest_bytes is not None:
1103 dest_bytes = get_int_parameter(dest_bytes)
1104 if dest_bytes is None:
1105 raise BadRequest('Invalid X-Object-Bytes header')
1108 if offset % request.backend.block_size == 0:
1109 # Update the hashes only.
1112 bi = int(offset / request.backend.block_size)
1113 bl = min(length, request.backend.block_size)
1114 if bi < len(hashmap):
1115 if bl == request.backend.block_size:
1116 hashmap[bi] = src_hashmap[sbi]
1118 data = request.backend.get_block(src_hashmap[sbi])
1119 hashmap[bi] = request.backend.update_block(hashmap[bi],
1122 hashmap.append(src_hashmap[sbi])
1130 data += request.backend.get_block(src_hashmap[sbi])
1131 if length < request.backend.block_size:
1132 data = data[:length]
1133 bytes = put_object_block(request, hashmap, data, offset)
1140 for d in socket_read_iterator(request, length,
1141 request.backend.block_size):
1142 # TODO: Raise 408 (Request Timeout) if this takes too long.
1143 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
1145 bytes = put_object_block(request, hashmap, data, offset)
1149 put_object_block(request, hashmap, data, offset)
1153 if dest_bytes is not None and dest_bytes < size:
1155 hashmap = hashmap[:(int((size - 1) / request.backend.block_size) + 1)]
1156 meta.update({'hash': hashmap_hash(request, hashmap)}) # Update ETag.
1158 version_id = request.backend.update_object_hashmap(request.user_uniq,
1159 v_account, v_container, v_object, size, hashmap, meta,
1160 replace, permissions)
1161 except NotAllowedError:
1162 raise Forbidden('Not allowed')
1164 raise ItemNotFound('Container does not exist')
1166 raise BadRequest('Invalid sharing header')
1167 except AttributeError, e:
1168 raise Conflict('\n'.join(e.data) + '\n')
1170 raise RequestEntityTooLarge('Quota exceeded')
1171 if public is not None:
1173 request.backend.update_object_public(request.user_uniq, v_account,
1174 v_container, v_object, public)
1175 except NotAllowedError:
1176 raise Forbidden('Not allowed')
1178 raise ItemNotFound('Object does not exist')
1180 response = HttpResponse(status=204)
1181 response['ETag'] = meta['hash']
1182 response['X-Object-Version'] = version_id
1185 @api_method('DELETE')
1186 def object_delete(request, v_account, v_container, v_object):
1187 # Normal Response Codes: 204
1188 # Error Response Codes: serviceUnavailable (503),
1189 # itemNotFound (404),
1193 until = get_int_parameter(request.GET.get('until'))
1195 request.backend.delete_object(request.user_uniq, v_account, v_container,
1197 except NotAllowedError:
1198 raise Forbidden('Not allowed')
1200 raise ItemNotFound('Object does not exist')
1201 return HttpResponse(status=204)
1204 def method_not_allowed(request):
1205 raise BadRequest('Method not allowed')