Statistics
| Branch: | Tag: | Revision:

root / pithos / api / functions.py @ 3c4bb1a0

History | View | Annotate | Download (50.2 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
# 
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
# 
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
# 
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
# 
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
# 
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import logging
35
import hashlib
36

    
37
from django.conf import settings
38
from django.http import HttpResponse
39
from django.template.loader import render_to_string
40
from django.utils import simplejson as json
41
from django.utils.http import parse_etags
42
from django.utils.encoding import smart_str
43
from xml.dom import minidom
44

    
45
from pithos.lib.filter import parse_filters
46

    
47
from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, Forbidden, ItemNotFound, Conflict,
48
    LengthRequired, PreconditionFailed, RequestEntityTooLarge, RangeNotSatisfiable, UnprocessableEntity)
49
from pithos.api.util import (json_encode_decimal, rename_meta_key, format_header_key, printable_header_dict,
50
    get_account_headers, put_account_headers, get_container_headers, put_container_headers, get_object_headers,
51
    put_object_headers, update_manifest_meta, update_sharing_meta, update_public_meta,
52
    validate_modification_preconditions, validate_matching_preconditions, split_container_object_string,
53
    copy_or_move_object, get_int_parameter, get_content_length, get_content_range, socket_read_iterator,
54
    SaveToBackendHandler, object_data_response, put_object_block, hashmap_md5, simple_list_response, api_method)
55
from pithos.backends.base import NotAllowedError, QuotaError
56

    
57

    
58
logger = logging.getLogger(__name__)
59

    
60

    
61
def top_demux(request):
62
    if request.method == 'GET':
63
        if getattr(request, 'user', None) is not None:
64
            return account_list(request)
65
        return authenticate(request)
66
    else:
67
        return method_not_allowed(request)
68

    
69
def account_demux(request, v_account):
70
    if request.method == 'HEAD':
71
        return account_meta(request, v_account)
72
    elif request.method == 'POST':
73
        return account_update(request, v_account)
74
    elif request.method == 'GET':
75
        return container_list(request, v_account)
76
    else:
77
        return method_not_allowed(request)
78

    
79
def container_demux(request, v_account, v_container):
80
    if request.method == 'HEAD':
81
        return container_meta(request, v_account, v_container)
82
    elif request.method == 'PUT':
83
        return container_create(request, v_account, v_container)
84
    elif request.method == 'POST':
85
        return container_update(request, v_account, v_container)
86
    elif request.method == 'DELETE':
87
        return container_delete(request, v_account, v_container)
88
    elif request.method == 'GET':
89
        return object_list(request, v_account, v_container)
90
    else:
91
        return method_not_allowed(request)
92

    
93
def object_demux(request, v_account, v_container, v_object):
94
    if request.method == 'HEAD':
95
        return object_meta(request, v_account, v_container, v_object)
96
    elif request.method == 'GET':
97
        return object_read(request, v_account, v_container, v_object)
98
    elif request.method == 'PUT':
99
        return object_write(request, v_account, v_container, v_object)
100
    elif request.method == 'COPY':
101
        return object_copy(request, v_account, v_container, v_object)
102
    elif request.method == 'MOVE':
103
        return object_move(request, v_account, v_container, v_object)
104
    elif request.method == 'POST':
105
        if request.META.get('CONTENT_TYPE', '').startswith('multipart/form-data'):
106
            return object_write_form(request, v_account, v_container, v_object)
107
        return object_update(request, v_account, v_container, v_object)
108
    elif request.method == 'DELETE':
109
        return object_delete(request, v_account, v_container, v_object)
110
    else:
111
        return method_not_allowed(request)
112

    
113
@api_method('GET', user_required=False)
114
def authenticate(request):
115
    # Normal Response Codes: 204
116
    # Error Response Codes: internalServerError (500),
117
    #                       forbidden (403),
118
    #                       badRequest (400)
119
    
120
    x_auth_user = request.META.get('HTTP_X_AUTH_USER')
121
    x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
122
    if not x_auth_user or not x_auth_key:
123
        raise BadRequest('Missing X-Auth-User or X-Auth-Key header')
124
    response = HttpResponse(status=204)
125
    
126
    uri = request.build_absolute_uri()
127
    if '?' in uri:
128
        uri = uri[:uri.find('?')]
129
    
130
    response['X-Auth-Token'] = x_auth_key
131
    response['X-Storage-Url'] = uri + ('' if uri.endswith('/') else '/') + x_auth_user
132
    return response
133

    
134
@api_method('GET', format_allowed=True)
135
def account_list(request):
136
    # Normal Response Codes: 200, 204
137
    # Error Response Codes: internalServerError (500),
138
    #                       badRequest (400)
139
    
140
    response = HttpResponse()
141
    
142
    marker = request.GET.get('marker')
143
    limit = get_int_parameter(request.GET.get('limit'))
144
    if not limit:
145
        limit = 10000
146
    
147
    accounts = request.backend.list_accounts(request.user_uniq, marker, limit)
148
    
149
    if request.serialization == 'text':
150
        if len(accounts) == 0:
151
            # The cloudfiles python bindings expect 200 if json/xml.
152
            response.status_code = 204
153
            return response
154
        response.status_code = 200
155
        response.content = '\n'.join(accounts) + '\n'
156
        return response
157
    
158
    account_meta = []
159
    for x in accounts:
160
        if x == request.user_uniq:
161
            continue
162
        try:
163
            meta = request.backend.get_account_meta(request.user_uniq, x, 'pithos')
164
            groups = request.backend.get_account_groups(request.user_uniq, x)
165
        except NotAllowedError:
166
            raise Forbidden('Not allowed')
167
        else:
168
            rename_meta_key(meta, 'modified', 'last_modified')
169
            rename_meta_key(meta, 'until_timestamp', 'x_account_until_timestamp')
170
            m = dict([(k[15:], v) for k, v in meta.iteritems() if k.startswith('X-Account-Meta-')])
171
            for k in m:
