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