1 # Copyright 2011-2012 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.
34 from xml.dom import minidom
35 from urllib import unquote
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_str
43 from django.views.decorators.csrf import csrf_exempt
45 from synnefo.lib.astakos import get_user
47 from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, Forbidden, ItemNotFound, Conflict,
48 LengthRequired, PreconditionFailed, RequestEntityTooLarge, RangeNotSatisfiable, UnprocessableEntity)
49 from pithos.api.util import (json_encode_decimal, rename_meta_key, format_header_key, printable_header_dict,
50 get_account_headers, put_account_headers, get_container_headers, put_container_headers, get_object_headers,
51 put_object_headers, update_manifest_meta, update_sharing_meta, update_public_meta,
52 validate_modification_preconditions, validate_matching_preconditions, split_container_object_string,
53 copy_or_move_object, get_int_parameter, get_content_length, get_content_range, socket_read_iterator,
54 SaveToBackendHandler, object_data_response, put_object_block, hashmap_md5, simple_list_response, api_method)
55 from pithos.api.settings import AUTHENTICATION_URL, AUTHENTICATION_USERS, COOKIE_NAME, UPDATE_MD5
57 from pithos.backends.base import NotAllowedError, QuotaError
58 from pithos.backends.filter import parse_filters
64 logger = logging.getLogger(__name__)
68 def top_demux(request):
69 get_user(request, AUTHENTICATION_URL, AUTHENTICATION_USERS)
70 if request.method == 'GET':
71 if getattr(request, 'user', None) is not None:
72 return account_list(request)
73 return authenticate(request)
75 return method_not_allowed(request)
78 def account_demux(request, v_account):
79 get_user(request, AUTHENTICATION_URL, AUTHENTICATION_USERS)
80 if request.method == 'HEAD':
81 return account_meta(request, v_account)
82 elif request.method == 'POST':
83 return account_update(request, v_account)
84 elif request.method == 'GET':
85 return container_list(request, v_account)
87 return method_not_allowed(request)
90 def container_demux(request, v_account, v_container):
91 get_user(request, AUTHENTICATION_URL, AUTHENTICATION_USERS)
92 if request.method == 'HEAD':
93 return container_meta(request, v_account, v_container)
94 elif request.method == 'PUT':
95 return container_create(request, v_account, v_container)
96 elif request.method == 'POST':
97 return container_update(request, v_account, v_container)
98 elif request.method == 'DELETE':
99 return container_delete(request, v_account, v_container)
100 elif request.method == 'GET':
101 return object_list(request, v_account, v_container)
103 return method_not_allowed(request)
106 def object_demux(request, v_account, v_container, v_object):
107 # Helper to avoid placing the token in the URL when loading objects from a browser.
109 if request.method in ('HEAD', 'GET') and COOKIE_NAME in request.COOKIES:
110 cookie_value = unquote(request.COOKIES.get(COOKIE_NAME, ''))
111 if cookie_value and '|' in cookie_value:
112 token = cookie_value.split('|', 1)[1]
113 get_user(request, AUTHENTICATION_URL, AUTHENTICATION_USERS, token)
114 if request.method == 'HEAD':
115 return object_meta(request, v_account, v_container, v_object)
116 elif request.method == 'GET':
117 return object_read(request, v_account, v_container, v_object)
118 elif request.method == 'PUT':
119 return object_write(request, v_account, v_container, v_object)
120 elif request.method == 'COPY':
121 return object_copy(request, v_account, v_container, v_object)
122 elif request.method == 'MOVE':
123 return object_move(request, v_account, v_container, v_object)
124 elif request.method == 'POST':
125 if request.META.get('CONTENT_TYPE', '').startswith('multipart/form-data'):
126 return object_write_form(request, v_account, v_container, v_object)
127 return object_update(request, v_account, v_container, v_object)
128 elif request.method == 'DELETE':
129 return object_delete(request, v_account, v_container, v_object)
131 return method_not_allowed(request)
133 @api_method('GET', user_required=False)
134 def authenticate(request):
135 # Normal Response Codes: 204
136 # Error Response Codes: internalServerError (500),
140 x_auth_user = request.META.get('HTTP_X_AUTH_USER')
141 x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
142 if not x_auth_user or not x_auth_key:
143 raise BadRequest('Missing X-Auth-User or X-Auth-Key header')
144 response = HttpResponse(status=204)
146 uri = request.build_absolute_uri()
148 uri = uri[:uri.find('?')]
150 response['X-Auth-Token'] = x_auth_key
151 response['X-Storage-Url'] = uri + ('' if uri.endswith('/') else '/') + x_auth_user
154 @api_method('GET', format_allowed=True)
155 def account_list(request):
156 # Normal Response Codes: 200, 204
157 # Error Response Codes: internalServerError (500),
160 response = HttpResponse()
162 marker = request.GET.get('marker')
163 limit = get_int_parameter(request.GET.get('limit'))
167 accounts = request.backend.list_accounts(request.user_uniq, marker, limit)
169 if request.serialization == 'text':
170 if len(accounts) == 0:
171 # The cloudfiles python bindings expect 200 if json/xml.
172 response.status_code = 204
174 response.status_code = 200
175 response.content = '\n'.join(accounts) + '\n'
180 if x == request.user_uniq:
183 meta = request.backend.get_account_meta(request.user_uniq, x, 'pithos', include_user_defined=False)
184 groups = request.backend.get_account_groups(request.user_uniq, x)
185 except NotAllowedError:
186 raise Forbidden('Not allowed')
188 rename_meta_key(meta, 'modified', 'last_modified')
189 rename_meta_key(meta, 'until_timestamp', 'x_account_until_timestamp')
191 meta['X-Account-Group'] = printable_header_dict(dict([(k, ','.join(v)) for k, v in groups.iteritems()]))
192 account_meta.append(printable_header_dict(meta))
193 if request.serialization == 'xml':
194 data = render_to_string('accounts.xml', {'accounts': account_meta})
195 elif request.serialization == 'json':
196 data = json.dumps(account_meta)
197 response.status_code = 200
198 response.content = data
202 def account_meta(request, v_account):
203 # Normal Response Codes: 204
204 # Error Response Codes: internalServerError (500),
208 until = get_int_parameter(request.GET.get('until'))
210 meta = request.backend.get_account_meta(request.user_uniq, v_account, 'pithos', until)
211 groups = request.backend.get_account_groups(request.user_uniq, v_account)
212 policy = request.backend.get_account_policy(request.user_uniq, v_account)
213 except NotAllowedError:
214 raise Forbidden('Not allowed')
216 validate_modification_preconditions(request, meta)
218 response = HttpResponse(status=204)
219 put_account_headers(response, meta, groups, policy)
223 def account_update(request, v_account):
224 # Normal Response Codes: 202
225 # Error Response Codes: internalServerError (500),
229 meta, groups = get_account_headers(request)
231 if 'update' in request.GET:
235 request.backend.update_account_groups(request.user_uniq, v_account,
237 except NotAllowedError:
238 raise Forbidden('Not allowed')
240 raise BadRequest('Invalid groups header')
243 request.backend.update_account_meta(request.user_uniq, v_account,
244 'pithos', meta, replace)
245 except NotAllowedError:
246 raise Forbidden('Not allowed')
247 return HttpResponse(status=202)
249 @api_method('GET', format_allowed=True)
250 def container_list(request, v_account):
251 # Normal Response Codes: 200, 204
252 # Error Response Codes: internalServerError (500),
253 # itemNotFound (404),
257 until = get_int_parameter(request.GET.get('until'))
259 meta = request.backend.get_account_meta(request.user_uniq, v_account, 'pithos', until)
260 groups = request.backend.get_account_groups(request.user_uniq, v_account)
261 policy = request.backend.get_account_policy(request.user_uniq, v_account)
262 except NotAllowedError:
263 raise Forbidden('Not allowed')
265 validate_modification_preconditions(request, meta)
267 response = HttpResponse()
268 put_account_headers(response, meta, groups, policy)
270 marker = request.GET.get('marker')
271 limit = get_int_parameter(request.GET.get('limit'))
276 if 'shared' in request.GET:
280 containers = request.backend.list_containers(request.user_uniq, v_account,
281 marker, limit, shared, until)
282 except NotAllowedError:
283 raise Forbidden('Not allowed')
287 if request.serialization == 'text':
288 if len(containers) == 0:
289 # The cloudfiles python bindings expect 200 if json/xml.
290 response.status_code = 204
292 response.status_code = 200
293 response.content = '\n'.join(containers) + '\n'
299 meta = request.backend.get_container_meta(request.user_uniq, v_account,
300 x, 'pithos', until, include_user_defined=False)
301 policy = request.backend.get_container_policy(request.user_uniq,
303 except NotAllowedError:
304 raise Forbidden('Not allowed')
308 rename_meta_key(meta, 'modified', 'last_modified')
309 rename_meta_key(meta, 'until_timestamp', 'x_container_until_timestamp')
311 meta['X-Container-Policy'] = printable_header_dict(dict([(k, v) for k, v in policy.iteritems()]))
312 container_meta.append(printable_header_dict(meta))
313 if request.serialization == 'xml':
314 data = render_to_string('containers.xml', {'account': v_account, 'containers': container_meta})
315 elif request.serialization == 'json':
316 data = json.dumps(container_meta)
317 response.status_code = 200
318 response.content = data
322 def container_meta(request, v_account, v_container):
323 # Normal Response Codes: 204
324 # Error Response Codes: internalServerError (500),
325 # itemNotFound (404),
329 until = get_int_parameter(request.GET.get('until'))
331 meta = request.backend.get_container_meta(request.user_uniq, v_account,
332 v_container, 'pithos', until)
333 meta['object_meta'] = request.backend.list_container_meta(request.user_uniq,
334 v_account, v_container, 'pithos', until)
335 policy = request.backend.get_container_policy(request.user_uniq, v_account,
337 except NotAllowedError:
338 raise Forbidden('Not allowed')
340 raise ItemNotFound('Container does not exist')
342 validate_modification_preconditions(request, meta)
344 response = HttpResponse(status=204)
345 put_container_headers(request, response, meta, policy)
349 def container_create(request, v_account, v_container):
350 # Normal Response Codes: 201, 202
351 # Error Response Codes: internalServerError (500),
352 # itemNotFound (404),
356 meta, policy = get_container_headers(request)
359 request.backend.put_container(request.user_uniq, v_account, v_container, policy)
361 except NotAllowedError:
362 raise Forbidden('Not allowed')
364 raise BadRequest('Invalid policy header')
368 if ret == 202 and policy:
370 request.backend.update_container_policy(request.user_uniq, v_account,
371 v_container, policy, replace=False)
372 except NotAllowedError:
373 raise Forbidden('Not allowed')
375 raise ItemNotFound('Container does not exist')
377 raise BadRequest('Invalid policy header')
380 request.backend.update_container_meta(request.user_uniq, v_account,
381 v_container, 'pithos', meta, replace=False)
382 except NotAllowedError:
383 raise Forbidden('Not allowed')
385 raise ItemNotFound('Container does not exist')
387 return HttpResponse(status=ret)
389 @api_method('POST', format_allowed=True)
390 def container_update(request, v_account, v_container):
391 # Normal Response Codes: 202
392 # Error Response Codes: internalServerError (500),
393 # itemNotFound (404),
397 meta, policy = get_container_headers(request)
399 if 'update' in request.GET:
403 request.backend.update_container_policy(request.user_uniq, v_account,
404 v_container, policy, replace)
405 except NotAllowedError:
406 raise Forbidden('Not allowed')
408 raise ItemNotFound('Container does not exist')
410 raise BadRequest('Invalid policy header')
413 request.backend.update_container_meta(request.user_uniq, v_account,
414 v_container, 'pithos', meta, replace)
415 except NotAllowedError:
416 raise Forbidden('Not allowed')
418 raise ItemNotFound('Container does not exist')
421 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
422 content_length = get_int_parameter(request.META.get('CONTENT_LENGTH', 0))
423 content_type = request.META.get('CONTENT_TYPE')
425 if content_type and content_type == 'application/octet-stream' and content_length != 0:
426 for data in socket_read_iterator(request, content_length,
427 request.backend.block_size):
428 # TODO: Raise 408 (Request Timeout) if this takes too long.
429 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
430 hashmap.append(request.backend.put_block(data))
432 response = HttpResponse(status=202)
434 response.content = simple_list_response(request, hashmap)
437 @api_method('DELETE')
438 def container_delete(request, v_account, v_container):
439 # Normal Response Codes: 204
440 # Error Response Codes: internalServerError (500),
442 # itemNotFound (404),
446 until = get_int_parameter(request.GET.get('until'))
448 request.backend.delete_container(request.user_uniq, v_account, v_container,
450 except NotAllowedError:
451 raise Forbidden('Not allowed')
453 raise ItemNotFound('Container does not exist')
455 raise Conflict('Container is not empty')
456 return HttpResponse(status=204)
458 @api_method('GET', format_allowed=True)
459 def object_list(request, v_account, v_container):
460 # Normal Response Codes: 200, 204
461 # Error Response Codes: internalServerError (500),
462 # itemNotFound (404),
466 until = get_int_parameter(request.GET.get('until'))
468 meta = request.backend.get_container_meta(request.user_uniq, v_account,
469 v_container, 'pithos', until)
470 meta['object_meta'] = request.backend.list_container_meta(request.user_uniq,
471 v_account, v_container, 'pithos', until)
472 policy = request.backend.get_container_policy(request.user_uniq, v_account,
474 except NotAllowedError:
475 raise Forbidden('Not allowed')
477 raise ItemNotFound('Container does not exist')
479 validate_modification_preconditions(request, meta)
481 response = HttpResponse()
482 put_container_headers(request, response, meta, policy)
484 path = request.GET.get('path')
485 prefix = request.GET.get('prefix')
486 delimiter = request.GET.get('delimiter')
488 # Path overrides prefix and delimiter.
496 if prefix and delimiter and not prefix.endswith(delimiter):
497 prefix = prefix + delimiter
500 prefix = prefix.lstrip('/')
502 marker = request.GET.get('marker')
503 limit = get_int_parameter(request.GET.get('limit'))
507 keys = request.GET.get('meta')
509 keys = [smart_str(x.strip()) for x in keys.split(',') if x.strip() != '']
510 included, excluded, opers = parse_filters(keys)
512 keys += [format_header_key('X-Object-Meta-' + x) for x in included]
513 keys += ['!'+format_header_key('X-Object-Meta-' + x) for x in excluded]
514 keys += ['%s%s%s' % (format_header_key('X-Object-Meta-' + k), o, v) for k, o, v in opers]
519 if 'shared' in request.GET:
522 if request.serialization == 'text':
524 objects = request.backend.list_objects(request.user_uniq, v_account,
525 v_container, prefix, delimiter, marker,
526 limit, virtual, 'pithos', keys, shared, until)
527 except NotAllowedError:
528 raise Forbidden('Not allowed')
530 raise ItemNotFound('Container does not exist')
532 if len(objects) == 0:
533 # The cloudfiles python bindings expect 200 if json/xml.
534 response.status_code = 204
536 response.status_code = 200
537 response.content = '\n'.join([x[0] for x in objects]) + '\n'
541 objects = request.backend.list_object_meta(request.user_uniq, v_account,
542 v_container, prefix, delimiter, marker,
543 limit, virtual, 'pithos', keys, shared, until)
544 object_permissions = {}
547 name_idx = len('/'.join((v_account, v_container, '')))
548 for x in request.backend.list_object_permissions(request.user_uniq,
549 v_account, v_container, prefix):
550 object = x[name_idx:]
551 object_permissions[object] = request.backend.get_object_permissions(
552 request.user_uniq, v_account, v_container, object)
553 for k, v in request.backend.list_object_public(request.user_uniq,
554 v_account, v_container, prefix).iteritems():
555 object_public[k[name_idx:]] = v
556 except NotAllowedError:
557 raise Forbidden('Not allowed')
559 raise ItemNotFound('Container does not exist')
564 # Virtual objects/directories.
565 object_meta.append(meta)
567 rename_meta_key(meta, 'hash', 'x_object_hash') # Will be replaced by checksum.
568 rename_meta_key(meta, 'checksum', 'hash')
569 rename_meta_key(meta, 'type', 'content_type')
570 rename_meta_key(meta, 'uuid', 'x_object_uuid')
571 if until is not None and 'modified' in meta:
572 del(meta['modified'])
574 rename_meta_key(meta, 'modified', 'last_modified')
575 rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
576 rename_meta_key(meta, 'version', 'x_object_version')
577 rename_meta_key(meta, 'version_timestamp', 'x_object_version_timestamp')
578 permissions = object_permissions.get(meta['name'], None)
580 update_sharing_meta(request, permissions, v_account, v_container, meta['name'], meta)
581 public = object_public.get(meta['name'], None)
583 update_public_meta(public, meta)
584 object_meta.append(printable_header_dict(meta))
585 if request.serialization == 'xml':
586 data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
587 elif request.serialization == 'json':
588 data = json.dumps(object_meta, default=json_encode_decimal)
589 response.status_code = 200
590 response.content = data
594 def object_meta(request, v_account, v_container, v_object):
595 # Normal Response Codes: 204
596 # Error Response Codes: internalServerError (500),
597 # itemNotFound (404),
601 version = request.GET.get('version')
603 meta = request.backend.get_object_meta(request.user_uniq, v_account,
604 v_container, v_object, 'pithos', version)
606 permissions = request.backend.get_object_permissions(request.user_uniq,
607 v_account, v_container, v_object)
608 public = request.backend.get_object_public(request.user_uniq, v_account,
609 v_container, v_object)
613 except NotAllowedError:
614 raise Forbidden('Not allowed')
616 raise ItemNotFound('Object does not exist')
618 raise ItemNotFound('Version does not exist')
620 update_manifest_meta(request, v_account, meta)
621 update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
622 update_public_meta(public, meta)
624 # Evaluate conditions.
625 validate_modification_preconditions(request, meta)
627 validate_matching_preconditions(request, meta)
629 response = HttpResponse(status=304)
630 response['ETag'] = meta['checksum']
633 response = HttpResponse(status=200)
634 put_object_headers(response, meta)
637 @api_method('GET', format_allowed=True)
638 def object_read(request, v_account, v_container, v_object):
639 # Normal Response Codes: 200, 206
640 # Error Response Codes: internalServerError (500),
641 # rangeNotSatisfiable (416),
642 # preconditionFailed (412),
643 # itemNotFound (404),
648 version = request.GET.get('version')
650 # Reply with the version list. Do this first, as the object may be deleted.
651 if version == 'list':
652 if request.serialization == 'text':
653 raise BadRequest('No format specified for version list.')
656 v = request.backend.list_versions(request.user_uniq, v_account,
657 v_container, v_object)
658 except NotAllowedError:
659 raise Forbidden('Not allowed')
661 if request.serialization == 'xml':
662 d['object'] = v_object
663 data = render_to_string('versions.xml', d)
664 elif request.serialization == 'json':
665 data = json.dumps(d, default=json_encode_decimal)
667 response = HttpResponse(data, status=200)
668 response['Content-Length'] = len(data)
672 meta = request.backend.get_object_meta(request.user_uniq, v_account,
673 v_container, v_object, 'pithos', version)
675 permissions = request.backend.get_object_permissions(request.user_uniq,
676 v_account, v_container, v_object)
677 public = request.backend.get_object_public(request.user_uniq, v_account,
678 v_container, v_object)
682 except NotAllowedError:
683 raise Forbidden('Not allowed')
685 raise ItemNotFound('Object does not exist')
687 raise ItemNotFound('Version does not exist')
689 update_manifest_meta(request, v_account, meta)
690 update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
691 update_public_meta(public, meta)
693 # Evaluate conditions.
694 validate_modification_preconditions(request, meta)
696 validate_matching_preconditions(request, meta)
698 response = HttpResponse(status=304)
699 response['ETag'] = meta['checksum']
702 hashmap_reply = False
703 if 'hashmap' in request.GET and request.serialization != 'text':
708 if 'X-Object-Manifest' in meta and not hashmap_reply:
710 src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
711 objects = request.backend.list_objects(request.user_uniq, v_account,
712 src_container, prefix=src_name, virtual=False)
713 except NotAllowedError:
714 raise Forbidden('Not allowed')
716 raise BadRequest('Invalid X-Object-Manifest header')
718 raise ItemNotFound('Container does not exist')
722 s, h = request.backend.get_object_hashmap(request.user_uniq,
723 v_account, src_container, x[0], x[1])
726 except NotAllowedError:
727 raise Forbidden('Not allowed')
729 raise ItemNotFound('Object does not exist')
731 raise ItemNotFound('Version does not exist')
734 s, h = request.backend.get_object_hashmap(request.user_uniq, v_account,
735 v_container, v_object, version)
738 except NotAllowedError:
739 raise Forbidden('Not allowed')
741 raise ItemNotFound('Object does not exist')
743 raise ItemNotFound('Version does not exist')
745 # Reply with the hashmap.
748 hashmap = sum(hashmaps, [])
750 'block_size': request.backend.block_size,
751 'block_hash': request.backend.hash_algorithm,
754 if request.serialization == 'xml':
755 d['object'] = v_object
756 data = render_to_string('hashes.xml', d)
757 elif request.serialization == 'json':
760 response = HttpResponse(data, status=200)
761 put_object_headers(response, meta)
762 response['Content-Length'] = len(data)
765 request.serialization = 'text' # Unset.
766 return object_data_response(request, sizes, hashmaps, meta)
768 @api_method('PUT', format_allowed=True)
769 def object_write(request, v_account, v_container, v_object):
770 # Normal Response Codes: 201
771 # Error Response Codes: internalServerError (500),
772 # unprocessableEntity (422),
773 # lengthRequired (411),
775 # itemNotFound (404),
779 # Evaluate conditions.
780 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
782 meta = request.backend.get_object_meta(request.user_uniq, v_account,
783 v_container, v_object, 'pithos')
784 except NotAllowedError:
785 raise Forbidden('Not allowed')
788 validate_matching_preconditions(request, meta)
790 copy_from = request.META.get('HTTP_X_COPY_FROM')
791 move_from = request.META.get('HTTP_X_MOVE_FROM')
792 if copy_from or move_from:
793 content_length = get_content_length(request) # Required by the API.
795 src_account = request.META.get('HTTP_X_SOURCE_ACCOUNT')
797 src_account = request.user_uniq
800 src_container, src_name = split_container_object_string(move_from)
802 raise BadRequest('Invalid X-Move-From header')
803 version_id = copy_or_move_object(request, src_account, src_container, src_name,
804 v_account, v_container, v_object, move=True)
807 src_container, src_name = split_container_object_string(copy_from)
809 raise BadRequest('Invalid X-Copy-From header')
810 version_id = copy_or_move_object(request, src_account, src_container, src_name,
811 v_account, v_container, v_object, move=False)
812 response = HttpResponse(status=201)
813 response['X-Object-Version'] = version_id
816 content_type, meta, permissions, public = get_object_headers(request)
818 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
819 content_length = get_content_length(request)
820 # Should be BadRequest, but API says otherwise.
821 if content_type is None:
822 raise LengthRequired('Missing Content-Type header')
824 if 'hashmap' in request.GET:
825 if request.serialization not in ('json', 'xml'):
826 raise BadRequest('Invalid hashmap format')
829 for block in socket_read_iterator(request, content_length,
830 request.backend.block_size):
831 data = '%s%s' % (data, block)
833 if request.serialization == 'json':
835 if not hasattr(d, '__getitem__'):
836 raise BadRequest('Invalid data formating')
838 hashmap = d['hashes']
839 size = int(d['bytes'])
841 raise BadRequest('Invalid data formatting')
842 elif request.serialization == 'xml':
844 xml = minidom.parseString(data)
845 obj = xml.getElementsByTagName('object')[0]
846 size = int(obj.attributes['bytes'].value)
848 hashes = xml.getElementsByTagName('hash')
851 hashmap.append(hash.firstChild.data)
853 raise BadRequest('Invalid data formatting')
855 checksum = '' # Do not set to None (will copy previous value).
860 for data in socket_read_iterator(request, content_length,
861 request.backend.block_size):
862 # TODO: Raise 408 (Request Timeout) if this takes too long.
863 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
865 hashmap.append(request.backend.put_block(data))
868 checksum = md5.hexdigest().lower()
869 etag = request.META.get('HTTP_ETAG')
870 if etag and parse_etags(etag)[0].lower() != checksum:
871 raise UnprocessableEntity('Object ETag does not match')
874 version_id = request.backend.update_object_hashmap(request.user_uniq,
875 v_account, v_container, v_object, size, content_type,
876 hashmap, checksum, 'pithos', meta, True, permissions)
877 except NotAllowedError:
878 raise Forbidden('Not allowed')
879 except IndexError, e:
880 raise Conflict(simple_list_response(request, e.data))
882 raise ItemNotFound('Container does not exist')
884 raise BadRequest('Invalid sharing header')
886 raise RequestEntityTooLarge('Quota exceeded')
887 if not checksum and UPDATE_MD5:
888 # Update the MD5 after the hashmap, as there may be missing hashes.
889 checksum = hashmap_md5(request.backend, hashmap, size)
891 request.backend.update_object_checksum(request.user_uniq,
892 v_account, v_container, v_object, version_id, checksum)
893 except NotAllowedError:
894 raise Forbidden('Not allowed')
895 if public is not None:
897 request.backend.update_object_public(request.user_uniq, v_account,
898 v_container, v_object, public)
899 except NotAllowedError:
900 raise Forbidden('Not allowed')
902 raise ItemNotFound('Object does not exist')
904 response = HttpResponse(status=201)
906 response['ETag'] = checksum
907 response['X-Object-Version'] = version_id
911 def object_write_form(request, v_account, v_container, v_object):
912 # Normal Response Codes: 201
913 # Error Response Codes: internalServerError (500),
914 # itemNotFound (404),
918 request.upload_handlers = [SaveToBackendHandler(request)]
919 if not request.FILES.has_key('X-Object-Data'):
920 raise BadRequest('Missing X-Object-Data field')
921 file = request.FILES['X-Object-Data']
925 version_id = request.backend.update_object_hashmap(request.user_uniq,
926 v_account, v_container, v_object, file.size, file.content_type,
927 file.hashmap, checksum, 'pithos', {}, True)
928 except NotAllowedError:
929 raise Forbidden('Not allowed')
931 raise ItemNotFound('Container does not exist')
933 raise RequestEntityTooLarge('Quota exceeded')
935 response = HttpResponse(status=201)
936 response['ETag'] = checksum
937 response['X-Object-Version'] = version_id
938 response.content = checksum
941 @api_method('COPY', format_allowed=True)
942 def object_copy(request, v_account, v_container, v_object):
943 # Normal Response Codes: 201
944 # Error Response Codes: internalServerError (500),
945 # itemNotFound (404),
949 dest_account = request.META.get('HTTP_DESTINATION_ACCOUNT')
951 dest_account = request.user_uniq
952 dest_path = request.META.get('HTTP_DESTINATION')
954 raise BadRequest('Missing Destination header')
956 dest_container, dest_name = split_container_object_string(dest_path)
958 raise BadRequest('Invalid Destination header')
960 # Evaluate conditions.
961 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
962 src_version = request.META.get('HTTP_X_SOURCE_VERSION')
964 meta = request.backend.get_object_meta(request.user_uniq, v_account,
965 v_container, v_object, 'pithos', src_version)
966 except NotAllowedError:
967 raise Forbidden('Not allowed')
968 except (NameError, IndexError):
969 raise ItemNotFound('Container or object does not exist')
970 validate_matching_preconditions(request, meta)
972 version_id = copy_or_move_object(request, v_account, v_container, v_object,
973 dest_account, dest_container, dest_name, move=False)
974 response = HttpResponse(status=201)
975 response['X-Object-Version'] = version_id
978 @api_method('MOVE', format_allowed=True)
979 def object_move(request, v_account, v_container, v_object):
980 # Normal Response Codes: 201
981 # Error Response Codes: internalServerError (500),
982 # itemNotFound (404),
986 dest_account = request.META.get('HTTP_DESTINATION_ACCOUNT')
988 dest_account = request.user_uniq
989 dest_path = request.META.get('HTTP_DESTINATION')
991 raise BadRequest('Missing Destination header')
993 dest_container, dest_name = split_container_object_string(dest_path)
995 raise BadRequest('Invalid Destination header')
997 # Evaluate conditions.
998 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
1000 meta = request.backend.get_object_meta(request.user_uniq, v_account,
1001 v_container, v_object, 'pithos')
1002 except NotAllowedError:
1003 raise Forbidden('Not allowed')
1005 raise ItemNotFound('Container or object does not exist')
1006 validate_matching_preconditions(request, meta)
1008 version_id = copy_or_move_object(request, v_account, v_container, v_object,
1009 dest_account, dest_container, dest_name, move=True)
1010 response = HttpResponse(status=201)
1011 response['X-Object-Version'] = version_id
1014 @api_method('POST', format_allowed=True)
1015 def object_update(request, v_account, v_container, v_object):
1016 # Normal Response Codes: 202, 204
1017 # Error Response Codes: internalServerError (500),
1019 # itemNotFound (404),
1023 content_type, meta, permissions, public = get_object_headers(request)
1026 prev_meta = request.backend.get_object_meta(request.user_uniq, v_account,
1027 v_container, v_object, 'pithos')
1028 except NotAllowedError:
1029 raise Forbidden('Not allowed')
1031 raise ItemNotFound('Object does not exist')
1033 # Evaluate conditions.
1034 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
1035 validate_matching_preconditions(request, prev_meta)
1038 if 'update' in request.GET:
1041 # A Content-Type or X-Source-Object header indicates data updates.
1042 src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
1043 if (not content_type or content_type != 'application/octet-stream') and not src_object:
1044 response = HttpResponse(status=202)
1046 # Do permissions first, as it may fail easier.
1047 if permissions is not None:
1049 request.backend.update_object_permissions(request.user_uniq,
1050 v_account, v_container, v_object, permissions)
1051 except NotAllowedError:
1052 raise Forbidden('Not allowed')
1054 raise ItemNotFound('Object does not exist')
1056 raise BadRequest('Invalid sharing header')
1057 if public is not None:
1059 request.backend.update_object_public(request.user_uniq, v_account,
1060 v_container, v_object, public)
1061 except NotAllowedError:
1062 raise Forbidden('Not allowed')
1064 raise ItemNotFound('Object does not exist')
1067 version_id = request.backend.update_object_meta(request.user_uniq,
1068 v_account, v_container, v_object, 'pithos', meta, replace)
1069 except NotAllowedError:
1070 raise Forbidden('Not allowed')
1072 raise ItemNotFound('Object does not exist')
1073 response['X-Object-Version'] = version_id
1077 # Single range update. Range must be in Content-Range.
1078 # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
1079 # (with the addition that '*' is allowed for the range - will append).
1080 content_range = request.META.get('HTTP_CONTENT_RANGE')
1081 if not content_range:
1082 raise BadRequest('Missing Content-Range header')
1083 ranges = get_content_range(request)
1085 raise RangeNotSatisfiable('Invalid Content-Range header')
1088 size, hashmap = request.backend.get_object_hashmap(request.user_uniq,
1089 v_account, v_container, v_object)
1090 except NotAllowedError:
1091 raise Forbidden('Not allowed')
1093 raise ItemNotFound('Object does not exist')
1095 offset, length, total = ranges
1099 raise RangeNotSatisfiable('Supplied offset is beyond object limits')
1101 src_account = request.META.get('HTTP_X_SOURCE_ACCOUNT')
1103 src_account = request.user_uniq
1104 src_container, src_name = split_container_object_string(src_object)
1105 src_version = request.META.get('HTTP_X_SOURCE_VERSION')
1107 src_size, src_hashmap = request.backend.get_object_hashmap(request.user_uniq,
1108 src_account, src_container, src_name, src_version)
1109 except NotAllowedError:
1110 raise Forbidden('Not allowed')
1112 raise ItemNotFound('Source object does not exist')
1116 elif length > src_size:
1117 raise BadRequest('Object length is smaller than range length')
1119 # Require either a Content-Length, or 'chunked' Transfer-Encoding.
1121 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
1122 content_length = get_content_length(request)
1125 length = content_length
1127 if content_length == -1:
1128 # TODO: Get up to length bytes in chunks.
1129 length = content_length
1130 elif length != content_length:
1131 raise BadRequest('Content length does not match range length')
1132 if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
1133 raise RangeNotSatisfiable('Supplied range will change provided object limits')
1135 dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
1136 if dest_bytes is not None:
1137 dest_bytes = get_int_parameter(dest_bytes)
1138 if dest_bytes is None:
1139 raise BadRequest('Invalid X-Object-Bytes header')
1142 if offset % request.backend.block_size == 0:
1143 # Update the hashes only.
1146 bi = int(offset / request.backend.block_size)
1147 bl = min(length, request.backend.block_size)
1148 if bi < len(hashmap):
1149 if bl == request.backend.block_size:
1150 hashmap[bi] = src_hashmap[sbi]
1152 data = request.backend.get_block(src_hashmap[sbi])
1153 hashmap[bi] = request.backend.update_block(hashmap[bi],
1156 hashmap.append(src_hashmap[sbi])
1164 data += request.backend.get_block(src_hashmap[sbi])
1165 if length < request.backend.block_size:
1166 data = data[:length]
1167 bytes = put_object_block(request, hashmap, data, offset)
1174 for d in socket_read_iterator(request, length,
1175 request.backend.block_size):
1176 # TODO: Raise 408 (Request Timeout) if this takes too long.
1177 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
1179 bytes = put_object_block(request, hashmap, data, offset)
1183 put_object_block(request, hashmap, data, offset)
1187 if dest_bytes is not None and dest_bytes < size:
1189 hashmap = hashmap[:(int((size - 1) / request.backend.block_size) + 1)]
1190 checksum = hashmap_md5(request.backend, hashmap, size) if UPDATE_MD5 else ''
1192 version_id = request.backend.update_object_hashmap(request.user_uniq,
1193 v_account, v_container, v_object, size, prev_meta['type'],
1194 hashmap, checksum, 'pithos', meta, replace, permissions)
1195 except NotAllowedError:
1196 raise Forbidden('Not allowed')
1198 raise ItemNotFound('Container does not exist')
1200 raise BadRequest('Invalid sharing header')
1202 raise RequestEntityTooLarge('Quota exceeded')
1203 if public is not None:
1205 request.backend.update_object_public(request.user_uniq, v_account,
1206 v_container, v_object, public)
1207 except NotAllowedError:
1208 raise Forbidden('Not allowed')
1210 raise ItemNotFound('Object does not exist')
1212 response = HttpResponse(status=204)
1213 response['ETag'] = checksum
1214 response['X-Object-Version'] = version_id
1217 @api_method('DELETE')
1218 def object_delete(request, v_account, v_container, v_object):
1219 # Normal Response Codes: 204
1220 # Error Response Codes: internalServerError (500),
1221 # itemNotFound (404),
1225 until = get_int_parameter(request.GET.get('until'))
1227 request.backend.delete_object(request.user_uniq, v_account, v_container,
1229 except NotAllowedError:
1230 raise Forbidden('Not allowed')
1232 raise ItemNotFound('Object does not exist')
1233 return HttpResponse(status=204)
1236 def method_not_allowed(request):
1237 raise BadRequest('Method not allowed')