172
                del(meta['X-Account-Meta-' + k])
173
            if m:
174
                meta['X-Account-Meta'] = printable_header_dict(m)
175
            if groups:
176
                meta['X-Account-Group'] = printable_header_dict(dict([(k, ','.join(v)) for k, v in groups.iteritems()]))
177
            account_meta.append(printable_header_dict(meta))
178
    if request.serialization == 'xml':
179
        data = render_to_string('accounts.xml', {'accounts': account_meta})
180
    elif request.serialization  == 'json':
181
        data = json.dumps(account_meta)
182
    response.status_code = 200
183
    response.content = data
184
    return response
185

    
186
@api_method('HEAD')
187
def account_meta(request, v_account):
188
    # Normal Response Codes: 204
189
    # Error Response Codes: internalServerError (500),
190
    #                       forbidden (403),
191
    #                       badRequest (400)
192
    
193
    until = get_int_parameter(request.GET.get('until'))
194
    try:
195
        meta = request.backend.get_account_meta(request.user_uniq, v_account, 'pithos', until)
196
        groups = request.backend.get_account_groups(request.user_uniq, v_account)
197
        policy = request.backend.get_account_policy(request.user_uniq, v_account)
198
    except NotAllowedError:
199
        raise Forbidden('Not allowed')
200
    
201
    validate_modification_preconditions(request, meta)
202
    
203
    response = HttpResponse(status=204)
204
    put_account_headers(response, meta, groups, policy)
205
    return response
206

    
207
@api_method('POST')
208
def account_update(request, v_account):
209
    # Normal Response Codes: 202
210
    # Error Response Codes: internalServerError (500),
211
    #                       forbidden (403),
212
    #                       badRequest (400)
213
    
214
    meta, groups = get_account_headers(request)
215
    replace = True
216
    if 'update' in request.GET:
217
        replace = False
218
    if groups:
219
        try:
220
            request.backend.update_account_groups(request.user_uniq, v_account,
221
                                                    groups, replace)
222
        except NotAllowedError:
223
            raise Forbidden('Not allowed')
224
        except ValueError:
225
            raise BadRequest('Invalid groups header')
226
    if meta or replace:
227
        try:
228
            request.backend.update_account_meta(request.user_uniq, v_account,
229
                                                'pithos', meta, replace)
230
        except NotAllowedError:
231
            raise Forbidden('Not allowed')
232
    return HttpResponse(status=202)
233

    
234
@api_method('GET', format_allowed=True)
235
def container_list(request, v_account):
236
    # Normal Response Codes: 200, 204
237
    # Error Response Codes: internalServerError (500),
238
    #                       itemNotFound (404),
239
    #                       forbidden (403),
240
    #                       badRequest (400)
241
    
242
    until = get_int_parameter(request.GET.get('until'))
243
    try:
244
        meta = request.backend.get_account_meta(request.user_uniq, v_account, 'pithos', until)
245
        groups = request.backend.get_account_groups(request.user_uniq, v_account)
246
        policy = request.backend.get_account_policy(request.user_uniq, v_account)
247
    except NotAllowedError:
248
        raise Forbidden('Not allowed')
249
    
250
    validate_modification_preconditions(request, meta)
251
    
252
    response = HttpResponse()
253
    put_account_headers(response, meta, groups, policy)
254
    
255
    marker = request.GET.get('marker')
256
    limit = get_int_parameter(request.GET.get('limit'))
257
    if not limit:
258
        limit = 10000
259
    
260
    shared = False
261
    if 'shared' in request.GET:
262
        shared = True
263
    
264
    try:
265
        containers = request.backend.list_containers(request.user_uniq, v_account,
266
                                                marker, limit, shared, until)
267
    except NotAllowedError:
268
        raise Forbidden('Not allowed')
269
    except NameError:
270
        containers = []
271
    
272
    if request.serialization == 'text':
273
        if len(containers) == 0:
274
            # The cloudfiles python bindings expect 200 if json/xml.
275
            response.status_code = 204
276
            return response
277
        response.status_code = 200
278
        response.content = '\n'.join(containers) + '\n'
279
        return response
280
    
281
    container_meta = []
282
    for x in containers:
283
        try:
284
            meta = request.backend.get_container_meta(request.user_uniq, v_account,
285
                                                        x, 'pithos', until)
286
            policy = request.backend.get_container_policy(request.user_uniq,
287
                                                            v_account, x)
288
        except NotAllowedError:
289
            raise Forbidden('Not allowed')
290
        except NameError:
291
            pass
292
        else:
293
            rename_meta_key(meta, 'modified', 'last_modified')
294
            rename_meta_key(meta, 'until_timestamp', 'x_container_until_timestamp')
295
            m = dict([(k[17:], v) for k, v in meta.iteritems() if k.startswith('X-Container-Meta-')])
296
            for k in m:
297
                del(meta['X-Container-Meta-' + k])
298
            if m:
299
                meta['X-Container-Meta'] = printable_header_dict(m)
300
            if policy:
301
                meta['X-Container-Policy'] = printable_header_dict(dict([(k, v) for k, v in policy.iteritems()]))
302
            container_meta.append(printable_header_dict(meta))
303
    if request.serialization == 'xml':
304
        data = render_to_string('containers.xml', {'account': v_account, 'containers': container_meta})
305
    elif request.serialization  == 'json':
306
        data = json.dumps(container_meta)
307
    response.status_code = 200
308
    response.content = data
309
    return response
310

    
311
@api_method('HEAD')
312
def container_meta(request, v_account, v_container):
313
    # Normal Response Codes: 204
314
    # Error Response Codes: internalServerError (500),
315
    #                       itemNotFound (404),
316
    #                       forbidden (403),
317
    #                       badRequest (400)
318
    
319
    until = get_int_parameter(request.GET.get('until'))
320
    try:
321
        meta = request.backend.get_container_meta(request.user_uniq, v_account,
322
                                                    v_container, 'pithos', until)
