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