3476e571057a5acba6be74084007f2d46a508d77
[pithos] / snf-pithos-app / pithos / api / functions.py
1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
2
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10
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.
15
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.
28
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.
33
34 from xml.dom import minidom
35
36 from django.conf import settings
37 from django.http import HttpResponse
38 from django.template.loader import render_to_string
39 from django.utils import simplejson as json
40 from django.utils.http import parse_etags
41 from django.utils.encoding import smart_str
42 from django.views.decorators.csrf import csrf_exempt
43
44 from synnefo.lib.astakos import get_user
45
46 from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, Forbidden, ItemNotFound, Conflict,
47     LengthRequired, PreconditionFailed, RequestEntityTooLarge, RangeNotSatisfiable, UnprocessableEntity)
48 from pithos.api.util import (json_encode_decimal, rename_meta_key, format_header_key, printable_header_dict,
49     get_account_headers, put_account_headers, get_container_headers, put_container_headers, get_object_headers,
50     put_object_headers, update_manifest_meta, update_sharing_meta, update_public_meta,
51     validate_modification_preconditions, validate_matching_preconditions, split_container_object_string,
52     copy_or_move_object, get_int_parameter, get_content_length, get_content_range, socket_read_iterator,
53     SaveToBackendHandler, object_data_response, put_object_block, hashmap_md5, simple_list_response, api_method)
54 from pithos.api.settings import AUTHENTICATION_URL, AUTHENTICATION_USERS, COOKIE_NAME
55
56 from pithos.backends.base import NotAllowedError, QuotaError
57 from pithos.backends.filter import parse_filters
58
59 import logging
60 import hashlib
61
62
63 logger = logging.getLogger(__name__)
64
65
66 @csrf_exempt
67 def top_demux(request):
68     get_user(request, AUTHENTICATION_URL, AUTHENTICATION_USERS)
69     if request.method == 'GET':
70         if getattr(request, 'user', None) is not None:
71             return account_list(request)
72         return authenticate(request)
73     else:
74         return method_not_allowed(request)
75
76 @csrf_exempt
77 def account_demux(request, v_account):
78     get_user(request, AUTHENTICATION_URL, AUTHENTICATION_USERS)
79     if request.method == 'HEAD':
80         return account_meta(request, v_account)
81     elif request.method == 'POST':
82         return account_update(request, v_account)
83     elif request.method == 'GET':
84         return container_list(request, v_account)
85     else:
86         return method_not_allowed(request)
87
88 @csrf_exempt
89 def container_demux(request, v_account, v_container):
90     get_user(request, AUTHENTICATION_URL, AUTHENTICATION_USERS)
91     if request.method == 'HEAD':
92         return container_meta(request, v_account, v_container)
93     elif request.method == 'PUT':
94         return container_create(request, v_account, v_container)
95     elif request.method == 'POST':
96         return container_update(request, v_account, v_container)
97     elif request.method == 'DELETE':
98         return container_delete(request, v_account, v_container)
99     elif request.method == 'GET':
100         return object_list(request, v_account, v_container)
101     else:
102         return method_not_allowed(request)
103
104 @csrf_exempt
105 def object_demux(request, v_account, v_container, v_object):
106     # Helper to avoid placing the token in the URL when loading objects from a browser.
107     token = None
108     if request.method in ('HEAD', 'GET') and COOKIE_NAME in request.COOKIES:
109         cookie_value = unquote(request.COOKIES.get('COOKIE_NAME', ''))
110         if cookie_value and '|' in cookie_value:
111             token = cookie_value.split('|', 1)[1]
112     get_user(request, AUTHENTICATION_URL, AUTHENTICATION_USERS, token)
113     if request.method == 'HEAD':
114         return object_meta(request, v_account, v_container, v_object)
115     elif request.method == 'GET':
116         return object_read(request, v_account, v_container, v_object)
117     elif request.method == 'PUT':
118         return object_write(request, v_account, v_container, v_object)
119     elif request.method == 'COPY':
120         return object_copy(request, v_account, v_container, v_object)
121     elif request.method == 'MOVE':
122         return object_move(request, v_account, v_container, v_object)
123     elif request.method == 'POST':
124         if request.META.get('CONTENT_TYPE', '').startswith('multipart/form-data'):
125             return object_write_form(request, v_account, v_container, v_object)
126         return object_update(request, v_account, v_container, v_object)
127     elif request.method == 'DELETE':
128         return object_delete(request, v_account, v_container, v_object)
129     else:
130         return method_not_allowed(request)
131
132 @api_method('GET', user_required=False)
133 def authenticate(request):
134     # Normal Response Codes: 204
135     # Error Response Codes: internalServerError (500),
136     #                       forbidden (403),
137     #                       badRequest (400)
138     
139     x_auth_user = request.META.get('HTTP_X_AUTH_USER')
140     x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
141     if not x_auth_user or not x_auth_key:
142         raise BadRequest('Missing X-Auth-User or X-Auth-Key header')
143     response = HttpResponse(status=204)
144     
145     uri = request.build_absolute_uri()
146     if '?' in uri:
147         uri = uri[:uri.find('?')]
148     
149     response['X-Auth-Token'] = x_auth_key
150     response['X-Storage-Url'] = uri + ('' if uri.endswith('/') else '/') + x_auth_user
151     return response
152
153 @api_method('GET', format_allowed=True)
154 def account_list(request):
155     # Normal Response Codes: 200, 204
156     # Error Response Codes: internalServerError (500),
157     #                       badRequest (400)
158     
159     response = HttpResponse()
160     
161     marker = request.GET.get('marker')
162     limit = get_int_parameter(request.GET.get('limit'))
163     if not limit:
164         limit = 10000
165     
166     accounts = request.backend.list_accounts(request.user_uniq, marker, limit)
167     
168     if request.serialization == 'text':
169         if len(accounts) == 0:
170             # The cloudfiles python bindings expect 200 if json/xml.
171             response.status_code = 204
172             return response
173         response.status_code = 200
174         response.content = '\n'.join(accounts) + '\n'
175         return response
176     
177     account_meta = []
178     for x in accounts:
179         if x == request.user_uniq:
180             continue
181         try:
182             meta = request.backend.get_account_meta(request.user_uniq, x, 'pithos', include_user_defined=False)
183             groups = request.backend.get_account_groups(request.user_uniq, x)
184         except NotAllowedError:
185             raise Forbidden('Not allowed')
186         else:
187             rename_meta_key(meta, 'modified', 'last_modified')
188             rename_meta_key(meta, 'until_timestamp', 'x_account_until_timestamp')
189             if groups:
190                 meta['X-Account-Group'] = printable_header_dict(dict([(k, ','.join(v)) for k, v in groups.iteritems()]))
191             account_meta.append(printable_header_dict(meta))
192     if request.serialization == 'xml':
193         data = render_to_string('accounts.xml', {'accounts': account_meta})
194     elif request.serialization  == 'json':
195         data = json.dumps(account_meta)
196     response.status_code = 200
197     response.content = data
198     return response
199
200 @api_method('HEAD')
201 def account_meta(request, v_account):
202     # Normal Response Codes: 204
203     # Error Response Codes: internalServerError (500),
204     #                       forbidden (403),
205     #                       badRequest (400)
206     
207     until = get_int_parameter(request.GET.get('until'))
208     try:
209         meta = request.backend.get_account_meta(request.user_uniq, v_account, 'pithos', until)
210         groups = request.backend.get_account_groups(request.user_uniq, v_account)
211         policy = request.backend.get_account_policy(request.user_uniq, v_account)
212     except NotAllowedError:
213         raise Forbidden('Not allowed')
214     
215     validate_modification_preconditions(request, meta)
216     
217     response = HttpResponse(status=204)
218     put_account_headers(response, meta, groups, policy)
219     return response
220
221 @api_method('POST')
222 def account_update(request, v_account):
223     # Normal Response Codes: 202
224     # Error Response Codes: internalServerError (500),
225     #                       forbidden (403),
226     #                       badRequest (400)
227     
228     meta, groups = get_account_headers(request)
229     replace = True
230     if 'update' in request.GET:
231         replace = False
232     if groups:
233         try:
234             request.backend.update_account_groups(request.user_uniq, v_account,
235                                                     groups, replace)
236         except NotAllowedError:
237             raise Forbidden('Not allowed')
238         except ValueError:
239             raise BadRequest('Invalid groups header')
240     if meta or replace:
241         try:
242             request.backend.update_account_meta(request.user_uniq, v_account,
243                                                 'pithos', meta, replace)
244         except NotAllowedError:
245             raise Forbidden('Not allowed')
246     return HttpResponse(status=202)
247
248 @api_method('GET', format_allowed=True)
249 def container_list(request, v_account):
250     # Normal Response Codes: 200, 204
251     # Error Response Codes: internalServerError (500),
252     #                       itemNotFound (404),
253     #                       forbidden (403),
254     #                       badRequest (400)
255     
256     until = get_int_parameter(request.GET.get('until'))
257     try:
258         meta = request.backend.get_account_meta(request.user_uniq, v_account, 'pithos', until)
259         groups = request.backend.get_account_groups(request.user_uniq, v_account)
260         policy = request.backend.get_account_policy(request.user_uniq, v_account)
261     except NotAllowedError:
262         raise Forbidden('Not allowed')
263     
264     validate_modification_preconditions(request, meta)
265     
266     response = HttpResponse()
267     put_account_headers(response, meta, groups, policy)
268     
269     marker = request.GET.get('marker')
270     limit = get_int_parameter(request.GET.get('limit'))
271     if not limit:
272         limit = 10000
273     
274     shared = False
275     if 'shared' in request.GET:
276         shared = True
277     
278     try:
279         containers = request.backend.list_containers(request.user_uniq, v_account,
280                                                 marker, limit, shared, until)
281     except NotAllowedError:
282         raise Forbidden('Not allowed')
283     except NameError:
284         containers = []
285     
286     if request.serialization == 'text':
287         if len(containers) == 0:
288             # The cloudfiles python bindings expect 200 if json/xml.
289             response.status_code = 204
290             return response
291         response.status_code = 200
292         response.content = '\n'.join(containers) + '\n'
293         return response
294     
295     container_meta = []
296     for x in containers:
297         try:
298             meta = request.backend.get_container_meta(request.user_uniq, v_account,
299                                                         x, 'pithos', until, include_user_defined=False)
300             policy = request.backend.get_container_policy(request.user_uniq,
301                                                             v_account, x)
302         except NotAllowedError:
303             raise Forbidden('Not allowed')
304         except NameError:
305             pass
306         else:
307             rename_meta_key(meta, 'modified', 'last_modified')
308             rename_meta_key(meta, 'until_timestamp', 'x_container_until_timestamp')
309             if policy:
310                 meta['X-Container-Policy'] = printable_header_dict(dict([(k, v) for k, v in policy.iteritems()]))
311             container_meta.append(printable_header_dict(meta))
312     if request.serialization == 'xml':
313         data = render_to_string('containers.xml', {'account': v_account, 'containers': container_meta})
314     elif request.serialization  == 'json':
315         data = json.dumps(container_meta)
316     response.status_code = 200
317     response.content = data
318     return response
319
320 @api_method('HEAD')
321 def container_meta(request, v_account, v_container):
322     # Normal Response Codes: 204
323     # Error Response Codes: internalServerError (500),
324     #                       itemNotFound (404),
325     #                       forbidden (403),
326     #                       badRequest (400)
327     
328     until = get_int_parameter(request.GET.get('until'))
329     try:
330         meta = request.backend.get_container_meta(request.user_uniq, v_account,
331                                                     v_container, 'pithos', until)
332         meta['object_meta'] = request.backend.list_container_meta(request.user_uniq,
333                                                 v_account, v_container, 'pithos', until)
334         policy = request.backend.get_container_policy(request.user_uniq, v_account,
335                                                         v_container)
336     except NotAllowedError:
337         raise Forbidden('Not allowed')
338     except NameError:
339         raise ItemNotFound('Container does not exist')
340     
341     validate_modification_preconditions(request, meta)
342     
343     response = HttpResponse(status=204)
344     put_container_headers(request, response, meta, policy)
345     return response
346
347 @api_method('PUT')
348 def container_create(request, v_account, v_container):
349     # Normal Response Codes: 201, 202
350     # Error Response Codes: internalServerError (500),
351     #                       itemNotFound (404),
352     #                       forbidden (403),
353     #                       badRequest (400)
354     
355     meta, policy = get_container_headers(request)
356     
357     try:
358         request.backend.put_container(request.user_uniq, v_account, v_container, policy)
359         ret = 201
360     except NotAllowedError:
361         raise Forbidden('Not allowed')
362     except ValueError:
363         raise BadRequest('Invalid policy header')
364     except NameError:
365         ret = 202
366     
367     if ret == 202 and policy:
368         try:
369             request.backend.update_container_policy(request.user_uniq, v_account,
370                                             v_container, policy, replace=False)
371         except NotAllowedError:
372             raise Forbidden('Not allowed')
373         except NameError:
374             raise ItemNotFound('Container does not exist')
375         except ValueError:
376             raise BadRequest('Invalid policy header')
377     if meta:
378         try:
379             request.backend.update_container_meta(request.user_uniq, v_account,
380                                             v_container, 'pithos', meta, replace=False)
381         except NotAllowedError:
382             raise Forbidden('Not allowed')
383         except NameError:
384             raise ItemNotFound('Container does not exist')
385     
386     return HttpResponse(status=ret)
387
388 @api_method('POST', format_allowed=True)
389 def container_update(request, v_account, v_container):
390     # Normal Response Codes: 202
391     # Error Response Codes: internalServerError (500),
392     #                       itemNotFound (404),
393     #                       forbidden (403),
394     #                       badRequest (400)
395     
396     meta, policy = get_container_headers(request)
397     replace = True
398     if 'update' in request.GET:
399         replace = False
400     if policy:
401         try:
402             request.backend.update_container_policy(request.user_uniq, v_account,
403                                                 v_container, policy, replace)
404         except NotAllowedError:
405             raise Forbidden('Not allowed')
406         except NameError:
407             raise ItemNotFound('Container does not exist')
408         except ValueError:
409             raise BadRequest('Invalid policy header')
410     if meta or replace:
411         try:
412             request.backend.update_container_meta(request.user_uniq, v_account,
413                                                     v_container, 'pithos', meta, replace)
414         except NotAllowedError:
415             raise Forbidden('Not allowed')
416         except NameError:
417             raise ItemNotFound('Container does not exist')
418     
419     content_length = -1
420     if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
421         content_length = get_int_parameter(request.META.get('CONTENT_LENGTH', 0))
422     content_type = request.META.get('CONTENT_TYPE')
423     hashmap = []
424     if content_type and content_type == 'application/octet-stream' and content_length != 0:
425         for data in socket_read_iterator(request, content_length,
426                                             request.backend.block_size):
427             # TODO: Raise 408 (Request Timeout) if this takes too long.
428             # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
429             hashmap.append(request.backend.put_block(data))
430     
431     response = HttpResponse(status=202)
432     if hashmap:
433         response.content = simple_list_response(request, hashmap)
434     return response
435
436 @api_method('DELETE')
437 def container_delete(request, v_account, v_container):
438     # Normal Response Codes: 204
439     # Error Response Codes: internalServerError (500),
440     #                       conflict (409),
441     #                       itemNotFound (404),
442     #                       forbidden (403),
443     #                       badRequest (400)
444     
445     until = get_int_parameter(request.GET.get('until'))
446     try:
447         request.backend.delete_container(request.user_uniq, v_account, v_container,
448                                             until)
449     except NotAllowedError:
450         raise Forbidden('Not allowed')
451     except NameError:
452         raise ItemNotFound('Container does not exist')
453     except IndexError:
454         raise Conflict('Container is not empty')
455     return HttpResponse(status=204)
456
457 @api_method('GET', format_allowed=True)
458 def object_list(request, v_account, v_container):
459     # Normal Response Codes: 200, 204
460     # Error Response Codes: internalServerError (500),
461     #                       itemNotFound (404),
462     #                       forbidden (403),
463     #                       badRequest (400)
464     
465     until = get_int_parameter(request.GET.get('until'))
466     try:
467         meta = request.backend.get_container_meta(request.user_uniq, v_account,
468                                                     v_container, 'pithos', until)
469         meta['object_meta'] = request.backend.list_container_meta(request.user_uniq,
470                                                 v_account, v_container, 'pithos', until)
471         policy = request.backend.get_container_policy(request.user_uniq, v_account,
472                                                         v_container)
473     except NotAllowedError:
474         raise Forbidden('Not allowed')
475     except NameError:
476         raise ItemNotFound('Container does not exist')
477     
478     validate_modification_preconditions(request, meta)
479     
480     response = HttpResponse()
481     put_container_headers(request, response, meta, policy)
482     
483     path = request.GET.get('path')
484     prefix = request.GET.get('prefix')
485     delimiter = request.GET.get('delimiter')
486     
487     # Path overrides prefix and delimiter.
488     virtual = True
489     if path:
490         prefix = path
491         delimiter = '/'
492         virtual = False
493     
494     # Naming policy.
495     if prefix and delimiter:
496         prefix = prefix + delimiter
497     if not prefix:
498         prefix = ''
499     prefix = prefix.lstrip('/')
500     
501     marker = request.GET.get('marker')
502     limit = get_int_parameter(request.GET.get('limit'))
503     if not limit:
504         limit = 10000
505     
506     keys = request.GET.get('meta')
507     if keys:
508         keys = [smart_str(x.strip()) for x in keys.split(',') if x.strip() != '']
509         included, excluded, opers = parse_filters(keys)
510         keys = []
511         keys += [format_header_key('X-Object-Meta-' + x) for x in included]
512         keys += ['!'+format_header_key('X-Object-Meta-' + x) for x in excluded]
513         keys += ['%s%s%s' % (format_header_key('X-Object-Meta-' + k), o, v) for k, o, v in opers]
514     else:
515         keys = []
516     
517     shared = False
518     if 'shared' in request.GET:
519         shared = True
520     
521     if request.serialization == 'text':
522         try:
523             objects = request.backend.list_objects(request.user_uniq, v_account,
524                                         v_container, prefix, delimiter, marker,
525                                         limit, virtual, 'pithos', keys, shared, until)
526         except NotAllowedError:
527             raise Forbidden('Not allowed')
528         except NameError:
529             raise ItemNotFound('Container does not exist')
530         
531         if len(objects) == 0:
532             # The cloudfiles python bindings expect 200 if json/xml.
533             response.status_code = 204
534             return response
535         response.status_code = 200
536         response.content = '\n'.join([x[0] for x in objects]) + '\n'
537         return response
538     
539     try:
540         objects = request.backend.list_object_meta(request.user_uniq, v_account,
541                                     v_container, prefix, delimiter, marker,
542                                     limit, virtual, 'pithos', keys, shared, until)
543         object_permissions = {}
544         object_public = {}
545         if until is None:
546             name_idx = len('/'.join((v_account, v_container, '')))
547             for x in request.backend.list_object_permissions(request.user_uniq,
548                                     v_account, v_container, prefix):
549                 object = x[name_idx:]
550                 object_permissions[object] = request.backend.get_object_permissions(
551                                     request.user_uniq, v_account, v_container, object)
552             for k, v in request.backend.list_object_public(request.user_uniq,
553                                     v_account, v_container, prefix).iteritems():
554                 object_public[k[name_idx:]] = v
555     except NotAllowedError:
556         raise Forbidden('Not allowed')
557     except NameError:
558         raise ItemNotFound('Container does not exist')
559     
560     object_meta = []
561     for meta in objects:
562         if len(meta) == 1:
563             # Virtual objects/directories.
564             object_meta.append(meta)
565         else:
566             rename_meta_key(meta, 'hash', 'x_object_hash') # Will be replaced by checksum.
567             rename_meta_key(meta, 'checksum', 'hash')
568             rename_meta_key(meta, 'type', 'content_type')
569             rename_meta_key(meta, 'uuid', 'x_object_uuid')
570             if until is not None and 'modified' in meta:
571                 del(meta['modified'])
572             else:
573                 rename_meta_key(meta, 'modified', 'last_modified')
574             rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
575             rename_meta_key(meta, 'version', 'x_object_version')
576             rename_meta_key(meta, 'version_timestamp', 'x_object_version_timestamp')
577             permissions = object_permissions.get(meta['name'], None)
578             if permissions:
579                 update_sharing_meta(request, permissions, v_account, v_container, meta['name'], meta)
580             public = object_public.get(meta['name'], None)
581             if public:
582                 update_public_meta(public, meta)
583             object_meta.append(printable_header_dict(meta))
584     if request.serialization == 'xml':
585         data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
586     elif request.serialization  == 'json':
587         data = json.dumps(object_meta, default=json_encode_decimal)
588     response.status_code = 200
589     response.content = data
590     return response
591
592 @api_method('HEAD')
593 def object_meta(request, v_account, v_container, v_object):
594     # Normal Response Codes: 204
595     # Error Response Codes: internalServerError (500),
596     #                       itemNotFound (404),
597     #                       forbidden (403),
598     #                       badRequest (400)
599     
600     version = request.GET.get('version')
601     try:
602         meta = request.backend.get_object_meta(request.user_uniq, v_account,
603                                                 v_container, v_object, 'pithos', version)
604         if version is None:
605             permissions = request.backend.get_object_permissions(request.user_uniq,
606                                             v_account, v_container, v_object)
607             public = request.backend.get_object_public(request.user_uniq, v_account,
608                                                         v_container, v_object)
609         else:
610             permissions = None
611             public = None
612     except NotAllowedError:
613         raise Forbidden('Not allowed')
614     except NameError:
615         raise ItemNotFound('Object does not exist')
616     except IndexError:
617         raise ItemNotFound('Version does not exist')
618     
619     update_manifest_meta(request, v_account, meta)
620     update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
621     update_public_meta(public, meta)
622     
623     # Evaluate conditions.
624     validate_modification_preconditions(request, meta)
625     try:
626         validate_matching_preconditions(request, meta)
627     except NotModified:
628         response = HttpResponse(status=304)
629         response['ETag'] = meta['checksum']
630         return response
631     
632     response = HttpResponse(status=200)
633     put_object_headers(response, meta)
634     return response
635
636 @api_method('GET', format_allowed=True)
637 def object_read(request, v_account, v_container, v_object):
638     # Normal Response Codes: 200, 206
639     # Error Response Codes: internalServerError (500),
640     #                       rangeNotSatisfiable (416),
641     #                       preconditionFailed (412),
642     #                       itemNotFound (404),
643     #                       forbidden (403),
644     #                       badRequest (400),
645     #                       notModified (304)
646     
647     version = request.GET.get('version')
648     
649     # Reply with the version list. Do this first, as the object may be deleted.
650     if version == 'list':
651         if request.serialization == 'text':
652             raise BadRequest('No format specified for version list.')
653         
654         try:
655             v = request.backend.list_versions(request.user_uniq, v_account,
656                                                 v_container, v_object)
657         except NotAllowedError:
658             raise Forbidden('Not allowed')
659         d = {'versions': v}
660         if request.serialization == 'xml':
661             d['object'] = v_object
662             data = render_to_string('versions.xml', d)
663         elif request.serialization  == 'json':
664             data = json.dumps(d, default=json_encode_decimal)
665         
666         response = HttpResponse(data, status=200)
667         response['Content-Length'] = len(data)
668         return response
669     
670     try:
671         meta = request.backend.get_object_meta(request.user_uniq, v_account,
672                                                 v_container, v_object, 'pithos', version)
673         if version is None:
674             permissions = request.backend.get_object_permissions(request.user_uniq,
675                                             v_account, v_container, v_object)
676             public = request.backend.get_object_public(request.user_uniq, v_account,
677                                                         v_container, v_object)
678         else:
679             permissions = None
680             public = None
681     except NotAllowedError:
682         raise Forbidden('Not allowed')
683     except NameError:
684         raise ItemNotFound('Object does not exist')
685     except IndexError:
686         raise ItemNotFound('Version does not exist')
687     
688     update_manifest_meta(request, v_account, meta)
689     update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
690     update_public_meta(public, meta)
691     
692     # Evaluate conditions.
693     validate_modification_preconditions(request, meta)
694     try:
695         validate_matching_preconditions(request, meta)
696     except NotModified:
697         response = HttpResponse(status=304)
698         response['ETag'] = meta['checksum']
699         return response
700     
701     hashmap_reply = False
702     if 'hashmap' in request.GET and request.serialization != 'text':
703         hashmap_reply = True
704     
705     sizes = []
706     hashmaps = []
707     if 'X-Object-Manifest' in meta and not hashmap_reply:
708         try:
709             src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
710             objects = request.backend.list_objects(request.user_uniq, v_account,
711                                 src_container, prefix=src_name, virtual=False)
712         except NotAllowedError:
713             raise Forbidden('Not allowed')
714         except ValueError:
715             raise BadRequest('Invalid X-Object-Manifest header')
716         except NameError:
717             raise ItemNotFound('Container does not exist')
718         
719         try:
720             for x in objects:
721                 s, h = request.backend.get_object_hashmap(request.user_uniq,
722                                         v_account, src_container, x[0], x[1])
723                 sizes.append(s)
724                 hashmaps.append(h)
725         except NotAllowedError:
726             raise Forbidden('Not allowed')
727         except NameError:
728             raise ItemNotFound('Object does not exist')
729         except IndexError:
730             raise ItemNotFound('Version does not exist')
731     else:
732         try:
733             s, h = request.backend.get_object_hashmap(request.user_uniq, v_account,
734                                                 v_container, v_object, version)
735             sizes.append(s)
736             hashmaps.append(h)
737         except NotAllowedError:
738             raise Forbidden('Not allowed')
739         except NameError:
740             raise ItemNotFound('Object does not exist')
741         except IndexError:
742             raise ItemNotFound('Version does not exist')
743     
744     # Reply with the hashmap.
745     if hashmap_reply:
746         size = sum(sizes)
747         hashmap = sum(hashmaps, [])
748         d = {
749             'block_size': request.backend.block_size,
750             'block_hash': request.backend.hash_algorithm,
751             'bytes': size,
752             'hashes': hashmap}
753         if request.serialization == 'xml':
754             d['object'] = v_object
755             data = render_to_string('hashes.xml', d)
756         elif request.serialization  == 'json':
757             data = json.dumps(d)
758         
759         response = HttpResponse(data, status=200)
760         put_object_headers(response, meta)
761         response['Content-Length'] = len(data)
762         return response
763     
764     request.serialization = 'text' # Unset.
765     return object_data_response(request, sizes, hashmaps, meta)
766
767 @api_method('PUT', format_allowed=True)
768 def object_write(request, v_account, v_container, v_object):
769     # Normal Response Codes: 201
770     # Error Response Codes: internalServerError (500),
771     #                       unprocessableEntity (422),
772     #                       lengthRequired (411),
773     #                       conflict (409),
774     #                       itemNotFound (404),
775     #                       forbidden (403),
776     #                       badRequest (400)
777     
778     # Evaluate conditions.
779     if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
780         try:
781             meta = request.backend.get_object_meta(request.user_uniq, v_account,
782                                                         v_container, v_object, 'pithos')
783         except NotAllowedError:
784             raise Forbidden('Not allowed')
785         except NameError:
786             meta = {}
787         validate_matching_preconditions(request, meta)
788     
789     copy_from = request.META.get('HTTP_X_COPY_FROM')
790     move_from = request.META.get('HTTP_X_MOVE_FROM')
791     if copy_from or move_from:
792         content_length = get_content_length(request) # Required by the API.
793         
794         src_account = request.META.get('HTTP_X_SOURCE_ACCOUNT')
795         if not src_account:
796             src_account = request.user_uniq
797         if move_from:
798             try:
799                 src_container, src_name = split_container_object_string(move_from)
800             except ValueError:
801                 raise BadRequest('Invalid X-Move-From header')
802             version_id = copy_or_move_object(request, src_account, src_container, src_name,
803                                                 v_account, v_container, v_object, move=True)
804         else:
805             try:
806                 src_container, src_name = split_container_object_string(copy_from)
807             except ValueError:
808                 raise BadRequest('Invalid X-Copy-From header')
809             version_id = copy_or_move_object(request, src_account, src_container, src_name,
810                                                 v_account, v_container, v_object, move=False)
811         response = HttpResponse(status=201)
812         response['X-Object-Version'] = version_id
813         return response
814     
815     content_type, meta, permissions, public = get_object_headers(request)
816     content_length = -1
817     if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
818         content_length = get_content_length(request)
819     # Should be BadRequest, but API says otherwise.
820     if not content_type:
821         raise LengthRequired('Missing Content-Type header')
822     
823     if 'hashmap' in request.GET:
824         if request.serialization not in ('json', 'xml'):
825             raise BadRequest('Invalid hashmap format')
826         
827         data = ''
828         for block in socket_read_iterator(request, content_length,
829                                             request.backend.block_size):
830             data = '%s%s' % (data, block)
831         
832         if request.serialization == 'json':
833             d = json.loads(data)
834             if not hasattr(d, '__getitem__'):
835                 raise BadRequest('Invalid data formating')
836             try:
837                 hashmap = d['hashes']
838                 size = int(d['bytes'])
839             except:
840                 raise BadRequest('Invalid data formatting')
841         elif request.serialization == 'xml':
842             try:
843                 xml = minidom.parseString(data)
844                 obj = xml.getElementsByTagName('object')[0]
845                 size = int(obj.attributes['bytes'].value)
846                 
847                 hashes = xml.getElementsByTagName('hash')
848                 hashmap = []
849                 for hash in hashes:
850                     hashmap.append(hash.firstChild.data)
851             except:
852                 raise BadRequest('Invalid data formatting')
853         
854         checksum = '' # Do not set to None (will copy previous value).
855     else:
856         md5 = hashlib.md5()
857         size = 0
858         hashmap = []
859         for data in socket_read_iterator(request, content_length,
860                                             request.backend.block_size):
861             # TODO: Raise 408 (Request Timeout) if this takes too long.
862             # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
863             size += len(data)
864             hashmap.append(request.backend.put_block(data))
865             md5.update(data)
866         
867         checksum = md5.hexdigest().lower()
868         etag = request.META.get('HTTP_ETAG')
869         if etag and parse_etags(etag)[0].lower() != checksum:
870             raise UnprocessableEntity('Object ETag does not match')
871     
872     try:
873         version_id = request.backend.update_object_hashmap(request.user_uniq,
874                         v_account, v_container, v_object, size, content_type,
875                         hashmap, checksum, 'pithos', meta, True, permissions)
876     except NotAllowedError:
877         raise Forbidden('Not allowed')
878     except IndexError, e:
879         raise Conflict(simple_list_response(request, e.data))
880     except NameError:
881         raise ItemNotFound('Container does not exist')
882     except ValueError:
883         raise BadRequest('Invalid sharing header')
884     except QuotaError:
885         raise RequestEntityTooLarge('Quota exceeded')
886     if not checksum:
887         # Update the MD5 after the hashmap, as there may be missing hashes.
888         checksum = hashmap_md5(request, hashmap, size)
889         try:
890             version_id = request.backend.update_object_checksum(request.user_uniq,
891                             v_account, v_container, v_object, version_id, checksum)
892         except NotAllowedError:
893             raise Forbidden('Not allowed')
894     if public is not None:
895         try:
896             request.backend.update_object_public(request.user_uniq, v_account,
897                                                 v_container, v_object, public)
898         except NotAllowedError:
899             raise Forbidden('Not allowed')
900         except NameError:
901             raise ItemNotFound('Object does not exist')
902     
903     response = HttpResponse(status=201)
904     if checksum:
905         response['ETag'] = checksum
906     response['X-Object-Version'] = version_id
907     return response
908
909 @api_method('POST')
910 def object_write_form(request, v_account, v_container, v_object):
911     # Normal Response Codes: 201
912     # Error Response Codes: internalServerError (500),
913     #                       itemNotFound (404),
914     #                       forbidden (403),
915     #                       badRequest (400)
916     
917     request.upload_handlers = [SaveToBackendHandler(request)]
918     if not request.FILES.has_key('X-Object-Data'):
919         raise BadRequest('Missing X-Object-Data field')
920     file = request.FILES['X-Object-Data']
921     
922     checksum = file.etag
923     try:
924         version_id = request.backend.update_object_hashmap(request.user_uniq,
925                         v_account, v_container, v_object, file.size, file.content_type,
926                         file.hashmap, checksum, 'pithos', {}, True)
927     except NotAllowedError:
928         raise Forbidden('Not allowed')
929     except NameError:
930         raise ItemNotFound('Container does not exist')
931     except QuotaError:
932         raise RequestEntityTooLarge('Quota exceeded')
933     
934     response = HttpResponse(status=201)
935     response['ETag'] = checksum
936     response['X-Object-Version'] = version_id
937     response.content = checksum
938     return response
939
940 @api_method('COPY', format_allowed=True)
941 def object_copy(request, v_account, v_container, v_object):
942     # Normal Response Codes: 201
943     # Error Response Codes: internalServerError (500),
944     #                       itemNotFound (404),
945     #                       forbidden (403),
946     #                       badRequest (400)
947     
948     dest_account = request.META.get('HTTP_DESTINATION_ACCOUNT')
949     if not dest_account:
950         dest_account = request.user_uniq
951     dest_path = request.META.get('HTTP_DESTINATION')
952     if not dest_path:
953         raise BadRequest('Missing Destination header')
954     try:
955         dest_container, dest_name = split_container_object_string(dest_path)
956     except ValueError:
957         raise BadRequest('Invalid Destination header')
958     
959     # Evaluate conditions.
960     if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
961         src_version = request.META.get('HTTP_X_SOURCE_VERSION')
962         try:
963             meta = request.backend.get_object_meta(request.user_uniq, v_account,
964                                             v_container, v_object, 'pithos', src_version)
965         except NotAllowedError:
966             raise Forbidden('Not allowed')
967         except (NameError, IndexError):
968             raise ItemNotFound('Container or object does not exist')
969         validate_matching_preconditions(request, meta)
970     
971     version_id = copy_or_move_object(request, v_account, v_container, v_object,
972                                         dest_account, dest_container, dest_name, move=False)
973     response = HttpResponse(status=201)
974     response['X-Object-Version'] = version_id
975     return response
976
977 @api_method('MOVE', format_allowed=True)
978 def object_move(request, v_account, v_container, v_object):
979     # Normal Response Codes: 201
980     # Error Response Codes: internalServerError (500),
981     #                       itemNotFound (404),
982     #                       forbidden (403),
983     #                       badRequest (400)
984     
985     dest_account = request.META.get('HTTP_DESTINATION_ACCOUNT')
986     if not dest_account:
987         dest_account = request.user_uniq
988     dest_path = request.META.get('HTTP_DESTINATION')
989     if not dest_path:
990         raise BadRequest('Missing Destination header')
991     try:
992         dest_container, dest_name = split_container_object_string(dest_path)
993     except ValueError:
994         raise BadRequest('Invalid Destination header')
995     
996     # Evaluate conditions.
997     if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
998         try:
999             meta = request.backend.get_object_meta(request.user_uniq, v_account,
1000                                                     v_container, v_object, 'pithos')
1001         except NotAllowedError:
1002             raise Forbidden('Not allowed')
1003         except NameError:
1004             raise ItemNotFound('Container or object does not exist')
1005         validate_matching_preconditions(request, meta)
1006     
1007     version_id = copy_or_move_object(request, v_account, v_container, v_object,
1008                                         dest_account, dest_container, dest_name, move=True)
1009     response = HttpResponse(status=201)
1010     response['X-Object-Version'] = version_id
1011     return response
1012
1013 @api_method('POST', format_allowed=True)
1014 def object_update(request, v_account, v_container, v_object):
1015     # Normal Response Codes: 202, 204
1016     # Error Response Codes: internalServerError (500),
1017     #                       conflict (409),
1018     #                       itemNotFound (404),
1019     #                       forbidden (403),
1020     #                       badRequest (400)
1021     
1022     content_type, meta, permissions, public = get_object_headers(request)
1023     
1024     try:
1025         prev_meta = request.backend.get_object_meta(request.user_uniq, v_account,
1026                                                     v_container, v_object, 'pithos')
1027     except NotAllowedError:
1028         raise Forbidden('Not allowed')
1029     except NameError:
1030         raise ItemNotFound('Object does not exist')
1031     
1032     # Evaluate conditions.
1033     if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
1034         validate_matching_preconditions(request, prev_meta)
1035     
1036     replace = True
1037     if 'update' in request.GET:
1038         replace = False
1039     
1040     # A Content-Type or X-Source-Object header indicates data updates.
1041     src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
1042     if (not content_type or content_type != 'application/octet-stream') and not src_object:
1043         response = HttpResponse(status=202)
1044         
1045         # Do permissions first, as it may fail easier.
1046         if permissions is not None:
1047             try:
1048                 request.backend.update_object_permissions(request.user_uniq,
1049                                 v_account, v_container, v_object, permissions)
1050             except NotAllowedError:
1051                 raise Forbidden('Not allowed')
1052             except NameError:
1053                 raise ItemNotFound('Object does not exist')
1054             except ValueError:
1055                 raise BadRequest('Invalid sharing header')
1056         if public is not None:
1057             try:
1058                 request.backend.update_object_public(request.user_uniq, v_account,
1059                                                 v_container, v_object, public)
1060             except NotAllowedError:
1061                 raise Forbidden('Not allowed')
1062             except NameError:
1063                 raise ItemNotFound('Object does not exist')
1064         if meta or replace:
1065             try:
1066                 version_id = request.backend.update_object_meta(request.user_uniq,
1067                                 v_account, v_container, v_object, 'pithos', meta, replace)
1068             except NotAllowedError:
1069                 raise Forbidden('Not allowed')
1070             except NameError:
1071                 raise ItemNotFound('Object does not exist')        
1072             response['X-Object-Version'] = version_id
1073         
1074         return response
1075     
1076     # Single range update. Range must be in Content-Range.
1077     # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
1078     # (with the addition that '*' is allowed for the range - will append).
1079     content_range = request.META.get('HTTP_CONTENT_RANGE')
1080     if not content_range:
1081         raise BadRequest('Missing Content-Range header')
1082     ranges = get_content_range(request)
1083     if not ranges:
1084         raise RangeNotSatisfiable('Invalid Content-Range header')
1085     
1086     try:
1087         size, hashmap = request.backend.get_object_hashmap(request.user_uniq,
1088                                             v_account, v_container, v_object)
1089     except NotAllowedError:
1090         raise Forbidden('Not allowed')
1091     except NameError:
1092         raise ItemNotFound('Object does not exist')
1093     
1094     offset, length, total = ranges
1095     if offset is None:
1096         offset = size
1097     elif offset > size:
1098         raise RangeNotSatisfiable('Supplied offset is beyond object limits')
1099     if src_object:
1100         src_account = request.META.get('HTTP_X_SOURCE_ACCOUNT')
1101         if not src_account:
1102             src_account = request.user_uniq
1103         src_container, src_name = split_container_object_string(src_object)
1104         src_version = request.META.get('HTTP_X_SOURCE_VERSION')
1105         try:
1106             src_size, src_hashmap = request.backend.get_object_hashmap(request.user_uniq,
1107                                         src_account, src_container, src_name, src_version)
1108         except NotAllowedError:
1109             raise Forbidden('Not allowed')
1110         except NameError:
1111             raise ItemNotFound('Source object does not exist')
1112         
1113         if length is None:
1114             length = src_size
1115         elif length > src_size:
1116             raise BadRequest('Object length is smaller than range length')
1117     else:
1118         # Require either a Content-Length, or 'chunked' Transfer-Encoding.
1119         content_length = -1
1120         if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
1121             content_length = get_content_length(request)
1122         
1123         if length is None:
1124             length = content_length
1125         else:
1126             if content_length == -1:
1127                 # TODO: Get up to length bytes in chunks.
1128                 length = content_length
1129             elif length != content_length:
1130                 raise BadRequest('Content length does not match range length')
1131     if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
1132         raise RangeNotSatisfiable('Supplied range will change provided object limits')
1133     
1134     dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
1135     if dest_bytes is not None:
1136         dest_bytes = get_int_parameter(dest_bytes)
1137         if dest_bytes is None:
1138             raise BadRequest('Invalid X-Object-Bytes header')
1139     
1140     if src_object:
1141         if offset % request.backend.block_size == 0:
1142             # Update the hashes only.
1143             sbi = 0
1144             while length > 0:
1145                 bi = int(offset / request.backend.block_size)
1146                 bl = min(length, request.backend.block_size)
1147                 if bi < len(hashmap):
1148                     if bl == request.backend.block_size:
1149                         hashmap[bi] = src_hashmap[sbi]
1150                     else:
1151                         data = request.backend.get_block(src_hashmap[sbi])
1152                         hashmap[bi] = request.backend.update_block(hashmap[bi],
1153                                                                 data[:bl], 0)
1154                 else:
1155                     hashmap.append(src_hashmap[sbi])
1156                 offset += bl
1157                 length -= bl
1158                 sbi += 1
1159         else:
1160             data = ''
1161             sbi = 0
1162             while length > 0:
1163                 data += request.backend.get_block(src_hashmap[sbi])
1164                 if length < request.backend.block_size:
1165                     data = data[:length]
1166                 bytes = put_object_block(request, hashmap, data, offset)
1167                 offset += bytes
1168                 data = data[bytes:]
1169                 length -= bytes
1170                 sbi += 1
1171     else:
1172         data = ''
1173         for d in socket_read_iterator(request, length,
1174                                         request.backend.block_size):
1175             # TODO: Raise 408 (Request Timeout) if this takes too long.
1176             # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
1177             data += d
1178             bytes = put_object_block(request, hashmap, data, offset)
1179             offset += bytes
1180             data = data[bytes:]
1181         if len(data) > 0:
1182             put_object_block(request, hashmap, data, offset)
1183     
1184     if offset > size:
1185         size = offset
1186     if dest_bytes is not None and dest_bytes < size:
1187         size = dest_bytes
1188         hashmap = hashmap[:(int((size - 1) / request.backend.block_size) + 1)]
1189     checksum = hashmap_md5(request, hashmap, size)
1190     try:
1191         version_id = request.backend.update_object_hashmap(request.user_uniq,
1192                         v_account, v_container, v_object, size, prev_meta['type'],
1193                         hashmap, checksum, 'pithos', meta, replace, permissions)
1194     except NotAllowedError:
1195         raise Forbidden('Not allowed')
1196     except NameError:
1197         raise ItemNotFound('Container does not exist')
1198     except ValueError:
1199         raise BadRequest('Invalid sharing header')
1200     except QuotaError:
1201         raise RequestEntityTooLarge('Quota exceeded')
1202     if public is not None:
1203         try:
1204             request.backend.update_object_public(request.user_uniq, v_account,
1205                                                 v_container, v_object, public)
1206         except NotAllowedError:
1207             raise Forbidden('Not allowed')
1208         except NameError:
1209             raise ItemNotFound('Object does not exist')
1210     
1211     response = HttpResponse(status=204)
1212     response['ETag'] = checksum
1213     response['X-Object-Version'] = version_id
1214     return response
1215
1216 @api_method('DELETE')
1217 def object_delete(request, v_account, v_container, v_object):
1218     # Normal Response Codes: 204
1219     # Error Response Codes: internalServerError (500),
1220     #                       itemNotFound (404),
1221     #                       forbidden (403),
1222     #                       badRequest (400)
1223     
1224     until = get_int_parameter(request.GET.get('until'))
1225     try:
1226         request.backend.delete_object(request.user_uniq, v_account, v_container,
1227                                         v_object, until)
1228     except NotAllowedError:
1229         raise Forbidden('Not allowed')
1230     except NameError:
1231         raise ItemNotFound('Object does not exist')
1232     return HttpResponse(status=204)
1233
1234 @api_method()
1235 def method_not_allowed(request):
1236     raise BadRequest('Method not allowed')