323
        meta['object_meta'] = request.backend.list_object_meta(request.user_uniq,
324
                                                v_account, v_container, 'pithos', until)
325
        policy = request.backend.get_container_policy(request.user_uniq, v_account,
326
                                                        v_container)
327
    except NotAllowedError:
328
        raise Forbidden('Not allowed')
329
    except NameError:
330
        raise ItemNotFound('Container does not exist')
331
    
332
    validate_modification_preconditions(request, meta)
333
    
334
    response = HttpResponse(status=204)
335
    put_container_headers(request, response, meta, policy)
336
    return response
337

    
338
@api_method('PUT')
339
def container_create(request, v_account, v_container):
340
    # Normal Response Codes: 201, 202
341
    # Error Response Codes: internalServerError (500),
342
    #                       itemNotFound (404),
343
    #                       forbidden (403),
344
    #                       badRequest (400)
345
    
346
    meta, policy = get_container_headers(request)
347
    
348
    try:
349
        request.backend.put_container(request.user_uniq, v_account, v_container, policy)
350
        ret = 201
351
    except NotAllowedError:
352
        raise Forbidden('Not allowed')
353
    except ValueError:
354
        raise BadRequest('Invalid policy header')
355
    except NameError:
356
        ret = 202
357
    
358
    if ret == 202 and policy:
359
        try:
360
            request.backend.update_container_policy(request.user_uniq, v_account,
361
                                            v_container, policy, replace=False)
362
        except NotAllowedError:
363
            raise Forbidden('Not allowed')
364
        except NameError:
365
            raise ItemNotFound('Container does not exist')
366
        except ValueError:
367
            raise BadRequest('Invalid policy header')
368
    if meta:
369
        try:
370
            request.backend.update_container_meta(request.user_uniq, v_account,
371
                                            v_container, 'pithos', meta, replace=False)
372
        except NotAllowedError:
373
            raise Forbidden('Not allowed')
374
        except NameError:
375
            raise ItemNotFound('Container does not exist')
376
    
377
    return HttpResponse(status=ret)
378

    
379
@api_method('POST', format_allowed=True)
380
def container_update(request, v_account, v_container):
381
    # Normal Response Codes: 202
382
    # Error Response Codes: internalServerError (500),
383
    #                       itemNotFound (404),
384
    #                       forbidden (403),
385
    #                       badRequest (400)
386
    
387
    meta, policy = get_container_headers(request)
388
    replace = True
389
    if 'update' in request.GET:
390
        replace = False
391
    if policy:
392
        try:
393
            request.backend.update_container_policy(request.user_uniq, v_account,
394
                                                v_container, policy, replace)
395
        except NotAllowedError:
396
            raise Forbidden('Not allowed')
397
        except NameError:
398
            raise ItemNotFound('Container does not exist')
399
        except ValueError:
400
            raise BadRequest('Invalid policy header')
401
    if meta or replace:
402
        try:
403
            request.backend.update_container_meta(request.user_uniq, v_account,
404
                                                    v_container, 'pithos', meta, replace)
405
        except NotAllowedError:
406
            raise Forbidden('Not allowed')
407
        except NameError:
408
            raise ItemNotFound('Container does not exist')
409
    
410
    content_length = -1
411
    if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
412
        content_length = get_int_parameter(request.META.get('CONTENT_LENGTH', 0))
413
    content_type = request.META.get('CONTENT_TYPE')
414
    hashmap = []
415
    if content_type and content_type == 'application/octet-stream' and content_length != 0:
416
        for data in socket_read_iterator(request, content_length,
417
                                            request.backend.block_size):
418
            # TODO: Raise 408 (Request Timeout) if this takes too long.
419
            # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
420
            hashmap.append(request.backend.put_block(data))
421
    
422
    response = HttpResponse(status=202)
423
    if hashmap:
424
        response.content = simple_list_response(request, hashmap)
425
    return response
426

    
427
@api_method('DELETE')
428
def container_delete(request, v_account, v_container):
429
    # Normal Response Codes: 204
430
    # Error Response Codes: internalServerError (500),
431
    #                       conflict (409),
432
    #                       itemNotFound (404),
433
    #                       forbidden (403),
434
    #                       badRequest (400)
435
    
436
    until = get_int_parameter(request.GET.get('until'))
437
    try:
438
        request.backend.delete_container(request.user_uniq, v_account, v_container,
439
                                            until)
440
    except NotAllowedError:
441
        raise Forbidden('Not allowed')
442
    except NameError:
443
        raise ItemNotFound('Container does not exist')
444
    except IndexError:
445
        raise Conflict('Container is not empty')
446
    return HttpResponse(status=204)
447

    
448
@api_method('GET', format_allowed=True)
449
def object_list(request, v_account, v_container):
450
    # Normal Response Codes: 200, 204
451
    # Error Response Codes: internalServerError (500),
452
    #                       itemNotFound (404),
453
    #                       forbidden (403),
454
    #                       badRequest (400)
455
    
456
    until = get_int_parameter(request.GET.get('until'))
457
    try:
458
        meta = request.backend.get_container_meta(request.user_uniq, v_account,
459
                                                    v_container, 'pithos', until)
460
        meta['object_meta'] = request.backend.list_object_meta(request.user_uniq,
461
                                                v_account, v_container, 'pithos', until)
462
        policy = request.backend.get_container_policy(request.user_uniq, v_account,
463
                                                        v_container)
464
    except NotAllowedError:
465
        raise Forbidden('Not allowed')
466
    except NameError:
467
        raise ItemNotFound('Container does not exist')
468
    
469
    validate_modification_preconditions(request, meta)
470
    
471
    response = HttpResponse()
472
    put_container_headers(request, response, meta, policy)
473
    
474
    path = request.GET.get('path')
475
    prefix = request.GET.get('prefix')
476
    delimiter = request.GET.get('delimiter')
477
    
478
    # Path overrides prefix and delimiter.
479
    virtual = True
480
    if path:
481
        prefix = path
