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