slight modifications to support list object versions
[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.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
43 from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, ItemNotFound, Conflict,
44     LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity)
45 from pithos.api.util import (format_meta_key, printable_meta_dict, get_account_meta,
46     put_account_meta, get_container_meta, put_container_meta, get_object_meta, put_object_meta,
47     update_manifest_meta, validate_modification_preconditions, validate_matching_preconditions,
48     split_container_object_string, copy_or_move_object, get_int_parameter, get_content_length,
49     get_content_range, raw_input_socket, socket_read_iterator, object_data_response,
50     hashmap_hash, api_method)
51 from pithos.backends import backend
52
53
54 logger = logging.getLogger(__name__)
55
56
57 def top_demux(request):
58     if request.method == 'GET':
59         return authenticate(request)
60     else:
61         return method_not_allowed(request)
62
63 def account_demux(request, v_account):
64     if request.method == 'HEAD':
65         return account_meta(request, v_account)
66     elif request.method == 'POST':
67         return account_update(request, v_account)
68     elif request.method == 'GET':
69         return container_list(request, v_account)
70     else:
71         return method_not_allowed(request)
72
73 def container_demux(request, v_account, v_container):
74     if request.method == 'HEAD':
75         return container_meta(request, v_account, v_container)
76     elif request.method == 'PUT':
77         return container_create(request, v_account, v_container)
78     elif request.method == 'POST':
79         return container_update(request, v_account, v_container)
80     elif request.method == 'DELETE':
81         return container_delete(request, v_account, v_container)
82     elif request.method == 'GET':
83         return object_list(request, v_account, v_container)
84     else:
85         return method_not_allowed(request)
86
87 def object_demux(request, v_account, v_container, v_object):
88     if request.method == 'HEAD':
89         return object_meta(request, v_account, v_container, v_object)
90     elif request.method == 'GET':
91         return object_read(request, v_account, v_container, v_object)
92     elif request.method == 'PUT':
93         return object_write(request, v_account, v_container, v_object)
94     elif request.method == 'COPY':
95         return object_copy(request, v_account, v_container, v_object)
96     elif request.method == 'MOVE':
97         return object_move(request, v_account, v_container, v_object)
98     elif request.method == 'POST':
99         return object_update(request, v_account, v_container, v_object)
100     elif request.method == 'DELETE':
101         return object_delete(request, v_account, v_container, v_object)
102     else:
103         return method_not_allowed(request)
104
105 @api_method('GET')
106 def authenticate(request):
107     # Normal Response Codes: 204
108     # Error Response Codes: serviceUnavailable (503),
109     #                       unauthorized (401),
110     #                       badRequest (400)
111     
112     x_auth_user = request.META.get('HTTP_X_AUTH_USER')
113     x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
114     if not x_auth_user or not x_auth_key:
115         raise BadRequest('Missing X-Auth-User or X-Auth-Key header')
116     response = HttpResponse(status=204)
117     response['X-Auth-Token'] = '0000'
118     response['X-Storage-Url'] = os.path.join(request.build_absolute_uri(), 'demo')
119     return response
120
121 @api_method('HEAD')
122 def account_meta(request, v_account):
123     # Normal Response Codes: 204
124     # Error Response Codes: serviceUnavailable (503),
125     #                       unauthorized (401),
126     #                       badRequest (400)
127     
128     until = get_int_parameter(request, 'until')
129     meta = backend.get_account_meta(request.user, v_account, until)
130     
131     response = HttpResponse(status=204)
132     put_account_meta(response, meta)
133     return response
134
135 @api_method('POST')
136 def account_update(request, v_account):
137     # Normal Response Codes: 202
138     # Error Response Codes: serviceUnavailable (503),
139     #                       unauthorized (401),
140     #                       badRequest (400)
141     
142     meta = get_account_meta(request)    
143     backend.update_account_meta(request.user, v_account, meta, replace=True)
144     return HttpResponse(status=202)
145
146 @api_method('GET', format_allowed=True)
147 def container_list(request, v_account):
148     # Normal Response Codes: 200, 204
149     # Error Response Codes: serviceUnavailable (503),
150     #                       itemNotFound (404),
151     #                       unauthorized (401),
152     #                       badRequest (400)
153     
154     until = get_int_parameter(request, 'until')
155     meta = backend.get_account_meta(request.user, v_account, until)
156     
157     validate_modification_preconditions(request, meta)
158     
159     response = HttpResponse()
160     put_account_meta(response, meta)
161     
162     marker = request.GET.get('marker')
163     limit = request.GET.get('limit')
164     if limit:
165         try:
166             limit = int(limit)
167             if limit <= 0:
168                 raise ValueError
169         except ValueError:
170             limit = 10000
171     
172     try:
173         containers = backend.list_containers(request.user, v_account, marker, limit, until)
174     except NameError:
175         containers = []
176     
177     if request.serialization == 'text':
178         if len(containers) == 0:
179             # The cloudfiles python bindings expect 200 if json/xml.
180             response.status_code = 204
181             return response
182         response.status_code = 200
183         response.content = '\n'.join([x[0] for x in containers]) + '\n'
184         return response
185     
186     container_meta = []
187     for x in containers:
188         if x[1] is not None:
189             try:
190                 meta = backend.get_container_meta(request.user, v_account, x[0], until)
191                 container_meta.append(printable_meta_dict(meta))
192             except NameError:
193                 pass
194     if request.serialization == 'xml':
195         data = render_to_string('containers.xml', {'account': v_account, 'containers': container_meta})
196     elif request.serialization  == 'json':
197         data = json.dumps(container_meta)
198     response.status_code = 200
199     response.content = data
200     return response
201
202 @api_method('HEAD')
203 def container_meta(request, v_account, v_container):
204     # Normal Response Codes: 204
205     # Error Response Codes: serviceUnavailable (503),
206     #                       itemNotFound (404),
207     #                       unauthorized (401),
208     #                       badRequest (400)
209     
210     until = get_int_parameter(request, 'until')
211     try:
212         meta = backend.get_container_meta(request.user, v_account, v_container, until)
213         meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
214     except NameError:
215         raise ItemNotFound('Container does not exist')
216     
217     response = HttpResponse(status=204)
218     put_container_meta(response, meta)
219     return response
220
221 @api_method('PUT')
222 def container_create(request, v_account, v_container):
223     # Normal Response Codes: 201, 202
224     # Error Response Codes: serviceUnavailable (503),
225     #                       itemNotFound (404),
226     #                       unauthorized (401),
227     #                       badRequest (400)
228     
229     meta = get_container_meta(request)
230     
231     try:
232         backend.put_container(request.user, v_account, v_container)
233         ret = 201
234     except NameError:
235         ret = 202
236     
237     if len(meta) > 0:
238         backend.update_container_meta(request.user, v_account, v_container, meta, replace=True)
239     
240     return HttpResponse(status=ret)
241
242 @api_method('POST')
243 def container_update(request, v_account, v_container):
244     # Normal Response Codes: 202
245     # Error Response Codes: serviceUnavailable (503),
246     #                       itemNotFound (404),
247     #                       unauthorized (401),
248     #                       badRequest (400)
249     
250     meta = get_container_meta(request)
251     try:
252         backend.update_container_meta(request.user, v_account, v_container, meta, replace=True)
253     except NameError:
254         raise ItemNotFound('Container does not exist')
255     return HttpResponse(status=202)
256
257 @api_method('DELETE')
258 def container_delete(request, v_account, v_container):
259     # Normal Response Codes: 204
260     # Error Response Codes: serviceUnavailable (503),
261     #                       conflict (409),
262     #                       itemNotFound (404),
263     #                       unauthorized (401),
264     #                       badRequest (400)
265     
266     try:
267         backend.delete_container(request.user, v_account, v_container)
268     except NameError:
269         raise ItemNotFound('Container does not exist')
270     except IndexError:
271         raise Conflict('Container is not empty')
272     return HttpResponse(status=204)
273
274 @api_method('GET', format_allowed=True)
275 def object_list(request, v_account, v_container):
276     # Normal Response Codes: 200, 204
277     # Error Response Codes: serviceUnavailable (503),
278     #                       itemNotFound (404),
279     #                       unauthorized (401),
280     #                       badRequest (400)
281     
282     until = get_int_parameter(request, 'until')
283     try:
284         meta = backend.get_container_meta(request.user, v_account, v_container, until)
285         meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
286     except NameError:
287         raise ItemNotFound('Container does not exist')
288     
289     validate_modification_preconditions(request, meta)
290     
291     response = HttpResponse()
292     put_container_meta(response, meta)
293     
294     path = request.GET.get('path')
295     prefix = request.GET.get('prefix')
296     delimiter = request.GET.get('delimiter')
297     
298     # Path overrides prefix and delimiter.
299     virtual = True
300     if path:
301         prefix = path
302         delimiter = '/'
303         virtual = False
304     
305     # Naming policy.
306     if prefix and delimiter:
307         prefix = prefix + delimiter
308     if not prefix:
309         prefix = ''
310     prefix = prefix.lstrip('/')
311     
312     marker = request.GET.get('marker')
313     limit = request.GET.get('limit')
314     if limit:
315         try:
316             limit = int(limit)
317             if limit <= 0:
318                 raise ValueError
319         except ValueError:
320             limit = 10000
321     
322     keys = request.GET.get('meta')
323     if keys:
324         keys = keys.split(',')
325         keys = [format_meta_key('X-Object-Meta-' + x.strip()) for x in keys if x.strip() != '']
326     else:
327         keys = []
328     
329     try:
330         objects = backend.list_objects(request.user, v_account, v_container, prefix, delimiter, marker, limit, virtual, keys, until)
331     except NameError:
332         raise ItemNotFound('Container does not exist')
333     
334     if request.serialization == 'text':
335         if len(objects) == 0:
336             # The cloudfiles python bindings expect 200 if json/xml.
337             response.status_code = 204
338             return response
339         response.status_code = 200
340         response.content = '\n'.join([x[0] for x in objects]) + '\n'
341         return response
342     
343     object_meta = []
344     for x in objects:
345         if x[1] is None:
346             # Virtual objects/directories.
347             object_meta.append({'subdir': x[0]})
348         else:
349             try:
350                 meta = backend.get_object_meta(request.user, v_account, v_container, x[0], x[1])
351                 object_meta.append(printable_meta_dict(meta))
352             except NameError:
353                 pass
354     if request.serialization == 'xml':
355         data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
356     elif request.serialization  == 'json':
357         data = json.dumps(object_meta)
358     response.status_code = 200
359     response.content = data
360     return response
361
362 @api_method('HEAD')
363 def object_meta(request, v_account, v_container, v_object):
364     # Normal Response Codes: 204
365     # Error Response Codes: serviceUnavailable (503),
366     #                       itemNotFound (404),
367     #                       unauthorized (401),
368     #                       badRequest (400)
369     
370     version = get_int_parameter(request, 'version')
371     try:
372         meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
373     except NameError:
374         raise ItemNotFound('Object does not exist')
375     except IndexError:
376         raise ItemNotFound('Version does not exist')
377     
378     update_manifest_meta(request, v_account, meta)
379     
380     response = HttpResponse(status=204)
381     put_object_meta(response, meta)
382     return response
383
384 @api_method('GET', format_allowed=True)
385 def object_read(request, v_account, v_container, v_object):
386     # Normal Response Codes: 200, 206
387     # Error Response Codes: serviceUnavailable (503),
388     #                       rangeNotSatisfiable (416),
389     #                       preconditionFailed (412),
390     #                       itemNotFound (404),
391     #                       unauthorized (401),
392     #                       badRequest (400),
393     #                       notModified (304)
394     
395     version = get_int_parameter(request, 'version')
396     if not version:
397         version = request.GET.get('version')
398     try:
399         meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
400     except NameError:
401         raise ItemNotFound('Object does not exist')
402     except IndexError:
403         raise ItemNotFound('Version does not exist')
404     
405     update_manifest_meta(request, v_account, meta)
406     
407     # Evaluate conditions.
408     validate_modification_preconditions(request, meta)
409     try:
410         validate_matching_preconditions(request, meta)
411     except NotModified:
412         response = HttpResponse(status=304)
413         response['ETag'] = meta['hash']
414         return response
415     
416     # Reply with the version list.
417     if version == 'list':
418         if request.serialization == 'text':
419             raise BadRequest('No format specified for version list.')
420         
421         d = {'versions': backend.list_versions(request.user, v_account, v_container, v_object)}
422         if request.serialization == 'xml':
423             d['object'] = v_object
424             data = render_to_string('versions.xml', d)
425         elif request.serialization  == 'json':
426             data = json.dumps(d)
427         
428         response = HttpResponse(data, status=200)
429         put_object_meta(response, meta)
430         response['Content-Length'] = len(data)
431         return response
432     
433     sizes = []
434     hashmaps = []
435     if 'X-Object-Manifest' in meta:
436         try:
437             src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
438             objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
439         except ValueError:
440             raise BadRequest('Invalid X-Object-Manifest header')
441         except NameError:
442             raise ItemNotFound('Container does not exist')
443         
444         try:
445             for x in objects:
446                 s, h = backend.get_object_hashmap(request.user, v_account, src_container, x[0], x[1])
447                 sizes.append(s)
448                 hashmaps.append(h)
449         except NameError:
450             raise ItemNotFound('Object does not exist')
451         except IndexError:
452             raise ItemNotFound('Version does not exist')
453     else:
454         try:
455             s, h = backend.get_object_hashmap(request.user, v_account, v_container, v_object, version)
456             sizes.append(s)
457             hashmaps.append(h)
458         except NameError:
459             raise ItemNotFound('Object does not exist')
460         except IndexError:
461             raise ItemNotFound('Version does not exist')
462     
463     # Reply with the hashmap.
464     if request.serialization != 'text':
465         size = sum(sizes)
466         hashmap = sum(hashmaps, [])
467         d = {'block_size': backend.block_size, 'block_hash': backend.hash_algorithm, 'bytes': size, 'hashes': hashmap}
468         if request.serialization == 'xml':
469             d['object'] = v_object
470             data = render_to_string('hashes.xml', d)
471         elif request.serialization  == 'json':
472             data = json.dumps(d)
473         
474         response = HttpResponse(data, status=200)
475         put_object_meta(response, meta)
476         response['Content-Length'] = len(data)
477         return response
478     
479     return object_data_response(request, sizes, hashmaps, meta)
480
481 @api_method('PUT')
482 def object_write(request, v_account, v_container, v_object):
483     # Normal Response Codes: 201
484     # Error Response Codes: serviceUnavailable (503),
485     #                       unprocessableEntity (422),
486     #                       lengthRequired (411),
487     #                       itemNotFound (404),
488     #                       unauthorized (401),
489     #                       badRequest (400)
490     
491     copy_from = request.META.get('HTTP_X_COPY_FROM')
492     move_from = request.META.get('HTTP_X_MOVE_FROM')
493     if copy_from or move_from:
494         # TODO: Why is this required? Copy this ammount?
495         content_length = get_content_length(request)
496         
497         if move_from:
498             try:
499                 src_container, src_name = split_container_object_string(move_from)
500             except ValueError:
501                 raise BadRequest('Invalid X-Move-From header')
502             copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=True)
503         else:
504             try:
505                 src_container, src_name = split_container_object_string(copy_from)
506             except ValueError:
507                 raise BadRequest('Invalid X-Copy-From header')
508             copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=False)
509         return HttpResponse(status=201)
510     
511     meta = get_object_meta(request)
512     content_length = -1
513     if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
514         content_length = get_content_length(request)
515     # Should be BadRequest, but API says otherwise.
516     if 'Content-Type' not in meta:
517         raise LengthRequired('Missing Content-Type header')
518     
519     md5 = hashlib.md5()
520     size = 0
521     hashmap = []
522     sock = raw_input_socket(request)
523     for data in socket_read_iterator(sock, content_length, backend.block_size):
524         # TODO: Raise 408 (Request Timeout) if this takes too long.
525         # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
526         size += len(data)
527         hashmap.append(backend.put_block(data))
528         md5.update(data)
529     
530     meta['hash'] = md5.hexdigest().lower()
531     etag = request.META.get('HTTP_ETAG')
532     if etag and parse_etags(etag)[0].lower() != meta['hash']:
533         raise UnprocessableEntity('Object ETag does not match')
534     
535     try:
536         backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True)
537     except NameError:
538         raise ItemNotFound('Container does not exist')
539     
540     response = HttpResponse(status=201)
541     response['ETag'] = meta['hash']
542     return response
543
544 @api_method('COPY')
545 def object_copy(request, v_account, v_container, v_object):
546     # Normal Response Codes: 201
547     # Error Response Codes: serviceUnavailable (503),
548     #                       itemNotFound (404),
549     #                       unauthorized (401),
550     #                       badRequest (400)
551     
552     dest_path = request.META.get('HTTP_DESTINATION')
553     if not dest_path:
554         raise BadRequest('Missing Destination header')
555     try:
556         dest_container, dest_name = split_container_object_string(dest_path)
557     except ValueError:
558         raise BadRequest('Invalid Destination header')
559     copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=False)
560     return HttpResponse(status=201)
561
562 @api_method('MOVE')
563 def object_move(request, v_account, v_container, v_object):
564     # Normal Response Codes: 201
565     # Error Response Codes: serviceUnavailable (503),
566     #                       itemNotFound (404),
567     #                       unauthorized (401),
568     #                       badRequest (400)
569     
570     dest_path = request.META.get('HTTP_DESTINATION')
571     if not dest_path:
572         raise BadRequest('Missing Destination header')
573     try:
574         dest_container, dest_name = split_container_object_string(dest_path)
575     except ValueError:
576         raise BadRequest('Invalid Destination header')
577     copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=True)
578     return HttpResponse(status=201)
579
580 @api_method('POST')
581 def object_update(request, v_account, v_container, v_object):
582     # Normal Response Codes: 202, 204
583     # Error Response Codes: serviceUnavailable (503),
584     #                       itemNotFound (404),
585     #                       unauthorized (401),
586     #                       badRequest (400)
587     
588     meta = get_object_meta(request)
589     content_type = meta.get('Content-Type')
590     if content_type:
591         del(meta['Content-Type']) # Do not allow changing the Content-Type.
592     
593     try:
594         prev_meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
595     except NameError:
596         raise ItemNotFound('Object does not exist')
597     
598     # Handle metadata changes.
599     if len(meta) != 0:
600         # Keep previous values of 'Content-Type' and 'hash'.
601         for k in ('Content-Type', 'hash'):
602             if k in prev_meta:
603                 meta[k] = prev_meta[k]
604         try:
605             backend.update_object_meta(request.user, v_account, v_container, v_object, meta, replace=True)
606         except NameError:
607             raise ItemNotFound('Object does not exist')
608     
609     # A Content-Type or Content-Range header may indicate data updates.
610     if content_type and content_type.startswith('multipart/byteranges'):
611         # TODO: Support multiple update ranges.
612         return HttpResponse(status=202)
613     # Single range update. Range must be in Content-Range.
614     # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
615     # (with the addition that '*' is allowed for the range - will append).
616     if content_type and content_type != 'application/octet-stream':
617         return HttpResponse(status=202)
618     content_range = request.META.get('HTTP_CONTENT_RANGE')
619     if not content_range:
620         return HttpResponse(status=202)
621     ranges = get_content_range(request)
622     if not ranges:
623         return HttpResponse(status=202)
624     # Require either a Content-Length, or 'chunked' Transfer-Encoding.
625     content_length = -1
626     if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
627         content_length = get_content_length(request)
628     
629     try:
630         size, hashmap = backend.get_object_hashmap(request.user, v_account, v_container, v_object)
631     except NameError:
632         raise ItemNotFound('Object does not exist')
633     
634     offset, length, total = ranges
635     if offset is None:
636         offset = size
637     if length is None or content_length == -1:
638         length = content_length # Nevermind the error.
639     elif length != content_length:
640         raise BadRequest('Content length does not match range length')
641     if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
642         raise RangeNotSatisfiable('Supplied range will change provided object limits')
643     
644     sock = raw_input_socket(request)
645     data = ''
646     for d in socket_read_iterator(sock, length, backend.block_size):
647         # TODO: Raise 408 (Request Timeout) if this takes too long.
648         # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
649         data += d
650         bi = int(offset / backend.block_size)
651         bo = offset % backend.block_size
652         bl = min(len(data), backend.block_size - bo)
653         offset += bl
654         h = backend.update_block(hashmap[bi], data[:bl], bo)
655         if bi < len(hashmap):
656             hashmap[bi] = h
657         else:
658             hashmap.append(h)
659         data = data[bl:]
660     if len(data) > 0:
661         bi = int(offset / backend.block_size)
662         offset += len(data)
663         h = backend.update_block(hashmap[bi], data)
664         if bi < len(hashmap):
665             hashmap[bi] = h
666         else:
667             hashmap.append(h)
668     
669     if offset > size:
670         size = offset
671     meta = {'hash': hashmap_hash(hashmap)} # Update ETag.
672     try:
673         backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, False)
674     except NameError:
675         raise ItemNotFound('Container does not exist')
676         
677     response = HttpResponse(status=204)
678     response['ETag'] = meta['hash']
679     return response
680
681 @api_method('DELETE')
682 def object_delete(request, v_account, v_container, v_object):
683     # Normal Response Codes: 204
684     # Error Response Codes: serviceUnavailable (503),
685     #                       itemNotFound (404),
686     #                       unauthorized (401),
687     #                       badRequest (400)
688     
689     try:
690         backend.delete_object(request.user, v_account, v_container, v_object)
691     except NameError:
692         raise ItemNotFound('Object does not exist')
693     return HttpResponse(status=204)
694
695 @api_method()
696 def method_not_allowed(request):
697     raise BadRequest('Method not allowed')