482
        delimiter = '/'
483
        virtual = False
484
    
485
    # Naming policy.
486
    if prefix and delimiter:
487
        prefix = prefix + delimiter
488
    if not prefix:
489
        prefix = ''
490
    prefix = prefix.lstrip('/')
491
    
492
    marker = request.GET.get('marker')
493
    limit = get_int_parameter(request.GET.get('limit'))
494
    if not limit:
495
        limit = 10000
496
    
497
    keys = request.GET.get('meta')
498
    if keys:
499
        keys = [smart_str(x.strip()) for x in keys.split(',') if x.strip() != '']
500
        included, excluded, opers = parse_filters(keys)
501
        keys = []
502
        keys += [format_header_key('X-Object-Meta-' + x) for x in included]
503
        keys += ['!'+format_header_key('X-Object-Meta-' + x) for x in excluded]
504
        keys += ['%s%s%s' % (format_header_key('X-Object-Meta-' + k), o, v) for k, o, v in opers]
505
    else:
506
        keys = []
507
    
508
    shared = False
509
    if 'shared' in request.GET:
510
        shared = True
511
    
512
    try:
513
        objects = request.backend.list_objects(request.user_uniq, v_account,
514
                                    v_container, prefix, delimiter, marker,
515
                                    limit, virtual, 'pithos', keys, shared, until)
516
    except NotAllowedError:
517
        raise Forbidden('Not allowed')
518
    except NameError:
519
        raise ItemNotFound('Container does not exist')
520
    
521
    if request.serialization == 'text':
522
        if len(objects) == 0:
523
            # The cloudfiles python bindings expect 200 if json/xml.
524
            response.status_code = 204
525
            return response
526
        response.status_code = 200
527
        response.content = '\n'.join([x[0] for x in objects]) + '\n'
528
        return response
529
    
530
    object_meta = []
531
    for x in objects:
532
        if x[1] is None:
533
            # Virtual objects/directories.
534
            object_meta.append({'subdir': x[0]})
535
        else:
536
            try:
537
                meta = request.backend.get_object_meta(request.user_uniq, v_account,
538
                                                        v_container, x[0], 'pithos', x[1])
539
                if until is None:
540
                    permissions = request.backend.get_object_permissions(
541
                                    request.user_uniq, v_account, v_container, x[0])
542
                    public = request.backend.get_object_public(request.user_uniq,
543
                                                v_account, v_container, x[0])
544
                else:
545
                    permissions = None
546
                    public = None
547
            except NotAllowedError:
548
                raise Forbidden('Not allowed')
549
            except NameError:
550
                pass
551
            else:
552
                rename_meta_key(meta, 'hash', 'x_object_hash') # Will be replaced by ETag.
553
                rename_meta_key(meta, 'ETag', 'hash')
554
                rename_meta_key(meta, 'uuid', 'x_object_uuid')
555
                rename_meta_key(meta, 'modified', 'last_modified')
556
                rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
557
                rename_meta_key(meta, 'version', 'x_object_version')
558
                rename_meta_key(meta, 'version_timestamp', 'x_object_version_timestamp')
559
                m = dict([(k[14:], v) for k, v in meta.iteritems() if k.startswith('X-Object-Meta-')])
560
                for k in m:
561
                    del(meta['X-Object-Meta-' + k])
562
                if m:
563
                    meta['X-Object-Meta'] = printable_header_dict(m)
564
                update_sharing_meta(request, permissions, v_account, v_container, x[0], meta)
565
                update_public_meta(public, meta)
566
                object_meta.append(printable_header_dict(meta))
567
    if request.serialization == 'xml':
568
        data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
569
    elif request.serialization  == 'json':
570
        data = json.dumps(object_meta, default=json_encode_decimal)
571
    response.status_code = 200
572
    response.content = data
573
    return response
574

    
575
@api_method('HEAD')
576
def object_meta(request, v_account, v_container, v_object):
577
    # Normal Response Codes: 204
578
    # Error Response Codes: internalServerError (500),
579
    #                       itemNotFound (404),
580
    #                       forbidden (403),
581
    #                       badRequest (400)
582
    
583
    version = request.GET.get('version')
584
    try:
585
        meta = request.backend.get_object_meta(request.user_uniq, v_account,
586
                                                v_container, v_object, 'pithos', version)
587
        if version is None:
588
            permissions = request.backend.get_object_permissions(request.user_uniq,
589
                                            v_account, v_container, v_object)
590
            public = request.backend.get_object_public(request.user_uniq, v_account,
591
                                                        v_container, v_object)
592
        else:
593
            permissions = None
594
            public = None
595
    except NotAllowedError:
596
        raise Forbidden('Not allowed')
597
    except NameError:
598
        raise ItemNotFound('Object does not exist')
599
    except IndexError:
600
        raise ItemNotFound('Version does not exist')
601
    
602
    update_manifest_meta(request, v_account, meta)
603
    update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
604
    update_public_meta(public, meta)
605
    
606
    # Evaluate conditions.
607
    validate_modification_preconditions(request, meta)
608
    try:
609
        validate_matching_preconditions(request, meta)
610
    except NotModified:
611
        response = HttpResponse(status=304)
612
        response['ETag'] = meta['ETag']
613
        return response
614
    
615
    response = HttpResponse(status=200)
616
    put_object_headers(response, meta)
617
    return response
618

    
619
@api_method('GET', format_allowed=True)
620
def object_read(request, v_account, v_container, v_object):
621
    # Normal Response Codes: 200, 206
622
    # Error Response Codes: internalServerError (500),
623
    #                       rangeNotSatisfiable (416),
624
    #                       preconditionFailed (412),
625
    #                       itemNotFound (404),
626
    #                       forbidden (403),
627
    #                       badRequest (400),
628
    #                       notModified (304)
629
    
630
    version = request.GET.get('version')
631
    
632
    # Reply with the version list. Do this first, as the object may be deleted.
633
    if version == 'list':
634
        if request.serialization == 'text':
