Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / functions.py @ 6255b97d

History | View | Annotate | Download (51.9 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
from xml.dom import minidom
35
from urllib import unquote
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 django.views.decorators.csrf import csrf_exempt
44

    
45
from synnefo.lib.astakos import get_user
46

    
47
from pithos.api.faults import (
48
    Fault, NotModified, BadRequest, Unauthorized, Forbidden, ItemNotFound,
49
    Conflict, LengthRequired, PreconditionFailed, RequestEntityTooLarge,
50
    RangeNotSatisfiable, UnprocessableEntity)
51
from pithos.api.util import (
52
    json_encode_decimal, rename_meta_key, format_header_key,
53
    printable_header_dict, get_account_headers, put_account_headers,
54
    get_container_headers, put_container_headers, get_object_headers,
55
    put_object_headers, update_manifest_meta, update_sharing_meta,
56
    update_public_meta, validate_modification_preconditions,
57
    validate_matching_preconditions, split_container_object_string,
58
    copy_or_move_object, get_int_parameter, get_content_length,
59
    get_content_range, socket_read_iterator, SaveToBackendHandler,
60
    object_data_response, put_object_block, hashmap_md5, simple_list_response,
61
    api_method, retrieve_username, retrieve_uuid,
62
    put_account_translation_headers)
63
from pithos.api.settings import UPDATE_MD5
64

    
65
from pithos.backends.base import (
66
    NotAllowedError, QuotaError, ContainerNotEmpty, ItemNotExists,
67
    VersionNotExists, ContainerExists)
68

    
69
from pithos.backends.filter import parse_filters
70

    
71
import logging
72
import hashlib
73

    
74

    
75
logger = logging.getLogger(__name__)
76

    
77

    
78
@csrf_exempt
79
def top_demux(request):
80
    if request.method == 'GET':
81
        try:
82
            request.GET['X-Auth-Token']
83
        except KeyError:
84
            try:
85
                request.META['HTTP_X_AUTH_TOKEN']
86
            except KeyError:
87
                return authenticate(request)
88
        return account_list(request)
89
    else:
90
        return method_not_allowed(request)
91

    
92

    
93
@csrf_exempt
94
def account_demux(request, v_account):
95
    if request.method == 'HEAD':
96
        return account_meta(request, v_account)
97
    elif request.method == 'POST':
98
        return account_update(request, v_account)
99
    elif request.method == 'GET':
100
        return container_list(request, v_account)
101
    else:
102
        return method_not_allowed(request)
103

    
104

    
105
@csrf_exempt
106
def container_demux(request, v_account, v_container):
107
    if request.method == 'HEAD':
108
        return container_meta(request, v_account, v_container)
109
    elif request.method == 'PUT':
110
        return container_create(request, v_account, v_container)
111
    elif request.method == 'POST':
112
        return container_update(request, v_account, v_container)
113
    elif request.method == 'DELETE':
114
        return container_delete(request, v_account, v_container)
115
    elif request.method == 'GET':
116
        return object_list(request, v_account, v_container)
117
    else:
118
        return method_not_allowed(request)
119

    
120

    
121
@csrf_exempt
122
def object_demux(request, v_account, v_container, v_object):
123
    # Helper to avoid placing the token in the URL when loading objects from a browser.
124
    if request.method == 'HEAD':
125
        return object_meta(request, v_account, v_container, v_object)
126
    elif request.method == 'GET':
127
        return object_read(request, v_account, v_container, v_object)
128
    elif request.method == 'PUT':
129
        return object_write(request, v_account, v_container, v_object)
130
    elif request.method == 'COPY':
131
        return object_copy(request, v_account, v_container, v_object)
132
    elif request.method == 'MOVE':
133
        return object_move(request, v_account, v_container, v_object)
134
    elif request.method == 'POST':
135
        if request.META.get('CONTENT_TYPE', '').startswith('multipart/form-data'):
136
            return object_write_form(request, v_account, v_container, v_object)
137
        return object_update(request, v_account, v_container, v_object)
138
    elif request.method == 'DELETE':
139
        return object_delete(request, v_account, v_container, v_object)
140
    else:
141
        return method_not_allowed(request)
142

    
143

    
144
@api_method('GET', user_required=False)
145
def authenticate(request):
146
    # Normal Response Codes: 204
147
    # Error Response Codes: internalServerError (500),
148
    #                       forbidden (403),
149
    #                       badRequest (400)
150

    
151
    x_auth_user = request.META.get('HTTP_X_AUTH_USER')
152
    x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
153
    if not x_auth_user or not x_auth_key:
154
        raise BadRequest('Missing X-Auth-User or X-Auth-Key header')
155
    response = HttpResponse(status=204)
156

    
157
    uri = request.build_absolute_uri()
158
    if '?' in uri:
159
        uri = uri[:uri.find('?')]
160

    
161
    response['X-Auth-Token'] = x_auth_key
162
    response['X-Storage-Url'] = uri + ('' if uri.endswith('/')
163
                                       else '/') + x_auth_user
164
    return response
165

    
166

    
167
@api_method('GET', format_allowed=True, request_usage=True)
168
def account_list(request):
169
    # Normal Response Codes: 200, 204
170
    # Error Response Codes: internalServerError (500),
171
    #                       badRequest (400)
172
    response = HttpResponse()
173

    
174
    marker = request.GET.get('marker')
175
    limit = get_int_parameter(request.GET.get('limit'))
176
    if not limit:
177
        limit = 10000
178

    
179
    accounts = request.backend.list_accounts(request.user_uniq, marker, limit)
180

    
181
    if request.serialization == 'text':
182
        if len(accounts) == 0:
183
            # The cloudfiles python bindings expect 200 if json/xml.
184
            response.status_code = 204
185
            return response
186
        response.status_code = 200
187
        put_account_translation_headers(response, accounts)
188
        response.content = '\n'.join(accounts) + '\n'
189
        return response
190

    
191
    account_meta = []
192
    for x in accounts:
193
        if x == request.user_uniq:
194
            continue
195
        try:
196
            meta = request.backend.get_account_meta(
197
                request.user_uniq, x, 'pithos', include_user_defined=False,
198
                external_quota=request.user_usage)
199
            groups = request.backend.get_account_groups(request.user_uniq, x)
200
        except NotAllowedError:
201
            raise Forbidden('Not allowed')
202
        else:
203
            meta['account_presentation'] = retrieve_username(x)
204
            rename_meta_key(meta, 'modified', 'last_modified')
205
            rename_meta_key(
206
                meta, 'until_timestamp', 'x_account_until_timestamp')
207
            if groups:
208
                meta['X-Account-Group'] = printable_header_dict(
209
                    dict([(k, ','.join(v)) for k, v in groups.iteritems()]))
210
            account_meta.append(printable_header_dict(meta))
211
    if request.serialization == 'xml':
212
        data = render_to_string('accounts.xml', {'accounts': account_meta})
213
    elif request.serialization == 'json':
214
        data = json.dumps(account_meta)
215
    response.status_code = 200
216
    response.content = data
217
    return response
218

    
219

    
220
@api_method('HEAD', request_usage=True)
221
def account_meta(request, v_account):
222
    # Normal Response Codes: 204
223
    # Error Response Codes: internalServerError (500),
224
    #                       forbidden (403),
225
    #                       badRequest (400)
226

    
227
    until = get_int_parameter(request.GET.get('until'))
228
    try:
229
        meta = request.backend.get_account_meta(
230
            request.user_uniq, v_account, 'pithos', until,
231
            external_quota=request.user_usage)
232
        groups = request.backend.get_account_groups(
233
            request.user_uniq, v_account)
234
        for k in groups:
235
            groups[k] = [retrieve_username(x) for x in groups[k]]
236
        policy = request.backend.get_account_policy(
237
            request.user_uniq, v_account, external_quota=request.user_usage)
238
    except NotAllowedError:
239
        raise Forbidden('Not allowed')
240

    
241
    validate_modification_preconditions(request, meta)
242

    
243
    response = HttpResponse(status=204)
244
    put_account_headers(response, meta, groups, policy)
245
    return response
246

    
247

    
248
@api_method('POST')
249
def account_update(request, v_account):
250
    # Normal Response Codes: 202
251
    # Error Response Codes: internalServerError (500),
252
    #                       forbidden (403),
253
    #                       badRequest (400)
254

    
255
    meta, groups = get_account_headers(request)
256
    for k in groups:
257
        try:
258
            groups[k] = [retrieve_uuid(x) for x in groups[k]]
259
        except ItemNotExists, e:
260
            raise BadRequest(
261
                'Bad X-Account-Group header value: unknown account: %s' % e)
262
    replace = True
263
    if 'update' in request.GET:
264
        replace = False
265
    if groups:
266
        try:
267
            request.backend.update_account_groups(request.user_uniq, v_account,
268
                                                  groups, replace)
269
        except NotAllowedError:
270
            raise Forbidden('Not allowed')
271
        except ValueError:
272
            raise BadRequest('Invalid groups header')
273
    if meta or replace:
274
        try:
275
            request.backend.update_account_meta(request.user_uniq, v_account,
276
                                                'pithos', meta, replace)
277
        except NotAllowedError:
278
            raise Forbidden('Not allowed')
279
    return HttpResponse(status=202)
280

    
281

    
282
@api_method('GET', format_allowed=True, request_usage=True)
283
def container_list(request, v_account):
284
    # Normal Response Codes: 200, 204
285
    # Error Response Codes: internalServerError (500),
286
    #                       itemNotFound (404),
287
    #                       forbidden (403),
288
    #                       badRequest (400)
289

    
290
    until = get_int_parameter(request.GET.get('until'))
291
    try:
292
        meta = request.backend.get_account_meta(
293
            request.user_uniq, v_account, 'pithos', until,
294
            external_quota=request.user_usage)
295
        groups = request.backend.get_account_groups(
296
            request.user_uniq, v_account)
297
        policy = request.backend.get_account_policy(
298
            request.user_uniq, v_account, external_quota = request.user_usage)
299
    except NotAllowedError:
300
        raise Forbidden('Not allowed')
301

    
302
    validate_modification_preconditions(request, meta)
303

    
304
    response = HttpResponse()
305
    put_account_headers(response, meta, groups, policy)
306

    
307
    marker = request.GET.get('marker')
308
    limit = get_int_parameter(request.GET.get('limit'))
309
    if not limit:
310
        limit = 10000
311

    
312
    shared = False
313
    if 'shared' in request.GET:
314
        shared = True
315
    public = False
316
    if 'public' in request.GET:
317
        public = True
318

    
319
    try:
320
        containers = request.backend.list_containers(
321
            request.user_uniq, v_account,
322
            marker, limit, shared, until, public)
323
    except NotAllowedError:
324
        raise Forbidden('Not allowed')
325
    except NameError:
326
        containers = []
327

    
328
    if request.serialization == 'text':
329
        if len(containers) == 0:
330
            # The cloudfiles python bindings expect 200 if json/xml.
331
            response.status_code = 204
332
            return response
333
        response.status_code = 200
334
        response.content = '\n'.join(containers) + '\n'
335
        return response
336

    
337
    container_meta = []
338
    for x in containers:
339
        try:
340
            meta = request.backend.get_container_meta(
341
                request.user_uniq, v_account,
342
                x, 'pithos', until, include_user_defined=False)
343
            policy = request.backend.get_container_policy(request.user_uniq,
344
                                                          v_account, x)
345
        except NotAllowedError:
346
            raise Forbidden('Not allowed')
347
        except NameError:
348
            pass
349
        else:
350
            rename_meta_key(meta, 'modified', 'last_modified')
351
            rename_meta_key(
352
                meta, 'until_timestamp', 'x_container_until_timestamp')
353
            if policy:
354
                meta['X-Container-Policy'] = printable_header_dict(
355
                    dict([(k, v) for k, v in policy.iteritems()]))
356
            container_meta.append(printable_header_dict(meta))
357
    if request.serialization == 'xml':
358
        data = render_to_string('containers.xml', {'account':
359
                                v_account, 'containers': container_meta})
360
    elif request.serialization == 'json':
361
        data = json.dumps(container_meta)
362
    response.status_code = 200
363
    response.content = data
364
    return response
365

    
366

    
367
@api_method('HEAD')
368
def container_meta(request, v_account, v_container):
369
    # Normal Response Codes: 204
370
    # Error Response Codes: internalServerError (500),
371
    #                       itemNotFound (404),
372
    #                       forbidden (403),
373
    #                       badRequest (400)
374

    
375
    until = get_int_parameter(request.GET.get('until'))
376
    try:
377
        meta = request.backend.get_container_meta(request.user_uniq, v_account,
378
                                                  v_container, 'pithos', until)
379
        meta['object_meta'] = request.backend.list_container_meta(request.user_uniq,
380
                                                                  v_account, v_container, 'pithos', until)
381
        policy = request.backend.get_container_policy(
382
            request.user_uniq, v_account,
383
            v_container)
384
    except NotAllowedError:
385
        raise Forbidden('Not allowed')
386
    except ItemNotExists:
387
        raise ItemNotFound('Container does not exist')
388

    
389
    validate_modification_preconditions(request, meta)
390

    
391
    response = HttpResponse(status=204)
392
    put_container_headers(request, response, meta, policy)
393
    return response
394

    
395

    
396
@api_method('PUT')
397
def container_create(request, v_account, v_container):
398
    # Normal Response Codes: 201, 202
399
    # Error Response Codes: internalServerError (500),
400
    #                       itemNotFound (404),
401
    #                       forbidden (403),
402
    #                       badRequest (400)
403

    
404
    meta, policy = get_container_headers(request)
405

    
406
    try:
407
        request.backend.put_container(
408
            request.user_uniq, v_account, v_container, policy)
409
        ret = 201
410
    except NotAllowedError:
411
        raise Forbidden('Not allowed')
412
    except ValueError:
413
        raise BadRequest('Invalid policy header')
414
    except ContainerExists:
415
        ret = 202
416

    
417
    if ret == 202 and policy:
418
        try:
419
            request.backend.update_container_policy(
420
                request.user_uniq, v_account,
421
                v_container, policy, replace=False)
422
        except NotAllowedError:
423
            raise Forbidden('Not allowed')
424
        except ItemNotExists:
425
            raise ItemNotFound('Container does not exist')
426
        except ValueError:
427
            raise BadRequest('Invalid policy header')
428
    if meta:
429
        try:
430
            request.backend.update_container_meta(request.user_uniq, v_account,
431
                                                  v_container, 'pithos', meta, replace=False)
432
        except NotAllowedError:
433
            raise Forbidden('Not allowed')
434
        except ItemNotExists:
435
            raise ItemNotFound('Container does not exist')
436

    
437
    return HttpResponse(status=ret)
438

    
439

    
440
@api_method('POST', format_allowed=True)
441
def container_update(request, v_account, v_container):
442
    # Normal Response Codes: 202
443
    # Error Response Codes: internalServerError (500),
444
    #                       itemNotFound (404),
445
    #                       forbidden (403),
446
    #                       badRequest (400)
447

    
448
    meta, policy = get_container_headers(request)
449
    replace = True
450
    if 'update' in request.GET:
451
        replace = False
452
    if policy:
453
        try:
454
            request.backend.update_container_policy(
455
                request.user_uniq, v_account,
456
                v_container, policy, replace)
457
        except NotAllowedError:
458
            raise Forbidden('Not allowed')
459
        except ItemNotExists:
460
            raise ItemNotFound('Container does not exist')
461
        except ValueError:
462
            raise BadRequest('Invalid policy header')
463
    if meta or replace:
464
        try:
465
            request.backend.update_container_meta(request.user_uniq, v_account,
466
                                                  v_container, 'pithos', meta, replace)
467
        except NotAllowedError:
468
            raise Forbidden('Not allowed')
469
        except ItemNotExists:
470
            raise ItemNotFound('Container does not exist')
471

    
472
    content_length = -1
473
    if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
474
        content_length = get_int_parameter(
475
            request.META.get('CONTENT_LENGTH', 0))
476
    content_type = request.META.get('CONTENT_TYPE')
477
    hashmap = []
478
    if content_type and content_type == 'application/octet-stream' and content_length != 0:
479
        for data in socket_read_iterator(request, content_length,
480
                                         request.backend.block_size):
481
            # TODO: Raise 408 (Request Timeout) if this takes too long.
482
            # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
483
            hashmap.append(request.backend.put_block(data))
484

    
485
    response = HttpResponse(status=202)
486
    if hashmap:
487
        response.content = simple_list_response(request, hashmap)
488
    return response
489

    
490

    
491
@api_method('DELETE')
492
def container_delete(request, v_account, v_container):
493
    # Normal Response Codes: 204
494
    # Error Response Codes: internalServerError (500),
495
    #                       conflict (409),
496
    #                       itemNotFound (404),
497
    #                       forbidden (403),
498
    #                       badRequest (400)
499

    
500
    until = get_int_parameter(request.GET.get('until'))
501

    
502
    delimiter = request.GET.get('delimiter')
503

    
504
    try:
505
        request.backend.delete_container(
506
            request.user_uniq, v_account, v_container,
507
            until, delimiter=delimiter)
508
    except NotAllowedError:
509
        raise Forbidden('Not allowed')
510
    except ItemNotExists:
511
        raise ItemNotFound('Container does not exist')
512
    except ContainerNotEmpty:
513
        raise Conflict('Container is not empty')
514
    return HttpResponse(status=204)
515

    
516

    
517
@api_method('GET', format_allowed=True)
518
def object_list(request, v_account, v_container):
519
    # Normal Response Codes: 200, 204
520
    # Error Response Codes: internalServerError (500),
521
    #                       itemNotFound (404),
522
    #                       forbidden (403),
523
    #                       badRequest (400)
524

    
525
    until = get_int_parameter(request.GET.get('until'))
526
    try:
527
        meta = request.backend.get_container_meta(request.user_uniq, v_account,
528
                                                  v_container, 'pithos', until)
529
        meta['object_meta'] = request.backend.list_container_meta(request.user_uniq,
530
                                                                  v_account, v_container, 'pithos', until)
531
        policy = request.backend.get_container_policy(
532
            request.user_uniq, v_account,
533
            v_container)
534
    except NotAllowedError:
535
        raise Forbidden('Not allowed')
536
    except ItemNotExists:
537
        raise ItemNotFound('Container does not exist')
538

    
539
    validate_modification_preconditions(request, meta)
540

    
541
    response = HttpResponse()
542
    put_container_headers(request, response, meta, policy)
543

    
544
    path = request.GET.get('path')
545
    prefix = request.GET.get('prefix')
546
    delimiter = request.GET.get('delimiter')
547

    
548
    # Path overrides prefix and delimiter.
549
    virtual = True
550
    if path:
551
        prefix = path
552
        delimiter = '/'
553
        virtual = False
554

    
555
    # Naming policy.
556
    if prefix and delimiter and not prefix.endswith(delimiter):
557
        prefix = prefix + delimiter
558
    if not prefix:
559
        prefix = ''
560
    prefix = prefix.lstrip('/')
561

    
562
    marker = request.GET.get('marker')
563
    limit = get_int_parameter(request.GET.get('limit'))
564
    if not limit:
565
        limit = 10000
566

    
567
    keys = request.GET.get('meta')
568
    if keys:
569
        keys = [smart_str(x.strip()) for x in keys.split(',')
570
                if x.strip() != '']
571
        included, excluded, opers = parse_filters(keys)
572
        keys = []
573
        keys += [format_header_key('X-Object-Meta-' + x) for x in included]
574
        keys += ['!' + format_header_key('X-Object-Meta-' + x)
575
                 for x in excluded]
576
        keys += ['%s%s%s' % (format_header_key(
577
            'X-Object-Meta-' + k), o, v) for k, o, v in opers]
578
    else:
579
        keys = []
580

    
581
    shared = False
582
    if 'shared' in request.GET:
583
        shared = True
584
    public = False
585
    if 'public' in request.GET:
586
        public = True
587

    
588
    if request.serialization == 'text':
589
        try:
590
            objects = request.backend.list_objects(
591
                request.user_uniq, v_account,
592
                v_container, prefix, delimiter, marker,
593
                limit, virtual, 'pithos', keys, shared,
594
                until, None, public)
595
        except NotAllowedError:
596
            raise Forbidden('Not allowed')
597
        except ItemNotExists:
598
            raise ItemNotFound('Container does not exist')
599

    
600
        if len(objects) == 0:
601
            # The cloudfiles python bindings expect 200 if json/xml.
602
            response.status_code = 204
603
            return response
604
        response.status_code = 200
605
        response.content = '\n'.join([x[0] for x in objects]) + '\n'
606
        return response
607

    
608
    try:
609
        objects = request.backend.list_object_meta(
610
            request.user_uniq, v_account,
611
            v_container, prefix, delimiter, marker,
612
            limit, virtual, 'pithos', keys, shared, until, None, public)
613
        object_permissions = {}
614
        object_public = {}
615
        if until is None:
616
            name_idx = len('/'.join((v_account, v_container, '')))
617
            for x in request.backend.list_object_permissions(request.user_uniq,
618
                                                             v_account, v_container, prefix):
619
                object = x[name_idx:]
620
                object_permissions[object] = request.backend.get_object_permissions(
621
                    request.user_uniq, v_account, v_container, object)
622
            for k, v in request.backend.list_object_public(request.user_uniq,
623
                                                           v_account, v_container, prefix).iteritems():
624
                object_public[k[name_idx:]] = v
625
    except NotAllowedError:
626
        raise Forbidden('Not allowed')
627
    except ItemNotExists:
628
        raise ItemNotFound('Container does not exist')
629

    
630
    object_meta = []
631
    for meta in objects:
632
        modified_by = meta.get('modified_by')
633
        if modified_by:
634
            meta['modified_by'] = retrieve_username(modified_by)
635
        if len(meta) == 1:
636
            # Virtual objects/directories.
637
            object_meta.append(meta)
638
        else:
639
            rename_meta_key(
640
                meta, 'hash', 'x_object_hash')  # Will be replaced by checksum.
641
            rename_meta_key(meta, 'checksum', 'hash')
642
            rename_meta_key(meta, 'type', 'content_type')
643
            rename_meta_key(meta, 'uuid', 'x_object_uuid')
644
            if until is not None and 'modified' in meta:
645
                del(meta['modified'])
646
            else:
647
                rename_meta_key(meta, 'modified', 'last_modified')
648
            rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
649
            rename_meta_key(meta, 'version', 'x_object_version')
650
            rename_meta_key(
651
                meta, 'version_timestamp', 'x_object_version_timestamp')
652
            permissions = object_permissions.get(meta['name'], None)
653
            if permissions:
654
                update_sharing_meta(request, permissions, v_account,
655
                                    v_container, meta['name'], meta)
656
            public = object_public.get(meta['name'], None)
657
            if public:
658
                update_public_meta(public, meta)
659
            object_meta.append(printable_header_dict(meta))
660
    if request.serialization == 'xml':
661
        data = render_to_string(
662
            'objects.xml', {'container': v_container, 'objects': object_meta})
663
    elif request.serialization == 'json':
664
        data = json.dumps(object_meta, default=json_encode_decimal)
665
    response.status_code = 200
666
    response.content = data
667
    return response
668

    
669

    
670
@api_method('HEAD')
671
def object_meta(request, v_account, v_container, v_object):
672
    # Normal Response Codes: 204
673
    # Error Response Codes: internalServerError (500),
674
    #                       itemNotFound (404),
675
    #                       forbidden (403),
676
    #                       badRequest (400)
677

    
678
    version = request.GET.get('version')
679
    try:
680
        meta = request.backend.get_object_meta(request.user_uniq, v_account,
681
                                               v_container, v_object, 'pithos', version)
682
        if version is None:
683
            permissions = request.backend.get_object_permissions(
684
                request.user_uniq,
685
                v_account, v_container, v_object)
686
            public = request.backend.get_object_public(
687
                request.user_uniq, v_account,
688
                v_container, v_object)
689
        else:
690
            permissions = None
691
            public = None
692
    except NotAllowedError:
693
        raise Forbidden('Not allowed')
694
    except ItemNotExists:
695
        raise ItemNotFound('Object does not exist')
696
    except VersionNotExists:
697
        raise ItemNotFound('Version does not exist')
698

    
699
    update_manifest_meta(request, v_account, meta)
700
    update_sharing_meta(
701
        request, permissions, v_account, v_container, v_object, meta)
702
    update_public_meta(public, meta)
703

    
704
    # Evaluate conditions.
705
    validate_modification_preconditions(request, meta)
706
    try:
707
        validate_matching_preconditions(request, meta)
708
    except NotModified:
709
        response = HttpResponse(status=304)
710
        response['ETag'] = meta['checksum']
711
        return response
712

    
713
    response = HttpResponse(status=200)
714
    put_object_headers(response, meta)
715
    return response
716

    
717

    
718
@api_method('GET', format_allowed=True)
719
def object_read(request, v_account, v_container, v_object):
720
    # Normal Response Codes: 200, 206
721
    # Error Response Codes: internalServerError (500),
722
    #                       rangeNotSatisfiable (416),
723
    #                       preconditionFailed (412),
724
    #                       itemNotFound (404),
725
    #                       forbidden (403),
726
    #                       badRequest (400),
727
    #                       notModified (304)
728

    
729
    version = request.GET.get('version')
730

    
731
    # Reply with the version list. Do this first, as the object may be deleted.
732
    if version == 'list':
733
        if request.serialization == 'text':
734
            raise BadRequest('No format specified for version list.')
735

    
736
        try:
737
            v = request.backend.list_versions(request.user_uniq, v_account,
738
                                              v_container, v_object)
739
        except NotAllowedError:
740
            raise Forbidden('Not allowed')
741
        except ItemNotExists:
742
            raise ItemNotFound('Object does not exist')
743
        d = {'versions': v}
744
        if request.serialization == 'xml':
745
            d['object'] = v_object
746
            data = render_to_string('versions.xml', d)
747
        elif request.serialization == 'json':
748
            data = json.dumps(d, default=json_encode_decimal)
749

    
750
        response = HttpResponse(data, status=200)
751
        response['Content-Length'] = len(data)
752
        return response
753

    
754
    try:
755
        meta = request.backend.get_object_meta(request.user_uniq, v_account,
756
                                               v_container, v_object, 'pithos', version)
757
        if version is None:
758
            permissions = request.backend.get_object_permissions(
759
                request.user_uniq,
760
                v_account, v_container, v_object)
761
            public = request.backend.get_object_public(
762
                request.user_uniq, v_account,
763
                v_container, v_object)
764
        else:
765
            permissions = None
766
            public = None
767
    except NotAllowedError:
768
        raise Forbidden('Not allowed')
769
    except ItemNotExists:
770
        raise ItemNotFound('Object does not exist')
771
    except VersionNotExists:
772
        raise ItemNotFound('Version does not exist')
773

    
774
    update_manifest_meta(request, v_account, meta)
775
    update_sharing_meta(
776
        request, permissions, v_account, v_container, v_object, meta)
777
    update_public_meta(public, meta)
778

    
779
    # Evaluate conditions.
780
    validate_modification_preconditions(request, meta)
781
    try:
782
        validate_matching_preconditions(request, meta)
783
    except NotModified:
784
        response = HttpResponse(status=304)
785
        response['ETag'] = meta['checksum']
786
        return response
787

    
788
    hashmap_reply = False
789
    if 'hashmap' in request.GET and request.serialization != 'text':
790
        hashmap_reply = True
791

    
792
    sizes = []
793
    hashmaps = []
794
    if 'X-Object-Manifest' in meta and not hashmap_reply:
795
        try:
796
            src_container, src_name = split_container_object_string(
797
                '/' + meta['X-Object-Manifest'])
798
            objects = request.backend.list_objects(
799
                request.user_uniq, v_account,
800
                src_container, prefix=src_name, virtual=False)
801
        except NotAllowedError:
802
            raise Forbidden('Not allowed')
803
        except ValueError:
804
            raise BadRequest('Invalid X-Object-Manifest header')
805
        except ItemNotExists:
806
            raise ItemNotFound('Container does not exist')
807

    
808
        try:
809
            for x in objects:
810
                s, h = request.backend.get_object_hashmap(request.user_uniq,
811
                                                          v_account, src_container, x[0], x[1])
812
                sizes.append(s)
813
                hashmaps.append(h)
814
        except NotAllowedError:
815
            raise Forbidden('Not allowed')
816
        except ItemNotExists:
817
            raise ItemNotFound('Object does not exist')
818
        except VersionNotExists:
819
            raise ItemNotFound('Version does not exist')
820
    else:
821
        try:
822
            s, h = request.backend.get_object_hashmap(
823
                request.user_uniq, v_account,
824
                v_container, v_object, version)
825
            sizes.append(s)
826
            hashmaps.append(h)
827
        except NotAllowedError:
828
            raise Forbidden('Not allowed')
829
        except ItemNotExists:
830
            raise ItemNotFound('Object does not exist')
831
        except VersionNotExists:
832
            raise ItemNotFound('Version does not exist')
833

    
834
    # Reply with the hashmap.
835
    if hashmap_reply:
836
        size = sum(sizes)
837
        hashmap = sum(hashmaps, [])
838
        d = {
839
            'block_size': request.backend.block_size,
840
            'block_hash': request.backend.hash_algorithm,
841
            'bytes': size,
842
            'hashes': hashmap}
843
        if request.serialization == 'xml':
844
            d['object'] = v_object
845
            data = render_to_string('hashes.xml', d)
846
        elif request.serialization == 'json':
847
            data = json.dumps(d)
848

    
849
        response = HttpResponse(data, status=200)
850
        put_object_headers(response, meta)
851
        response['Content-Length'] = len(data)
852
        return response
853

    
854
    request.serialization = 'text'  # Unset.
855
    return object_data_response(request, sizes, hashmaps, meta)
856

    
857

    
858
@api_method('PUT', format_allowed=True)
859
def object_write(request, v_account, v_container, v_object):
860
    # Normal Response Codes: 201
861
    # Error Response Codes: internalServerError (500),
862
    #                       unprocessableEntity (422),
863
    #                       lengthRequired (411),
864
    #                       conflict (409),
865
    #                       itemNotFound (404),
866
    #                       forbidden (403),
867
    #                       badRequest (400)
868

    
869
    # Evaluate conditions.
870
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
871
        try:
872
            meta = request.backend.get_object_meta(
873
                request.user_uniq, v_account,
874
                v_container, v_object, 'pithos')
875
        except NotAllowedError:
876
            raise Forbidden('Not allowed')
877
        except NameError:
878
            meta = {}
879
        validate_matching_preconditions(request, meta)
880

    
881
    copy_from = request.META.get('HTTP_X_COPY_FROM')
882
    move_from = request.META.get('HTTP_X_MOVE_FROM')
883
    if copy_from or move_from:
884
        delimiter = request.GET.get('delimiter')
885
        content_length = get_content_length(request)  # Required by the API.
886

    
887
        src_account = request.META.get('HTTP_X_SOURCE_ACCOUNT')
888
        if not src_account:
889
            src_account = request.user_uniq
890
        if move_from:
891
            try:
892
                src_container, src_name = split_container_object_string(
893
                    move_from)
894
            except ValueError:
895
                raise BadRequest('Invalid X-Move-From header')
896
            version_id = copy_or_move_object(
897
                request, src_account, src_container, src_name,
898
                v_account, v_container, v_object, move=True, delimiter=delimiter)
899
        else:
900
            try:
901
                src_container, src_name = split_container_object_string(
902
                    copy_from)
903
            except ValueError:
904
                raise BadRequest('Invalid X-Copy-From header')
905
            version_id = copy_or_move_object(
906
                request, src_account, src_container, src_name,
907
                v_account, v_container, v_object, move=False, delimiter=delimiter)
908
        response = HttpResponse(status=201)
909
        response['X-Object-Version'] = version_id
910
        return response
911

    
912
    content_type, meta, permissions, public = get_object_headers(request)
913
    content_length = -1
914
    if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
915
        content_length = get_content_length(request)
916
    # Should be BadRequest, but API says otherwise.
917
    if content_type is None:
918
        raise LengthRequired('Missing Content-Type header')
919

    
920
    if 'hashmap' in request.GET:
921
        if request.serialization not in ('json', 'xml'):
922
            raise BadRequest('Invalid hashmap format')
923

    
924
        data = ''
925
        for block in socket_read_iterator(request, content_length,
926
                                          request.backend.block_size):
927
            data = '%s%s' % (data, block)
928

    
929
        if request.serialization == 'json':
930
            d = json.loads(data)
931
            if not hasattr(d, '__getitem__'):
932
                raise BadRequest('Invalid data formating')
933
            try:
934
                hashmap = d['hashes']
935
                size = int(d['bytes'])
936
            except:
937
                raise BadRequest('Invalid data formatting')
938
        elif request.serialization == 'xml':
939
            try:
940
                xml = minidom.parseString(data)
941
                obj = xml.getElementsByTagName('object')[0]
942
                size = int(obj.attributes['bytes'].value)
943

    
944
                hashes = xml.getElementsByTagName('hash')
945
                hashmap = []
946
                for hash in hashes:
947
                    hashmap.append(hash.firstChild.data)
948
            except:
949
                raise BadRequest('Invalid data formatting')
950

    
951
        checksum = ''  # Do not set to None (will copy previous value).
952
    else:
953
        md5 = hashlib.md5()
954
        size = 0
955
        hashmap = []
956
        for data in socket_read_iterator(request, content_length,
957
                                         request.backend.block_size):
958
            # TODO: Raise 408 (Request Timeout) if this takes too long.
959
            # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
960
            size += len(data)
961
            hashmap.append(request.backend.put_block(data))
962
            md5.update(data)
963

    
964
        checksum = md5.hexdigest().lower()
965
        etag = request.META.get('HTTP_ETAG')
966
        if etag and parse_etags(etag)[0].lower() != checksum:
967
            raise UnprocessableEntity('Object ETag does not match')
968

    
969
    try:
970
        version_id = request.backend.update_object_hashmap(request.user_uniq,
971
                                                           v_account, v_container, v_object, size, content_type,
972
                                                           hashmap, checksum, 'pithos', meta, True, permissions)
973
    except NotAllowedError:
974
        raise Forbidden('Not allowed')
975
    except IndexError, e:
976
        raise Conflict(simple_list_response(request, e.data))
977
    except ItemNotExists:
978
        raise ItemNotFound('Container does not exist')
979
    except ValueError:
980
        raise BadRequest('Invalid sharing header')
981
    except QuotaError:
982
        raise RequestEntityTooLarge('Quota exceeded')
983
    if not checksum and UPDATE_MD5:
984
        # Update the MD5 after the hashmap, as there may be missing hashes.
985
        checksum = hashmap_md5(request.backend, hashmap, size)
986
        try:
987
            request.backend.update_object_checksum(request.user_uniq,
988
                                                   v_account, v_container, v_object, version_id, checksum)
989
        except NotAllowedError:
990
            raise Forbidden('Not allowed')
991
    if public is not None:
992
        try:
993
            request.backend.update_object_public(request.user_uniq, v_account,
994
                                                 v_container, v_object, public)
995
        except NotAllowedError:
996
            raise Forbidden('Not allowed')
997
        except ItemNotExists:
998
            raise ItemNotFound('Object does not exist')
999

    
1000
    response = HttpResponse(status=201)
1001
    if checksum:
1002
        response['ETag'] = checksum
1003
    response['X-Object-Version'] = version_id
1004
    return response
1005

    
1006

    
1007
@api_method('POST')
1008
def object_write_form(request, v_account, v_container, v_object):
1009
    # Normal Response Codes: 201
1010
    # Error Response Codes: internalServerError (500),
1011
    #                       itemNotFound (404),
1012
    #                       forbidden (403),
1013
    #                       badRequest (400)
1014

    
1015
    request.upload_handlers = [SaveToBackendHandler(request)]
1016
    if 'X-Object-Data' not in request.FILES:
1017
        raise BadRequest('Missing X-Object-Data field')
1018
    file = request.FILES['X-Object-Data']
1019

    
1020
    checksum = file.etag
1021
    try:
1022
        version_id = request.backend.update_object_hashmap(request.user_uniq,
1023
                                                           v_account, v_container, v_object, file.size, file.content_type,
1024
                                                           file.hashmap, checksum, 'pithos', {}, True)
1025
    except NotAllowedError:
1026
        raise Forbidden('Not allowed')
1027
    except ItemNotExists:
1028
        raise ItemNotFound('Container does not exist')
1029
    except QuotaError:
1030
        raise RequestEntityTooLarge('Quota exceeded')
1031

    
1032
    response = HttpResponse(status=201)
1033
    response['ETag'] = checksum
1034
    response['X-Object-Version'] = version_id
1035
    response.content = checksum
1036
    return response
1037

    
1038

    
1039
@api_method('COPY', format_allowed=True)
1040
def object_copy(request, v_account, v_container, v_object):
1041
    # Normal Response Codes: 201
1042
    # Error Response Codes: internalServerError (500),
1043
    #                       itemNotFound (404),
1044
    #                       forbidden (403),
1045
    #                       badRequest (400)
1046

    
1047
    dest_account = request.META.get('HTTP_DESTINATION_ACCOUNT')
1048
    if not dest_account:
1049
        dest_account = request.user_uniq
1050
    dest_path = request.META.get('HTTP_DESTINATION')
1051
    if not dest_path:
1052
        raise BadRequest('Missing Destination header')
1053
    try:
1054
        dest_container, dest_name = split_container_object_string(dest_path)
1055
    except ValueError:
1056
        raise BadRequest('Invalid Destination header')
1057

    
1058
    # Evaluate conditions.
1059
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
1060
        src_version = request.META.get('HTTP_X_SOURCE_VERSION')
1061
        try:
1062
            meta = request.backend.get_object_meta(
1063
                request.user_uniq, v_account,
1064
                v_container, v_object, 'pithos', src_version)
1065
        except NotAllowedError:
1066
            raise Forbidden('Not allowed')
1067
        except (ItemNotExists, VersionNotExists):
1068
            raise ItemNotFound('Container or object does not exist')
1069
        validate_matching_preconditions(request, meta)
1070

    
1071
    delimiter = request.GET.get('delimiter')
1072

    
1073
    version_id = copy_or_move_object(request, v_account, v_container, v_object,
1074
                                     dest_account, dest_container, dest_name, move=False, delimiter=delimiter)
1075
    response = HttpResponse(status=201)
1076
    response['X-Object-Version'] = version_id
1077
    return response
1078

    
1079

    
1080
@api_method('MOVE', format_allowed=True)
1081
def object_move(request, v_account, v_container, v_object):
1082
    # Normal Response Codes: 201
1083
    # Error Response Codes: internalServerError (500),
1084
    #                       itemNotFound (404),
1085
    #                       forbidden (403),
1086
    #                       badRequest (400)
1087

    
1088
    dest_account = request.META.get('HTTP_DESTINATION_ACCOUNT')
1089
    if not dest_account:
1090
        dest_account = request.user_uniq
1091
    dest_path = request.META.get('HTTP_DESTINATION')
1092
    if not dest_path:
1093
        raise BadRequest('Missing Destination header')
1094
    try:
1095
        dest_container, dest_name = split_container_object_string(dest_path)
1096
    except ValueError:
1097
        raise BadRequest('Invalid Destination header')
1098

    
1099
    # Evaluate conditions.
1100
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
1101
        try:
1102
            meta = request.backend.get_object_meta(
1103
                request.user_uniq, v_account,
1104
                v_container, v_object, 'pithos')
1105
        except NotAllowedError:
1106
            raise Forbidden('Not allowed')
1107
        except ItemNotExists:
1108
            raise ItemNotFound('Container or object does not exist')
1109
        validate_matching_preconditions(request, meta)
1110

    
1111
    delimiter = request.GET.get('delimiter')
1112

    
1113
    version_id = copy_or_move_object(request, v_account, v_container, v_object,
1114
                                     dest_account, dest_container, dest_name, move=True, delimiter=delimiter)
1115
    response = HttpResponse(status=201)
1116
    response['X-Object-Version'] = version_id
1117
    return response
1118

    
1119

    
1120
@api_method('POST', format_allowed=True)
1121
def object_update(request, v_account, v_container, v_object):
1122
    # Normal Response Codes: 202, 204
1123
    # Error Response Codes: internalServerError (500),
1124
    #                       conflict (409),
1125
    #                       itemNotFound (404),
1126
    #                       forbidden (403),
1127
    #                       badRequest (400)
1128

    
1129
    content_type, meta, permissions, public = get_object_headers(request)
1130

    
1131
    try:
1132
        prev_meta = request.backend.get_object_meta(
1133
            request.user_uniq, v_account,
1134
            v_container, v_object, 'pithos')
1135
    except NotAllowedError:
1136
        raise Forbidden('Not allowed')
1137
    except ItemNotExists:
1138
        raise ItemNotFound('Object does not exist')
1139

    
1140
    # Evaluate conditions.
1141
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
1142
        validate_matching_preconditions(request, prev_meta)
1143

    
1144
    replace = True
1145
    if 'update' in request.GET:
1146
        replace = False
1147

    
1148
    # A Content-Type or X-Source-Object header indicates data updates.
1149
    src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
1150
    if (not content_type or content_type != 'application/octet-stream') and not src_object:
1151
        response = HttpResponse(status=202)
1152

    
1153
        # Do permissions first, as it may fail easier.
1154
        if permissions is not None:
1155
            try:
1156
                request.backend.update_object_permissions(request.user_uniq,
1157
                                                          v_account, v_container, v_object, permissions)
1158
            except NotAllowedError:
1159
                raise Forbidden('Not allowed')
1160
            except ItemNotExists:
1161
                raise ItemNotFound('Object does not exist')
1162
            except ValueError:
1163
                raise BadRequest('Invalid sharing header')
1164
        if public is not None:
1165
            try:
1166
                request.backend.update_object_public(
1167
                    request.user_uniq, v_account,
1168
                    v_container, v_object, public)
1169
            except NotAllowedError:
1170
                raise Forbidden('Not allowed')
1171
            except ItemNotExists:
1172
                raise ItemNotFound('Object does not exist')
1173
        if meta or replace:
1174
            try:
1175
                version_id = request.backend.update_object_meta(
1176
                    request.user_uniq,
1177
                    v_account, v_container, v_object, 'pithos', meta, replace)
1178
            except NotAllowedError:
1179
                raise Forbidden('Not allowed')
1180
            except ItemNotExists:
1181
                raise ItemNotFound('Object does not exist')
1182
            response['X-Object-Version'] = version_id
1183

    
1184
        return response
1185

    
1186
    # Single range update. Range must be in Content-Range.
1187
    # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
1188
    # (with the addition that '*' is allowed for the range - will append).
1189
    content_range = request.META.get('HTTP_CONTENT_RANGE')
1190
    if not content_range:
1191
        raise BadRequest('Missing Content-Range header')
1192
    ranges = get_content_range(request)
1193
    if not ranges:
1194
        raise RangeNotSatisfiable('Invalid Content-Range header')
1195

    
1196
    try:
1197
        size, hashmap = request.backend.get_object_hashmap(request.user_uniq,
1198
                                                           v_account, v_container, v_object)
1199
    except NotAllowedError:
1200
        raise Forbidden('Not allowed')
1201
    except ItemNotExists:
1202
        raise ItemNotFound('Object does not exist')
1203

    
1204
    offset, length, total = ranges
1205
    if offset is None:
1206
        offset = size
1207
    elif offset > size:
1208
        raise RangeNotSatisfiable('Supplied offset is beyond object limits')
1209
    if src_object:
1210
        src_account = request.META.get('HTTP_X_SOURCE_ACCOUNT')
1211
        if not src_account:
1212
            src_account = request.user_uniq
1213
        src_container, src_name = split_container_object_string(src_object)
1214
        src_version = request.META.get('HTTP_X_SOURCE_VERSION')
1215
        try:
1216
            src_size, src_hashmap = request.backend.get_object_hashmap(
1217
                request.user_uniq,
1218
                src_account, src_container, src_name, src_version)
1219
        except NotAllowedError:
1220
            raise Forbidden('Not allowed')
1221
        except ItemNotExists:
1222
            raise ItemNotFound('Source object does not exist')
1223

    
1224
        if length is None:
1225
            length = src_size
1226
        elif length > src_size:
1227
            raise BadRequest('Object length is smaller than range length')
1228
    else:
1229
        # Require either a Content-Length, or 'chunked' Transfer-Encoding.
1230
        content_length = -1
1231
        if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
1232
            content_length = get_content_length(request)
1233

    
1234
        if length is None:
1235
            length = content_length
1236
        else:
1237
            if content_length == -1:
1238
                # TODO: Get up to length bytes in chunks.
1239
                length = content_length
1240
            elif length != content_length:
1241
                raise BadRequest('Content length does not match range length')
1242
    if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
1243
        raise RangeNotSatisfiable(
1244
            'Supplied range will change provided object limits')
1245

    
1246
    dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
1247
    if dest_bytes is not None:
1248
        dest_bytes = get_int_parameter(dest_bytes)
1249
        if dest_bytes is None:
1250
            raise BadRequest('Invalid X-Object-Bytes header')
1251

    
1252
    if src_object:
1253
        if offset % request.backend.block_size == 0:
1254
            # Update the hashes only.
1255
            sbi = 0
1256
            while length > 0:
1257
                bi = int(offset / request.backend.block_size)
1258
                bl = min(length, request.backend.block_size)
1259
                if bi < len(hashmap):
1260
                    if bl == request.backend.block_size:
1261
                        hashmap[bi] = src_hashmap[sbi]
1262
                    else:
1263
                        data = request.backend.get_block(src_hashmap[sbi])
1264
                        hashmap[bi] = request.backend.update_block(hashmap[bi],
1265
                                                                   data[:bl], 0)
1266
                else:
1267
                    hashmap.append(src_hashmap[sbi])
1268
                offset += bl
1269
                length -= bl
1270
                sbi += 1
1271
        else:
1272
            data = ''
1273
            sbi = 0
1274
            while length > 0:
1275
                data += request.backend.get_block(src_hashmap[sbi])
1276
                if length < request.backend.block_size:
1277
                    data = data[:length]
1278
                bytes = put_object_block(request, hashmap, data, offset)
1279
                offset += bytes
1280
                data = data[bytes:]
1281
                length -= bytes
1282
                sbi += 1
1283
    else:
1284
        data = ''
1285
        for d in socket_read_iterator(request, length,
1286
                                      request.backend.block_size):
1287
            # TODO: Raise 408 (Request Timeout) if this takes too long.
1288
            # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
1289
            data += d
1290
            bytes = put_object_block(request, hashmap, data, offset)
1291
            offset += bytes
1292
            data = data[bytes:]
1293
        if len(data) > 0:
1294
            put_object_block(request, hashmap, data, offset)
1295

    
1296
    if offset > size:
1297
        size = offset
1298
    if dest_bytes is not None and dest_bytes < size:
1299
        size = dest_bytes
1300
        hashmap = hashmap[:(int((size - 1) / request.backend.block_size) + 1)]
1301
    checksum = hashmap_md5(
1302
        request.backend, hashmap, size) if UPDATE_MD5 else ''
1303
    try:
1304
        version_id = request.backend.update_object_hashmap(request.user_uniq,
1305
                                                           v_account, v_container, v_object, size, prev_meta[
1306
                                                           'type'],
1307
                                                           hashmap, checksum, 'pithos', meta, replace, permissions)
1308
    except NotAllowedError:
1309
        raise Forbidden('Not allowed')
1310
    except ItemNotExists:
1311
        raise ItemNotFound('Container does not exist')
1312
    except ValueError:
1313
        raise BadRequest('Invalid sharing header')
1314
    except QuotaError:
1315
        raise RequestEntityTooLarge('Quota exceeded')
1316
    if public is not None:
1317
        try:
1318
            request.backend.update_object_public(request.user_uniq, v_account,
1319
                                                 v_container, v_object, public)
1320
        except NotAllowedError:
1321
            raise Forbidden('Not allowed')
1322
        except ItemNotExists:
1323
            raise ItemNotFound('Object does not exist')
1324

    
1325
    response = HttpResponse(status=204)
1326
    response['ETag'] = checksum
1327
    response['X-Object-Version'] = version_id
1328
    return response
1329

    
1330

    
1331
@api_method('DELETE')
1332
def object_delete(request, v_account, v_container, v_object):
1333
    # Normal Response Codes: 204
1334
    # Error Response Codes: internalServerError (500),
1335
    #                       itemNotFound (404),
1336
    #                       forbidden (403),
1337
    #                       badRequest (400)
1338

    
1339
    until = get_int_parameter(request.GET.get('until'))
1340
    delimiter = request.GET.get('delimiter')
1341

    
1342
    try:
1343
        request.backend.delete_object(
1344
            request.user_uniq, v_account, v_container,
1345
            v_object, until, delimiter=delimiter)
1346
    except NotAllowedError:
1347
        raise Forbidden('Not allowed')
1348
    except ItemNotExists:
1349
        raise ItemNotFound('Object does not exist')
1350
    return HttpResponse(status=204)
1351

    
1352

    
1353
@api_method()
1354
def method_not_allowed(request):
1355
    raise BadRequest('Method not allowed')