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