635
            raise BadRequest('No format specified for version list.')
636
        
637
        try:
638
            v = request.backend.list_versions(request.user_uniq, v_account,
639
                                                v_container, v_object)
640
        except NotAllowedError:
641
            raise Forbidden('Not allowed')
642
        d = {'versions': v}
643
        if request.serialization == 'xml':
644
            d['object'] = v_object
645
            data = render_to_string('versions.xml', d)
646
        elif request.serialization  == 'json':
647
            data = json.dumps(d, default=json_encode_decimal)
648
        
649
        response = HttpResponse(data, status=200)
650
        response['Content-Length'] = len(data)
651
        return response
652
    
653
    try:
654
        meta = request.backend.get_object_meta(request.user_uniq, v_account,
655
                                                v_container, v_object, 'pithos', version)
656
        if version is None:
657
            permissions = request.backend.get_object_permissions(request.user_uniq,
658
                                            v_account, v_container, v_object)
659
            public = request.backend.get_object_public(request.user_uniq, v_account,
660
                                                        v_container, v_object)
661
        else:
662
            permissions = None
663
            public = None
664
    except NotAllowedError:
665
        raise Forbidden('Not allowed')
666
    except NameError:
667
        raise ItemNotFound('Object does not exist')
668
    except IndexError:
669
        raise ItemNotFound('Version does not exist')
670
    
671
    update_manifest_meta(request, v_account, meta)
672
    update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
673
    update_public_meta(public, meta)
674
    
675
    # Evaluate conditions.
676
    validate_modification_preconditions(request, meta)
677
    try:
678
        validate_matching_preconditions(request, meta)
679
    except NotModified:
680
        response = HttpResponse(status=304)
681
        response['ETag'] = meta['ETag']
682
        return response
683
    
684
    sizes = []
685
    hashmaps = []
686
    if 'X-Object-Manifest' in meta:
687
        try:
688
            src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
689
            objects = request.backend.list_objects(request.user_uniq, v_account,
690
                                src_container, prefix=src_name, virtual=False)
691
        except NotAllowedError:
692
            raise Forbidden('Not allowed')
693
        except ValueError:
694
            raise BadRequest('Invalid X-Object-Manifest header')
695
        except NameError:
696
            raise ItemNotFound('Container does not exist')
697
        
698
        try:
699
            for x in objects:
700
                s, h = request.backend.get_object_hashmap(request.user_uniq,
701
                                        v_account, src_container, x[0], x[1])
702
                sizes.append(s)
703
                hashmaps.append(h)
704
        except NotAllowedError:
705
            raise Forbidden('Not allowed')
706
        except NameError:
707
            raise ItemNotFound('Object does not exist')
708
        except IndexError:
709
            raise ItemNotFound('Version does not exist')
710
    else:
711
        try:
712
            s, h = request.backend.get_object_hashmap(request.user_uniq, v_account,
713
                                                v_container, v_object, version)
714
            sizes.append(s)
715
            hashmaps.append(h)
716
        except NotAllowedError:
717
            raise Forbidden('Not allowed')
718
        except NameError:
719
            raise ItemNotFound('Object does not exist')
720
        except IndexError:
721
            raise ItemNotFound('Version does not exist')
722
    
723
    # Reply with the hashmap.
724
    if 'hashmap' in request.GET and request.serialization != 'text':
725
        size = sum(sizes)
726
        hashmap = sum(hashmaps, [])
727
        d = {
728
            'block_size': request.backend.block_size,
729
            'block_hash': request.backend.hash_algorithm,
730
            'bytes': size,
731
            'hashes': hashmap}
732
        if request.serialization == 'xml':
733
            d['object'] = v_object
734
            data = render_to_string('hashes.xml', d)
735
        elif request.serialization  == 'json':
736
            data = json.dumps(d)
737
        
738
        response = HttpResponse(data, status=200)
739
        put_object_headers(response, meta)
740
        response['Content-Length'] = len(data)
741
        return response
742
    
743
    request.serialization = 'text' # Unset.
744
    return object_data_response(request, sizes, hashmaps, meta)
745

    
746
@api_method('PUT', format_allowed=True)
747
def object_write(request, v_account, v_container, v_object):
748
    # Normal Response Codes: 201
749
    # Error Response Codes: internalServerError (500),
750
    #                       unprocessableEntity (422),
751
    #                       lengthRequired (411),
752
    #                       conflict (409),
753
    #                       itemNotFound (404),
754
    #                       forbidden (403),
755
    #                       badRequest (400)
756
    
757
    # Evaluate conditions.
758
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
759
        try:
760
            meta = request.backend.get_object_meta(request.user_uniq, v_account,
761
                                                        v_container, v_object, 'pithos')
762
        except NotAllowedError:
763
            raise Forbidden('Not allowed')
764
        except NameError:
765
            meta = {}
766
        validate_matching_preconditions(request, meta)
767
    
768
    copy_from = request.META.get('HTTP_X_COPY_FROM')
769
    move_from = request.META.get('HTTP_X_MOVE_FROM')
770
    if copy_from or move_from:
771
        content_length = get_content_length(request) # Required by the API.
772
        
773
        src_account = request.META.get('HTTP_X_SOURCE_ACCOUNT')
774
        if not src_account:
775
            src_account = request.user_uniq
776
        if move_from:
777
            try:
778
                src_container, src_name = split_container_object_string(move_from)
779
            except ValueError:
780
                raise BadRequest('Invalid X-Move-From header')
781
            version_id = copy_or_move_object(request, src_account, src_container, src_name,
782
                                                v_account, v_container, v_object, move=True)
783
        else:
784
            try:
785
                src_container, src_name = split_container_object_string(copy_from)
786
            except ValueError:
787
                raise BadRequest('Invalid X-Copy-From header')
788
            version_id = copy_or_move_object(request, src_account, src_container, src_name,
789
                                                v_account, v_container, v_object, move=False)
790
        response = HttpResponse(status=201)
