Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / functions.py @ ddea3095

History | View | Annotate | Download (52.1 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 = '/'.join((v_account, v_container, ''))
617
            name_idx = len(name)
618
            for x in request.backend.list_object_permissions(request.user_uniq,
619
                                                             v_account, v_container, prefix):
620

    
621
                # filter out objects which are not under the container
622
                if name != x[:name_idx]:
623
                    continue
624

    
625
                object = x[name_idx:]
626
                object_permissions[object] = request.backend.get_object_permissions(
627
                    request.user_uniq, v_account, v_container, object)
628
            for k, v in request.backend.list_object_public(request.user_uniq,
629
                                                           v_account, v_container, prefix).iteritems():
630
                object_public[k[name_idx:]] = v
631
    except NotAllowedError:
632
        raise Forbidden('Not allowed')
633
    except ItemNotExists:
634
        raise ItemNotFound('Container does not exist')
635

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

    
675

    
676
@api_method('HEAD')
677
def object_meta(request, v_account, v_container, v_object):
678
    # Normal Response Codes: 204
679
    # Error Response Codes: internalServerError (500),
680
    #                       itemNotFound (404),
681
    #                       forbidden (403),
682
    #                       badRequest (400)
683

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

    
705
    update_manifest_meta(request, v_account, meta)
706
    update_sharing_meta(
707
        request, permissions, v_account, v_container, v_object, meta)
708
    update_public_meta(public, meta)
709

    
710
    # Evaluate conditions.
711
    validate_modification_preconditions(request, meta)
712
    try:
713
        validate_matching_preconditions(request, meta)
714
    except NotModified:
715
        response = HttpResponse(status=304)
716
        response['ETag'] = meta['checksum']
717
        return response
718

    
719
    response = HttpResponse(status=200)
720
    put_object_headers(response, meta)
721
    return response
722

    
723

    
724
@api_method('GET', format_allowed=True)
725
def object_read(request, v_account, v_container, v_object):
726
    # Normal Response Codes: 200, 206
727
    # Error Response Codes: internalServerError (500),
728
    #                       rangeNotSatisfiable (416),
729
    #                       preconditionFailed (412),
730
    #                       itemNotFound (404),
731
    #                       forbidden (403),
732
    #                       badRequest (400),
733
    #                       notModified (304)
734

    
735
    version = request.GET.get('version')
736

    
737
    # Reply with the version list. Do this first, as the object may be deleted.
738
    if version == 'list':
739
        if request.serialization == 'text':
740
            raise BadRequest('No format specified for version list.')
741

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

    
756
        response = HttpResponse(data, status=200)
757
        response['Content-Length'] = len(data)
758
        return response
759

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

    
780
    update_manifest_meta(request, v_account, meta)
781
    update_sharing_meta(
782
        request, permissions, v_account, v_container, v_object, meta)
783
    update_public_meta(public, meta)
784

    
785
    # Evaluate conditions.
786
    validate_modification_preconditions(request, meta)
787
    try:
788
        validate_matching_preconditions(request, meta)
789
    except NotModified:
790
        response = HttpResponse(status=304)
791
        response['ETag'] = meta['checksum']
792
        return response
793

    
794
    hashmap_reply = False
795
    if 'hashmap' in request.GET and request.serialization != 'text':
796
        hashmap_reply = True
797

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

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

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

    
855
        response = HttpResponse(data, status=200)
856
        put_object_headers(response, meta)
857
        response['Content-Length'] = len(data)
858
        return response
859

    
860
    request.serialization = 'text'  # Unset.
861
    return object_data_response(request, sizes, hashmaps, meta)
862

    
863

    
864
@api_method('PUT', format_allowed=True)
865
def object_write(request, v_account, v_container, v_object):
866
    # Normal Response Codes: 201
867
    # Error Response Codes: internalServerError (500),
868
    #                       unprocessableEntity (422),
869
    #                       lengthRequired (411),
870
    #                       conflict (409),
871
    #                       itemNotFound (404),
872
    #                       forbidden (403),
873
    #                       badRequest (400)
874

    
875
    # Evaluate conditions.
876
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
877
        try:
878
            meta = request.backend.get_object_meta(
879
                request.user_uniq, v_account,
880
                v_container, v_object, 'pithos')
881
        except NotAllowedError:
882
            raise Forbidden('Not allowed')
883
        except NameError:
884
            meta = {}
885
        validate_matching_preconditions(request, meta)
886

    
887
    copy_from = request.META.get('HTTP_X_COPY_FROM')
888
    move_from = request.META.get('HTTP_X_MOVE_FROM')
889
    if copy_from or move_from:
890
        delimiter = request.GET.get('delimiter')
891
        content_length = get_content_length(request)  # Required by the API.
892

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

    
918
    content_type, meta, permissions, public = get_object_headers(request)
919
    content_length = -1
920
    if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
921
        content_length = get_content_length(request)
922
    # Should be BadRequest, but API says otherwise.
923
    if content_type is None:
924
        raise LengthRequired('Missing Content-Type header')
925

    
926
    if 'hashmap' in request.GET:
927
        if request.serialization not in ('json', 'xml'):
928
            raise BadRequest('Invalid hashmap format')
929

    
930
        data = ''
931
        for block in socket_read_iterator(request, content_length,
932
                                          request.backend.block_size):
933
            data = '%s%s' % (data, block)
934

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

    
950
                hashes = xml.getElementsByTagName('hash')
951
                hashmap = []
952
                for hash in hashes:
953
                    hashmap.append(hash.firstChild.data)
954
            except:
955
                raise BadRequest('Invalid data formatting')
956

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

    
970
        checksum = md5.hexdigest().lower()
971
        etag = request.META.get('HTTP_ETAG')
972
        if etag and parse_etags(etag)[0].lower() != checksum:
973
            raise UnprocessableEntity('Object ETag does not match')
974

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

    
1006
    response = HttpResponse(status=201)
1007
    if checksum:
1008
        response['ETag'] = checksum
1009
    response['X-Object-Version'] = version_id
1010
    return response
1011

    
1012

    
1013
@api_method('POST')
1014
def object_write_form(request, v_account, v_container, v_object):
1015
    # Normal Response Codes: 201
1016
    # Error Response Codes: internalServerError (500),
1017
    #                       itemNotFound (404),
1018
    #                       forbidden (403),
1019
    #                       badRequest (400)
1020

    
1021
    request.upload_handlers = [SaveToBackendHandler(request)]
1022
    if 'X-Object-Data' not in request.FILES:
1023
        raise BadRequest('Missing X-Object-Data field')
1024
    file = request.FILES['X-Object-Data']
1025

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

    
1038
    response = HttpResponse(status=201)
1039
    response['ETag'] = checksum
1040
    response['X-Object-Version'] = version_id
1041
    response.content = checksum
1042
    return response
1043

    
1044

    
1045
@api_method('COPY', format_allowed=True)
1046
def object_copy(request, v_account, v_container, v_object):
1047
    # Normal Response Codes: 201
1048
    # Error Response Codes: internalServerError (500),
1049
    #                       itemNotFound (404),
1050
    #                       forbidden (403),
1051
    #                       badRequest (400)
1052

    
1053
    dest_account = request.META.get('HTTP_DESTINATION_ACCOUNT')
1054
    if not dest_account:
1055
        dest_account = request.user_uniq
1056
    dest_path = request.META.get('HTTP_DESTINATION')
1057
    if not dest_path:
1058
        raise BadRequest('Missing Destination header')
1059
    try:
1060
        dest_container, dest_name = split_container_object_string(dest_path)
1061
    except ValueError:
1062
        raise BadRequest('Invalid Destination header')
1063

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

    
1077
    delimiter = request.GET.get('delimiter')
1078

    
1079
    version_id = copy_or_move_object(request, v_account, v_container, v_object,
1080
                                     dest_account, dest_container, dest_name, move=False, delimiter=delimiter)
1081
    response = HttpResponse(status=201)
1082
    response['X-Object-Version'] = version_id
1083
    return response
1084

    
1085

    
1086
@api_method('MOVE', format_allowed=True)
1087
def object_move(request, v_account, v_container, v_object):
1088
    # Normal Response Codes: 201
1089
    # Error Response Codes: internalServerError (500),
1090
    #                       itemNotFound (404),
1091
    #                       forbidden (403),
1092
    #                       badRequest (400)
1093

    
1094
    dest_account = request.META.get('HTTP_DESTINATION_ACCOUNT')
1095
    if not dest_account:
1096
        dest_account = request.user_uniq
1097
    dest_path = request.META.get('HTTP_DESTINATION')
1098
    if not dest_path:
1099
        raise BadRequest('Missing Destination header')
1100
    try:
1101
        dest_container, dest_name = split_container_object_string(dest_path)
1102
    except ValueError:
1103
        raise BadRequest('Invalid Destination header')
1104

    
1105
    # Evaluate conditions.
1106
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
1107
        try:
1108
            meta = request.backend.get_object_meta(
1109
                request.user_uniq, v_account,
1110
                v_container, v_object, 'pithos')
1111
        except NotAllowedError:
1112
            raise Forbidden('Not allowed')
1113
        except ItemNotExists:
1114
            raise ItemNotFound('Container or object does not exist')
1115
        validate_matching_preconditions(request, meta)
1116

    
1117
    delimiter = request.GET.get('delimiter')
1118

    
1119
    version_id = copy_or_move_object(request, v_account, v_container, v_object,
1120
                                     dest_account, dest_container, dest_name, move=True, delimiter=delimiter)
1121
    response = HttpResponse(status=201)
1122
    response['X-Object-Version'] = version_id
1123
    return response
1124

    
1125

    
1126
@api_method('POST', format_allowed=True)
1127
def object_update(request, v_account, v_container, v_object):
1128
    # Normal Response Codes: 202, 204
1129
    # Error Response Codes: internalServerError (500),
1130
    #                       conflict (409),
1131
    #                       itemNotFound (404),
1132
    #                       forbidden (403),
1133
    #                       badRequest (400)
1134

    
1135
    content_type, meta, permissions, public = get_object_headers(request)
1136

    
1137
    try:
1138
        prev_meta = request.backend.get_object_meta(
1139
            request.user_uniq, v_account,
1140
            v_container, v_object, 'pithos')
1141
    except NotAllowedError:
1142
        raise Forbidden('Not allowed')
1143
    except ItemNotExists:
1144
        raise ItemNotFound('Object does not exist')
1145

    
1146
    # Evaluate conditions.
1147
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
1148
        validate_matching_preconditions(request, prev_meta)
1149

    
1150
    replace = True
1151
    if 'update' in request.GET:
1152
        replace = False
1153

    
1154
    # A Content-Type or X-Source-Object header indicates data updates.
1155
    src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
1156
    if (not content_type or content_type != 'application/octet-stream') and not src_object:
1157
        response = HttpResponse(status=202)
1158

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

    
1190
        return response
1191

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

    
1202
    try:
1203
        size, hashmap = request.backend.get_object_hashmap(request.user_uniq,
1204
                                                           v_account, v_container, v_object)
1205
    except NotAllowedError:
1206
        raise Forbidden('Not allowed')
1207
    except ItemNotExists:
1208
        raise ItemNotFound('Object does not exist')
1209

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

    
1230
        if length is None:
1231
            length = src_size
1232
        elif length > src_size:
1233
            raise BadRequest('Object length is smaller than range length')
1234
    else:
1235
        # Require either a Content-Length, or 'chunked' Transfer-Encoding.
1236
        content_length = -1
1237
        if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
1238
            content_length = get_content_length(request)
1239

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

    
1252
    dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
1253
    if dest_bytes is not None:
1254
        dest_bytes = get_int_parameter(dest_bytes)
1255
        if dest_bytes is None:
1256
            raise BadRequest('Invalid X-Object-Bytes header')
1257

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

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

    
1331
    response = HttpResponse(status=204)
1332
    response['ETag'] = checksum
1333
    response['X-Object-Version'] = version_id
1334
    return response
1335

    
1336

    
1337
@api_method('DELETE')
1338
def object_delete(request, v_account, v_container, v_object):
1339
    # Normal Response Codes: 204
1340
    # Error Response Codes: internalServerError (500),
1341
    #                       itemNotFound (404),
1342
    #                       forbidden (403),
1343
    #                       badRequest (400)
1344

    
1345
    until = get_int_parameter(request.GET.get('until'))
1346
    delimiter = request.GET.get('delimiter')
1347

    
1348
    try:
1349
        request.backend.delete_object(
1350
            request.user_uniq, v_account, v_container,
1351
            v_object, until, delimiter=delimiter)
1352
    except NotAllowedError:
1353
        raise Forbidden('Not allowed')
1354
    except ItemNotExists:
1355
        raise ItemNotFound('Object does not exist')
1356
    return HttpResponse(status=204)
1357

    
1358

    
1359
@api_method()
1360
def method_not_allowed(request):
1361
    raise BadRequest('Method not allowed')