Statistics
| Branch: | Tag: | Revision:

root / pithos / api / functions.py @ 83dd59c5

History | View | Annotate | Download (25 kB)

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
    validate_modification_preconditions, validate_matching_preconditions, split_container_object_string,
48
    copy_or_move_object, get_int_parameter, get_content_length, get_content_range, raw_input_socket,
49
    socket_read_iterator, object_data_response, hashmap_hash, api_method)
50
from pithos.backends import backend
51

    
52

    
53
logger = logging.getLogger(__name__)
54

    
55

    
56
def top_demux(request):
57
    if request.method == 'GET':
58
        return authenticate(request)
59
    else:
60
        return method_not_allowed(request)
61

    
62
def account_demux(request, v_account):
63
    if request.method == 'HEAD':
64
        return account_meta(request, v_account)
65
    elif request.method == 'POST':
66
        return account_update(request, v_account)
67
    elif request.method == 'GET':
68
        return container_list(request, v_account)
69
    else:
70
        return method_not_allowed(request)
71

    
72
def container_demux(request, v_account, v_container):
73
    if request.method == 'HEAD':
74
        return container_meta(request, v_account, v_container)
75
    elif request.method == 'PUT':
76
        return container_create(request, v_account, v_container)
77
    elif request.method == 'POST':
78
        return container_update(request, v_account, v_container)
79
    elif request.method == 'DELETE':
80
        return container_delete(request, v_account, v_container)
81
    elif request.method == 'GET':
82
        return object_list(request, v_account, v_container)
83
    else:
84
        return method_not_allowed(request)
85

    
86
def object_demux(request, v_account, v_container, v_object):
87
    if request.method == 'HEAD':
88
        return object_meta(request, v_account, v_container, v_object)
89
    elif request.method == 'GET':
90
        return object_read(request, v_account, v_container, v_object)
91
    elif request.method == 'PUT':
92
        return object_write(request, v_account, v_container, v_object)
93
    elif request.method == 'COPY':
94
        return object_copy(request, v_account, v_container, v_object)
95
    elif request.method == 'MOVE':
96
        return object_move(request, v_account, v_container, v_object)
97
    elif request.method == 'POST':
98
        return object_update(request, v_account, v_container, v_object)
99
    elif request.method == 'DELETE':
100
        return object_delete(request, v_account, v_container, v_object)
101
    else:
102
        return method_not_allowed(request)
103

    
104
@api_method('GET')
105
def authenticate(request):
106
    # Normal Response Codes: 204
107
    # Error Response Codes: serviceUnavailable (503),
108
    #                       unauthorized (401),
109
    #                       badRequest (400)
110
    
111
    x_auth_user = request.META.get('HTTP_X_AUTH_USER')
112
    x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
113
    if not x_auth_user or not x_auth_key:
114
        raise BadRequest('Missing X-Auth-User or X-Auth-Key header')
115
    response = HttpResponse(status=204)
116
    response['X-Auth-Token'] = '0000'
117
    response['X-Storage-Url'] = os.path.join(request.build_absolute_uri(), 'demo')
118
    return response
119

    
120
@api_method('HEAD')
121
def account_meta(request, v_account):
122
    # Normal Response Codes: 204
123
    # Error Response Codes: serviceUnavailable (503),
124
    #                       unauthorized (401),
125
    #                       badRequest (400)
126
    
127
    until = get_int_parameter(request, 'until')
128
    meta = backend.get_account_meta(request.user, v_account, until)
129
    
130
    response = HttpResponse(status=204)
131
    put_account_meta(response, meta)
132
    return response
133

    
134
@api_method('POST')
135
def account_update(request, v_account):
136
    # Normal Response Codes: 202
137
    # Error Response Codes: serviceUnavailable (503),
138
    #                       unauthorized (401),
139
    #                       badRequest (400)
140
    
141
    meta = get_account_meta(request)    
142
    backend.update_account_meta(request.user, v_account, meta, replace=True)
143
    return HttpResponse(status=202)
144

    
145
@api_method('GET', format_allowed=True)
146
def container_list(request, v_account):
147
    # Normal Response Codes: 200, 204
148
    # Error Response Codes: serviceUnavailable (503),
149
    #                       itemNotFound (404),
150
    #                       unauthorized (401),
151
    #                       badRequest (400)
152
    
153
    until = get_int_parameter(request, 'until')
154
    meta = backend.get_account_meta(request.user, v_account, until)
155
    
156
    validate_modification_preconditions(request, meta)
157
    
158
    response = HttpResponse()
