Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / functions.py @ 890c2065

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_displayname, retrieve_uuid, retrieve_displaynames,
62
    retrieve_uuids)
63

    
64
from pithos.api.settings import UPDATE_MD5
65

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

    
70
from pithos.backends.filter import parse_filters
71

    
72
import logging
73
import hashlib
74

    
75

    
76
logger = logging.getLogger(__name__)
77

    
78

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

    
93

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

    
105

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

    
121

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

    
144

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

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

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

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

    
167

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

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

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

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

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

    
220

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

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

    
242
    validate_modification_preconditions(request, meta)
243

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

    
248

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

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

    
282

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

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

    
303
    validate_modification_preconditions(request, meta)
304

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

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

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

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

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

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

    
367

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

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

    
390
    validate_modification_preconditions(request, meta)
391

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

    
396

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

    
405
    meta, policy = get_container_headers(request)
406

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

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

    
438
    return HttpResponse(status=ret)
439

    
440

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

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

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

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

    
491

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

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

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

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

    
517

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

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

    
540
    validate_modification_preconditions(request, meta)
541

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

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

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

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

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

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

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

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

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

    
609
    try:
610
        objects = request.backend.list_object_meta(
611
            request.user_uniq, v_account,
612
            v_container, prefix, delimiter, marker,
613
            limit, virtual, 'pithos', keys, shared, until, None, public)
614
        object_permissions = {}
615
        object_public = {}
616
        if until is None:
617
            name = '/'.join((v_account, v_container, ''))
618
            name_idx = len(name)
619
            for x in request.backend.list_object_permissions(request.user_uniq,
620
                                                             v_account, v_container, prefix):
621

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

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

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

    
676

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

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

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

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

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

    
724

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
864

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1013

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

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

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

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

    
1045

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

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

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

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

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

    
1086

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

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

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

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

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

    
1126

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

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

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

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

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

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

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

    
1191
        return response
1192

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

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

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

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

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

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

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

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

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

    
1337

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

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

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

    
1359

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