791
        response['X-Object-Version'] = version_id
792
        return response
793
    
794
    meta, permissions, public = get_object_headers(request)
795
    content_length = -1
796
    if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
797
        content_length = get_content_length(request)
798
    # Should be BadRequest, but API says otherwise.
799
    if 'Content-Type' not in meta:
800
        raise LengthRequired('Missing Content-Type header')
801
    
802
    if 'hashmap' in request.GET:
803
        if request.serialization not in ('json', 'xml'):
804
            raise BadRequest('Invalid hashmap format')
805
        
806
        data = ''
807
        for block in socket_read_iterator(request, content_length,
808
                                            request.backend.block_size):
809
            data = '%s%s' % (data, block)
810
        
811
        if request.serialization == 'json':
812
            d = json.loads(data)
813
            if not hasattr(d, '__getitem__'):
814
                raise BadRequest('Invalid data formating')
815
            try:
816
                hashmap = d['hashes']
817
                size = int(d['bytes'])
818
            except:
819
                raise BadRequest('Invalid data formatting')
820
        elif request.serialization == 'xml':
821
            try:
822
                xml = minidom.parseString(data)
823
                obj = xml.getElementsByTagName('object')[0]
824
                size = int(obj.attributes['bytes'].value)
825
                
826
                hashes = xml.getElementsByTagName('hash')
827
                hashmap = []
828
                for hash in hashes:
829
                    hashmap.append(hash.firstChild.data)
830
            except:
831
                raise BadRequest('Invalid data formatting')
832
    else:
833
        md5 = hashlib.md5()
834
        size = 0
835
        hashmap = []
836
        for data in socket_read_iterator(request, content_length,
837
                                            request.backend.block_size):
838
            # TODO: Raise 408 (Request Timeout) if this takes too long.
839
            # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
840
            size += len(data)
841
            hashmap.append(request.backend.put_block(data))
842
            md5.update(data)
843
        
844
        meta['ETag'] = md5.hexdigest().lower()
845
        etag = request.META.get('HTTP_ETAG')
846
        if etag and parse_etags(etag)[0].lower() != meta['ETag']:
847
            raise UnprocessableEntity('Object ETag does not match')
848
    
849
    try:
850
        version_id = request.backend.update_object_hashmap(request.user_uniq,
851
                        v_account, v_container, v_object, size, hashmap,
852
                        'pithos', meta, True, permissions)
853
    except NotAllowedError:
854
        raise Forbidden('Not allowed')
855
    except IndexError, e:
856
        raise Conflict(simple_list_response(request, e.data))
857
    except NameError:
858
        raise ItemNotFound('Container does not exist')
859
    except ValueError:
860
        raise BadRequest('Invalid sharing header')
861
    except AttributeError, e:
862
        raise Conflict(simple_list_response(request, e.data))
863
    except QuotaError:
864
        raise RequestEntityTooLarge('Quota exceeded')
865
    if 'ETag' not in meta:
866
        # Update the MD5 after the hashmap, as there may be missing hashes.
867
        # TODO: This will create a new version, even if done synchronously...
868
        etag = hashmap_md5(request, hashmap, size)
869
        meta.update({'ETag': etag}) # Update ETag.
870
        try:
871
            version_id = request.backend.update_object_meta(request.user_uniq,
872
                            v_account, v_container, v_object, 'pithos', {'ETag': etag}, False)
873
        except NotAllowedError:
874
            raise Forbidden('Not allowed')
875
    if public is not None:
876
        try:
877
            request.backend.update_object_public(request.user_uniq, v_account,
878
                                                v_container, v_object, public)
879
        except NotAllowedError:
880
            raise Forbidden('Not allowed')
881
        except NameError:
882
            raise ItemNotFound('Object does not exist')
883
    
884
    response = HttpResponse(status=201)
885
    response['ETag'] = meta['ETag']
886
    response['X-Object-Version'] = version_id
887
    return response
888

    
889
@api_method('POST')
890
def object_write_form(request, v_account, v_container, v_object):
891
    # Normal Response Codes: 201
892
    # Error Response Codes: internalServerError (500),
893
    #                       itemNotFound (404),
894
    #                       forbidden (403),
895
    #                       badRequest (400)
896
    
897
    request.upload_handlers = [SaveToBackendHandler(request)]
898
    if not request.FILES.has_key('X-Object-Data'):
899
        raise BadRequest('Missing X-Object-Data field')
900
    file = request.FILES['X-Object-Data']
901
    
902
    meta = {}
903
    meta['Content-Type'] = file.content_type
904
    meta['ETag'] = file.etag
905
    
906
    try:
907
        version_id = request.backend.update_object_hashmap(request.user_uniq,
908
                        v_account, v_container, v_object, file.size, file.hashmap,
909
                        'pithos', meta, True)
910
    except NotAllowedError:
911
        raise Forbidden('Not allowed')
912
    except NameError:
913
        raise ItemNotFound('Container does not exist')
914
    except QuotaError:
915
        raise RequestEntityTooLarge('Quota exceeded')
916
    
917
    response = HttpResponse(status=201)
918
    response['ETag'] = meta['ETag']
919
    response['X-Object-Version'] = version_id
920
    response.content = meta['ETag']
921
    return response
922

    
923
@api_method('COPY', format_allowed=True)
924
def object_copy(request, v_account, v_container, v_object):
925
    # Normal Response Codes: 201
926
    # Error Response Codes: internalServerError (500),
927
    #                       itemNotFound (404),
928
    #                       forbidden (403),
929
    #                       badRequest (400)
930
    
931
    dest_account = request.META.get('HTTP_DESTINATION_ACCOUNT')
932
    if not dest_account:
933
        dest_account = request.user_uniq
934
    dest_path = request.META.get('HTTP_DESTINATION')
935
    if not dest_path:
936
        raise BadRequest('Missing Destination header')
937
    try:
938
        dest_container, dest_name = split_container_object_string(dest_path)
939
    except ValueError:
940
        raise BadRequest('Invalid Destination header')
941
    
942
    # Evaluate conditions.
943
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
944
        src_version = request.META.get('HTTP_X_SOURCE_VERSION')
945
        try:
946
            meta = request.backend.get_object_meta(request.user_uniq, v_account,
947
                                            v_container, v_object, 'pithos', src_version)
948
        except NotAllowedError:
949
            raise Forbidden('Not allowed')
950
        except (NameError, IndexError):
951
            raise ItemNotFound('Container or object does not exist')
952
        validate_matching_preconditions(request, meta)
953
    
954
    version_id = copy_or_move_object(request, v_account, v_container, v_object,
955
                                        dest_account, dest_container, dest_name, move=False)
956
    response = HttpResponse(status=201)
957
    response['X-Object-Version'] = version_id
958
    return response
959

    
960
@api_method('MOVE', format_allowed=True)
961
def object_move(request, v_account, v_container, v_object):
962
    # Normal Response Codes: 201
963
    # Error Response Codes: internalServerError (500),
964
    #                       itemNotFound (404),
965
    #                       forbidden (403),
966
    #                       badRequest (400)
967
    
968
    dest_account = request.META.get('HTTP_DESTINATION_ACCOUNT')
969
    if not dest_account:
970
        dest_account = request.user_uniq
971
    dest_path = request.META.get('HTTP_DESTINATION')
972
    if not dest_path:
973
        raise BadRequest('Missing Destination header')
974
    try:
975
        dest_container, dest_name = split_container_object_string(dest_path)
976
    except ValueError:
977
        raise BadRequest('Invalid Destination header')
978
    
979
    # Evaluate conditions.
980
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
981
        try:
982
            meta = request.backend.get_object_meta(request.user_uniq, v_account,
983
                                                    v_container, v_object, 'pithos')
984
        except NotAllowedError:
985
            raise Forbidden('Not allowed')
986
        except NameError:
987
            raise ItemNotFound('Container or object does not exist')
988
        validate_matching_preconditions(request, meta)
989
    
990
    version_id = copy_or_move_object(request, v_account, v_container, v_object,
991
                                        dest_account, dest_container, dest_name, move=True)
992
    response = HttpResponse(status=201)
993
    response['X-Object-Version'] = version_id
994
    return response
995

    
996
@api_method('POST', format_allowed=True)
997
def object_update(request, v_account, v_container, v_object):
998
    # Normal Response Codes: 202, 204
999
    # Error Response Codes: internalServerError (500),
1000
    #                       conflict (409),
1001
    #                       itemNotFound (404),
1002
    #                       forbidden (403),
1003
    #                       badRequest (400)
1004
    
1005
    meta, permissions, public = get_object_headers(request)
1006
    content_type = meta.get('Content-Type')
1007
    if content_type:
1008
        del(meta['Content-Type']) # Do not allow changing the Content-Type.
1009
    
1010
    try:
1011
        prev_meta = request.backend.get_object_meta(request.user_uniq, v_account,
1012
                                                    v_container, v_object, 'pithos')
1013
    except NotAllowedError:
1014
        raise Forbidden('Not allowed')
1015
    except NameError:
1016
        raise ItemNotFound('Object does not exist')
1017
    
1018
    # Evaluate conditions.
1019
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
1020
        validate_matching_preconditions(request, prev_meta)
1021
    
1022
    # If replacing, keep previous values of 'Content-Type' and 'ETag'.
1023
    replace = True
1024
    if 'update' in request.GET:
1025
        replace = False
1026
    if replace:
1027
        for k in ('Content-Type', 'ETag'):
1028
            if k in prev_meta:
1029
                meta[k] = prev_meta[k]
1030
    
1031
    # A Content-Type or X-Source-Object header indicates data updates.
1032
    src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
1033
    if (not content_type or content_type != 'application/octet-stream') and not src_object:
1034
        response = HttpResponse(status=202)
1035
        
1036
        # Do permissions first, as it may fail easier.
1037
        if permissions is not None:
1038
            try:
1039
                request.backend.update_object_permissions(request.user_uniq,
1040
                                v_account, v_container, v_object, permissions)
1041
            except NotAllowedError:
1042
                raise Forbidden('Not allowed')
1043
            except NameError:
1044
                raise ItemNotFound('Object does not exist')
1045
            except ValueError:
1046
                raise BadRequest('Invalid sharing header')
1047
            except AttributeError, e:
1048
                raise Conflict(simple_list_response(request, e.data))
1049
        if public is not None:
1050
            try:
1051
                request.backend.update_object_public(request.user_uniq, v_account,
1052
                                                v_container, v_object, public)
1053
            except NotAllowedError:
1054
                raise Forbidden('Not allowed')
1055
            except NameError:
1056
                raise ItemNotFound('Object does not exist')
1057
        if meta or replace:
1058
            try:
1059
                version_id = request.backend.update_object_meta(request.user_uniq,
1060
                                v_account, v_container, v_object, 'pithos', meta, replace)
1061
            except NotAllowedError:
1062
                raise Forbidden('Not allowed')
1063
            except NameError:
1064
                raise ItemNotFound('Object does not exist')        
1065
            response['X-Object-Version'] = version_id
1066
        
1067
        return response
1068
    
1069
    # Single range update. Range must be in Content-Range.
1070
    # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
1071
    # (with the addition that '*' is allowed for the range - will append).
1072
    content_range = request.META.get('HTTP_CONTENT_RANGE')
1073
    if not content_range:
1074
        raise BadRequest('Missing Content-Range header')
1075
    ranges = get_content_range(request)
1076
    if not ranges:
1077
        raise RangeNotSatisfiable('Invalid Content-Range header')
1078
    
1079
    try:
1080
        size, hashmap = request.backend.get_object_hashmap(request.user_uniq,
1081
                                            v_account, v_container, v_object)
1082
    except NotAllowedError:
1083
        raise Forbidden('Not allowed')
1084
    except NameError:
1085
        raise ItemNotFound('Object does not exist')
1086
    
1087
    offset, length, total = ranges
1088
    if offset is None:
1089
        offset = size
1090
    elif offset > size:
1091
        raise RangeNotSatisfiable('Supplied offset is beyond object limits')
1092
    if src_object:
1093
        src_account = request.META.get('HTTP_X_SOURCE_ACCOUNT')
1094
        if not src_account:
1095
            src_account = request.user_uniq
1096
        src_container, src_name = split_container_object_string(src_object)
1097
        src_version = request.META.get('HTTP_X_SOURCE_VERSION')
1098
        try:
1099
            src_size, src_hashmap = request.backend.get_object_hashmap(request.user_uniq,
1100
                                        src_account, src_container, src_name, src_version)
1101
        except NotAllowedError:
1102
            raise Forbidden('Not allowed')
1103
        except NameError:
1104
            raise ItemNotFound('Source object does not exist')
1105
        
1106
        if length is None:
1107
            length = src_size
1108
        elif length > src_size:
1109
            raise BadRequest('Object length is smaller than range length')
1110
    else:
1111
        # Require either a Content-Length, or 'chunked' Transfer-Encoding.
1112
        content_length = -1
1113
        if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
1114
            content_length = get_content_length(request)
1115
        
1116
        if length is None:
1117
            length = content_length
1118
        else:
1119
            if content_length == -1:
1120
                # TODO: Get up to length bytes in chunks.
1121
                length = content_length
1122
            elif length != content_length:
1123
                raise BadRequest('Content length does not match range length')
1124
    if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
1125
        raise RangeNotSatisfiable('Supplied range will change provided object limits')
1126
    
1127
    dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
1128
    if dest_bytes is not None:
1129
        dest_bytes = get_int_parameter(dest_bytes)
1130
        if dest_bytes is None:
1131
            raise BadRequest('Invalid X-Object-Bytes header')
1132
    
1133
    if src_object:
1134
        if offset % request.backend.block_size == 0:
1135
            # Update the hashes only.
1136
            sbi = 0
1137
            while length > 0:
1138
                bi = int(offset / request.backend.block_size)
1139
                bl = min(length, request.backend.block_size)
1140
                if bi < len(hashmap):
1141
                    if bl == request.backend.block_size:
1142
                        hashmap[bi] = src_hashmap[sbi]
1143
                    else:
1144
                        data = request.backend.get_block(src_hashmap[sbi])
1145
                        hashmap[bi] = request.backend.update_block(hashmap[bi],
1146
                                                                data[:bl], 0)
1147
                else:
1148
                    hashmap.append(src_hashmap[sbi])
1149
                offset += bl
1150
                length -= bl
1151
                sbi += 1
1152
        else:
1153
            data = ''
1154
            sbi = 0
1155
            while length > 0:
1156
                data += request.backend.get_block(src_hashmap[sbi])
1157
                if length < request.backend.block_size:
1158
                    data = data[:length]
1159
                bytes = put_object_block(request, hashmap, data, offset)
1160
                offset += bytes
1161
                data = data[bytes:]
1162
                length -= bytes
1163
                sbi += 1
1164
    else:
1165
        data = ''
1166
        for d in socket_read_iterator(request, length,
1167
                                        request.backend.block_size):
1168
            # TODO: Raise 408 (Request Timeout) if this takes too long.
1169
            # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
1170
            data += d
1171
            bytes = put_object_block(request, hashmap, data, offset)
1172
            offset += bytes
1173
            data = data[bytes:]
1174
        if len(data) > 0:
1175
            put_object_block(request, hashmap, data, offset)
1176
    
1177
    if offset > size:
1178
        size = offset
1179
    if dest_bytes is not None and dest_bytes < size:
1180
        size = dest_bytes
1181
        hashmap = hashmap[:(int((size - 1) / request.backend.block_size) + 1)]
1182
    meta.update({'ETag': hashmap_md5(request, hashmap, size)}) # Update ETag.
1183
    try:
1184
        version_id = request.backend.update_object_hashmap(request.user_uniq,
1185
                        v_account, v_container, v_object, size, hashmap,
1186
                        'pithos', meta, replace, permissions)
1187
    except NotAllowedError:
1188
        raise Forbidden('Not allowed')
1189
    except NameError:
1190
        raise ItemNotFound('Container does not exist')
1191
    except ValueError:
1192
        raise BadRequest('Invalid sharing header')
1193
    except AttributeError, e:
1194
        raise Conflict(simple_list_response(request, e.data))
1195
    except QuotaError:
1196
        raise RequestEntityTooLarge('Quota exceeded')
1197
    if public is not None:
1198
        try:
1199
            request.backend.update_object_public(request.user_uniq, v_account,
1200
                                                v_container, v_object, public)
1201
        except NotAllowedError:
1202
            raise Forbidden('Not allowed')
1203
        except NameError:
1204
            raise ItemNotFound('Object does not exist')
1205
    
1206
    response = HttpResponse(status=204)
1207
    response['ETag'] = meta['ETag']
1208
    response['X-Object-Version'] = version_id
1209
    return response
1210

    
1211
@api_method('DELETE')
1212
def object_delete(request, v_account, v_container, v_object):
1213
    # Normal Response Codes: 204
1214
    # Error Response Codes: internalServerError (500),
1215
    #                       itemNotFound (404),
1216
    #                       forbidden (403),
1217
    #                       badRequest (400)
1218
    
1219
    until = get_int_parameter(request.GET.get('until'))
1220
    try:
1221
        request.backend.delete_object(request.user_uniq, v_account, v_container,
1222
                                        v_object, until)
1223
    except NotAllowedError:
1224
        raise Forbidden('Not allowed')
1225
    except NameError:
1226
        raise ItemNotFound('Object does not exist')
1227
    return HttpResponse(status=204)
1228

    
1229
@api_method()
1230
def method_not_allowed(request):
1231
    raise BadRequest('Method not allowed')