159
    put_account_meta(response, meta)
160
    
161
    marker = request.GET.get('marker')
162
    limit = request.GET.get('limit')
163
    if limit:
164
        try:
165
            limit = int(limit)
166
            if limit <= 0:
167
                raise ValueError
168
        except ValueError:
169
            limit = 10000
170
    
171
    try:
172
        containers = backend.list_containers(request.user, v_account, marker, limit, until)
173
    except NameError:
174
        containers = []
175
    
176
    if request.serialization == 'text':
177
        if len(containers) == 0:
178
            # The cloudfiles python bindings expect 200 if json/xml.
179
            response.status_code = 204
180
            return response
181
        response.status_code = 200
182
        response.content = '\n'.join([x[0] for x in containers]) + '\n'
183
        return response
184
    
185
    container_meta = []
186
    for x in containers:
187
        if x[1] is not None:
188
            try:
189
                meta = backend.get_container_meta(request.user, v_account, x[0], until)
190
                container_meta.append(printable_meta_dict(meta))
191
            except NameError:
192
                pass
193
    if request.serialization == 'xml':
194
        data = render_to_string('containers.xml', {'account': v_account, 'containers': container_meta})
195
    elif request.serialization  == 'json':
196
        data = json.dumps(container_meta)
197
    response.status_code = 200
198
    response.content = data
199
    return response
200

    
201
@api_method('HEAD')
202
def container_meta(request, v_account, v_container):
203
    # Normal Response Codes: 204
204
    # Error Response Codes: serviceUnavailable (503),
205
    #                       itemNotFound (404),
206
    #                       unauthorized (401),
207
    #                       badRequest (400)
208
    
209
    until = get_int_parameter(request, 'until')
210
    try:
211
        meta = backend.get_container_meta(request.user, v_account, v_container, until)
212
        meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
213
    except NameError:
214
        raise ItemNotFound('Container does not exist')
215
    
216
    response = HttpResponse(status=204)
217
    put_container_meta(response, meta)
218
    return response
219

    
220
@api_method('PUT')
221
def container_create(request, v_account, v_container):
222
    # Normal Response Codes: 201, 202
223
    # Error Response Codes: serviceUnavailable (503),
224
    #                       itemNotFound (404),
225
    #                       unauthorized (401),
226
    #                       badRequest (400)
227
    
228
    meta = get_container_meta(request)
229
    
230
    try:
231
        backend.put_container(request.user, v_account, v_container)
232
        ret = 201
233
    except NameError:
234
        ret = 202
235
    
236
    if len(meta) > 0:
237
        backend.update_container_meta(request.user, v_account, v_container, meta, replace=True)
238
    
239
    return HttpResponse(status=ret)
240

    
241
@api_method('POST')
242
def container_update(request, v_account, v_container):
243
    # Normal Response Codes: 202
244
    # Error Response Codes: serviceUnavailable (503),
245
    #                       itemNotFound (404),
246
    #                       unauthorized (401),
247
    #                       badRequest (400)
248
    
249
    meta = get_container_meta(request)
250
    try:
251
        backend.update_container_meta(request.user, v_account, v_container, meta, replace=True)
252
    except NameError:
253
        raise ItemNotFound('Container does not exist')
254
    return HttpResponse(status=202)
255

    
256
@api_method('DELETE')
257
def container_delete(request, v_account, v_container):
258
    # Normal Response Codes: 204
259
    # Error Response Codes: serviceUnavailable (503),
260
    #                       conflict (409),
261
    #                       itemNotFound (404),
262
    #                       unauthorized (401),
263
    #                       badRequest (400)
264
    
265
    try:
266
        backend.delete_container(request.user, v_account, v_container)
267
    except NameError:
268
        raise ItemNotFound('Container does not exist')
269
    except IndexError:
270
        raise Conflict('Container is not empty')
271
    return HttpResponse(status=204)
272

    
273
@api_method('GET', format_allowed=True)
274
def object_list(request, v_account, v_container):
275
    # Normal Response Codes: 200, 204
276
    # Error Response Codes: serviceUnavailable (503),
277
    #                       itemNotFound (404),
278
    #                       unauthorized (401),
279
    #                       badRequest (400)
280
    
281
    until = get_int_parameter(request, 'until')
282
    try:
283
        meta = backend.get_container_meta(request.user, v_account, v_container, until)
284
        meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
285
    except NameError:
286
        raise ItemNotFound('Container does not exist')
287
    
288
    validate_modification_preconditions(request, meta)
289
    
290
    response = HttpResponse()
291
    put_container_meta(response, meta)
