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