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