292
    
293
    path = request.GET.get('path')
294
    prefix = request.GET.get('prefix')
295
    delimiter = request.GET.get('delimiter')
296
    
297
    # Path overrides prefix and delimiter.
298
    virtual = True
299
    if path:
300
        prefix = path
301
        delimiter = '/'
302
        virtual = False
303
    
304
    # Naming policy.
305
    if prefix and delimiter:
306
        prefix = prefix + delimiter
307
    if not prefix:
308
        prefix = ''
309
    prefix = prefix.lstrip('/')
310
    
311
    marker = request.GET.get('marker')
312
    limit = request.GET.get('limit')
313
    if limit:
314
        try:
315
            limit = int(limit)
316
            if limit <= 0:
317
                raise ValueError
318
        except ValueError:
319
            limit = 10000
320
    
321
    keys = request.GET.get('meta')
322
    if keys:
323
        keys = keys.split(',')
324
        keys = [format_meta_key('X-Object-Meta-' + x.strip()) for x in keys if x.strip() != '']
325
    else:
326
        keys = []
327
    
328
    try:
329
        objects = backend.list_objects(request.user, v_account, v_container, prefix, delimiter, marker, limit, virtual, keys, until)
330
    except NameError:
331
        raise ItemNotFound('Container does not exist')
332
    
333
    if request.serialization == 'text':
334
        if len(objects) == 0:
335
            # The cloudfiles python bindings expect 200 if json/xml.
336
            response.status_code = 204
337
            return response
338
        response.status_code = 200
339
        response.content = '\n'.join([x[0] for x in objects]) + '\n'
340
        return response
341
    
342
    object_meta = []
343
    for x in objects:
344
        if x[1] is None:
345
            # Virtual objects/directories.
346
            object_meta.append({'subdir': x[0]})
347
        else:
348
            try:
349
                meta = backend.get_object_meta(request.user, v_account, v_container, x[0], x[1])
350
                object_meta.append(printable_meta_dict(meta))
351
            except NameError:
352
                pass
353
    if request.serialization == 'xml':
354
        data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
355
    elif request.serialization  == 'json':
356
        data = json.dumps(object_meta)
357
    response.status_code = 200
358
    response.content = data
359
    return response
360

    
361
@api_method('HEAD')
362
def object_meta(request, v_account, v_container, v_object):
363
    # Normal Response Codes: 204
364
    # Error Response Codes: serviceUnavailable (503),
365
    #                       itemNotFound (404),
366
    #                       unauthorized (401),
367
    #                       badRequest (400)
368
    
369
    version = get_int_parameter(request, 'version')
370
    try:
371
        meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
372
    except NameError:
373
        raise ItemNotFound('Object does not exist')
374
    except IndexError:
375
        raise ItemNotFound('Version does not exist')
376
    
377
    response = HttpResponse(status=204)
378
    put_object_meta(response, meta)
379
    return response
380

    
381
@api_method('GET', format_allowed=True)
382
def object_read(request, v_account, v_container, v_object):
383
    # Normal Response Codes: 200, 206
384
    # Error Response Codes: serviceUnavailable (503),
385
    #                       rangeNotSatisfiable (416),
386
    #                       preconditionFailed (412),
387
    #                       itemNotFound (404),
388
    #                       unauthorized (401),
389
    #                       badRequest (400),
390
    #                       notModified (304)
391
    
392
    version = get_int_parameter(request, 'version')
393
    try:
394
        meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
395
    except NameError:
396
        raise ItemNotFound('Object does not exist')
397
    except IndexError:
398
        raise ItemNotFound('Version does not exist')
399
    
400
    # Evaluate conditions.
401
    validate_modification_preconditions(request, meta)
402
    try:
403
        validate_matching_preconditions(request, meta)
404
    except NotModified:
405
        response = HttpResponse(status=304)
406
        response['ETag'] = meta['hash']
407
        return response
408
    
409
    # Reply with the version list.
410
    if version == 'list':
411
        if request.serialization == 'text':
412
            raise BadRequest('No format specified for version list.')
413
        
414
        d = {'versions': backend.list_versions(request.user, v_account, v_container, v_object)}
415
        if request.serialization == 'xml':
416
            d['object'] = v_object
417
            data = render_to_string('versions.xml', d)
418
        elif request.serialization  == 'json':
419
            data = json.dumps(d)
420
        
421
        response = HttpResponse(data, status=200)
422
        put_object_meta(response, meta)
423
        response['Content-Length'] = len(data)
424
        return response
425
    
426
    try:
427
        size, hashmap = backend.get_object_hashmap(request.user, v_account, v_container, v_object, version)
428
    except NameError:
429
        raise ItemNotFound('Object does not exist')
430
    except IndexError:
431
        raise ItemNotFound('Version does not exist')
432
    
433
    # Reply with the hashmap.
434
    if request.serialization != 'text':
435
        d = {'block_size': backend.block_size, 'block_hash': backend.hash_algorithm, 'bytes': size, 'hashes': hashmap}
436
        if request.serialization == 'xml':
437
            d['object'] = v_object
438
            data = render_to_string('hashes.xml', d)
439
        elif request.serialization  == 'json':
440
            data = json.dumps(d)
441
        
442
        response = HttpResponse(data, status=200)
443
        put_object_meta(response, meta)
444
        response['Content-Length'] = len(data)
445
        return response
446
    
447
    return object_data_response(request, size, hashmap, meta)
448

    
449
@api_method('PUT')
450
def object_write(request, v_account, v_container, v_object):
451
    # Normal Response Codes: 201
452
    # Error Response Codes: serviceUnavailable (503),
453
    #                       unprocessableEntity (422),
454
    #                       lengthRequired (411),
455
    #                       itemNotFound (404),
456
    #                       unauthorized (401),
457
    #                       badRequest (400)
458
    
459
    copy_from = request.META.get('HTTP_X_COPY_FROM')
460
    move_from = request.META.get('HTTP_X_MOVE_FROM')
461
    if copy_from or move_from:
462
        # TODO: Why is this required? Copy this ammount?
463
        content_length = get_content_length(request)
464
        
465
        if move_from:
466
            try:
467
                src_container, src_name = split_container_object_string(move_from)
468
            except ValueError:
469
                raise BadRequest('Invalid X-Move-From header')
470
            copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=True)
471
        else:
472
            try:
473
                src_container, src_name = split_container_object_string(copy_from)
474
            except ValueError:
475
                raise BadRequest('Invalid X-Copy-From header')
476
            copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=False)
477
        return HttpResponse(status=201)
478
    
479
    meta = get_object_meta(request)
480
    content_length = -1
481
    if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
482
        content_length = get_content_length(request)
483
    # Should be BadRequest, but API says otherwise.
484
    if 'Content-Type' not in meta:
485
        raise LengthRequired('Missing Content-Type header')
486
    
487
    md5 = hashlib.md5()
488
    size = 0
489
    hashmap = []
490
    sock = raw_input_socket(request)
491
    for data in socket_read_iterator(sock, content_length, backend.block_size):
492
        # TODO: Raise 408 (Request Timeout) if this takes too long.
493
        # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
494
        size += len(data)
495
        hashmap.append(backend.put_block(data))
496
        md5.update(data)
497
    
498
    meta['hash'] = md5.hexdigest().lower()
499
    etag = request.META.get('HTTP_ETAG')
500
    if etag and parse_etags(etag)[0].lower() != meta['hash']:
501
        raise UnprocessableEntity('Object ETag does not match')
502
    
503
    try:
504
        backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True)
505
    except NameError:
506
        raise ItemNotFound('Container does not exist')
507
    
508
    response = HttpResponse(status=201)
509
    response['ETag'] = meta['hash']
510
    return response
511

    
512
@api_method('COPY')
513
def object_copy(request, v_account, v_container, v_object):
514
    # Normal Response Codes: 201
515
    # Error Response Codes: serviceUnavailable (503),
516
    #                       itemNotFound (404),
517
    #                       unauthorized (401),
518
    #                       badRequest (400)
519
    
520
    dest_path = request.META.get('HTTP_DESTINATION')
521
    if not dest_path:
522
        raise BadRequest('Missing Destination header')
523
    try:
524
        dest_container, dest_name = split_container_object_string(dest_path)
525
    except ValueError:
526
        raise BadRequest('Invalid Destination header')
527
    copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=False)
528
    return HttpResponse(status=201)
529

    
530
@api_method('MOVE')
531
def object_move(request, v_account, v_container, v_object):
532
    # Normal Response Codes: 201
533
    # Error Response Codes: serviceUnavailable (503),
534
    #                       itemNotFound (404),
535
    #                       unauthorized (401),
536
    #                       badRequest (400)
537
    
538
    dest_path = request.META.get('HTTP_DESTINATION')
539
    if not dest_path:
540
        raise BadRequest('Missing Destination header')
541
    try:
542
        dest_container, dest_name = split_container_object_string(dest_path)
543
    except ValueError:
544
        raise BadRequest('Invalid Destination header')
545
    copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=True)
546
    return HttpResponse(status=201)
547

    
548
@api_method('POST')
549
def object_update(request, v_account, v_container, v_object):
550
    # Normal Response Codes: 202, 204
551
    # Error Response Codes: serviceUnavailable (503),
552
    #                       itemNotFound (404),
553
    #                       unauthorized (401),
554
    #                       badRequest (400)
555
    
556
    meta = get_object_meta(request)
557
    content_type = meta.get('Content-Type')
558
    if content_type:
559
        del(meta['Content-Type']) # Do not allow changing the Content-Type.
560
    
561
    try:
562
        prev_meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
563
    except NameError:
564
        raise ItemNotFound('Object does not exist')
565
    
566
    # Handle metadata changes.
567
    if len(meta) != 0:
568
        # Keep previous values of 'Content-Type' and 'hash'.
569
        for k in ('Content-Type', 'hash'):
570
            if k in prev_meta:
571
                meta[k] = prev_meta[k]
572
        try:
573
            backend.update_object_meta(request.user, v_account, v_container, v_object, meta, replace=True)
574
        except NameError:
575
            raise ItemNotFound('Object does not exist')
576
    
577
    # A Content-Type or Content-Range header may indicate data updates.
578
    if content_type and content_type.startswith('multipart/byteranges'):
579
        # TODO: Support multiple update ranges.
580
        return HttpResponse(status=202)
581
    # Single range update. Range must be in Content-Range.
582
    # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
583
    # (with the addition that '*' is allowed for the range - will append).
584
    if content_type and content_type != 'application/octet-stream':
585
        return HttpResponse(status=202)
586
    content_range = request.META.get('HTTP_CONTENT_RANGE')
587
    if not content_range:
588
        return HttpResponse(status=202)
589
    ranges = get_content_range(request)
590
    if not ranges:
591
        return HttpResponse(status=202)
592
    # Require either a Content-Length, or 'chunked' Transfer-Encoding.
593
    content_length = -1
594
    if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
595
        content_length = get_content_length(request)
596
    
597
    try:
598
        size, hashmap = backend.get_object_hashmap(request.user, v_account, v_container, v_object)
599
    except NameError:
600
        raise ItemNotFound('Object does not exist')
601
    
602
    offset, length, total = ranges
603
    if offset is None:
604
        offset = size
605
    if length is None or content_length == -1:
606
        length = content_length # Nevermind the error.
607
    elif length != content_length:
608
        raise BadRequest('Content length does not match range length')
609
    if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
610
        raise RangeNotSatisfiable('Supplied range will change provided object limits')
611
    
612
    sock = raw_input_socket(request)
613
    data = ''
614
    for d in socket_read_iterator(sock, length, backend.block_size):
615
        # TODO: Raise 408 (Request Timeout) if this takes too long.
616
        # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
617
        data += d
618
        bi = int(offset / backend.block_size)
619
        bo = offset % backend.block_size
620
        bl = min(len(data), backend.block_size - bo)
621
        offset += bl
622
        h = backend.update_block(hashmap[bi], data[:bl], bo)
623
        if bi < len(hashmap):
624
            hashmap[bi] = h
625
        else:
626
            hashmap.append(h)
627
        data = data[bl:]
628
    if len(data) > 0:
629
        bi = int(offset / backend.block_size)
630
        offset += len(data)
631
        h = backend.update_block(hashmap[bi], data)
632
        if bi < len(hashmap):
633
            hashmap[bi] = h
634
        else:
635
            hashmap.append(h)
636
    
637
    if offset > size:
638
        size = offset
639
    meta = {'hash': hashmap_hash(hashmap)} # Update ETag.
640
    try:
641
        backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, False)
642
    except NameError:
643
        raise ItemNotFound('Container does not exist')
644
        
645
    response = HttpResponse(status=204)
646
    response['ETag'] = meta['hash']
647
    return response
648

    
649
@api_method('DELETE')
650
def object_delete(request, v_account, v_container, v_object):
651
    # Normal Response Codes: 204
652
    # Error Response Codes: serviceUnavailable (503),
653
    #                       itemNotFound (404),
654
    #                       unauthorized (401),
655
    #                       badRequest (400)
656
    
657
    try:
658
        backend.delete_object(request.user, v_account, v_container, v_object)
659
    except NameError:
660
        raise ItemNotFound('Object does not exist')
661
    return HttpResponse(status=204)
662

    
663
@api_method()
664
def method_not_allowed(request):
665
    raise BadRequest('Method not allowed')