Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / util.py @ b17e5550

History | View | Annotate | Download (38.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 functools import wraps
35
from traceback import format_exc
36
from datetime import datetime
37
from urllib import quote, unquote
38

    
39
from django.conf import settings
40
from django.http import HttpResponse
41
from django.template.loader import render_to_string
42
from django.utils import simplejson as json
43
from django.utils.http import http_date, parse_etags
44
from django.utils.encoding import smart_unicode, smart_str
45
from django.core.files.uploadhandler import FileUploadHandler
46
from django.core.files.uploadedfile import UploadedFile
47

    
48
from snf_django.lib.api.parsedate import parse_http_date_safe, parse_http_date
49
from snf_django.lib.astakos import user_for_token
50
from snf_django.lib import api
51
from snf_django.lib.api import faults, utils
52

    
53
from pithos.api.settings import (BACKEND_DB_MODULE, BACKEND_DB_CONNECTION,
54
                                 BACKEND_BLOCK_MODULE, BACKEND_BLOCK_PATH,
55
                                 BACKEND_BLOCK_UMASK,
56
                                 BACKEND_QUEUE_MODULE, BACKEND_QUEUE_HOSTS,
57
                                 BACKEND_QUEUE_EXCHANGE,
58
                                 QUOTAHOLDER_POOLSIZE,
59
                                 SERVICE_TOKEN,
60
                                 ASTAKOS_URL,
61
                                 BACKEND_ACCOUNT_QUOTA, BACKEND_CONTAINER_QUOTA,
62
                                 BACKEND_VERSIONING,
63
                                 BACKEND_FREE_VERSIONING, BACKEND_POOL_SIZE,
64
                                 COOKIE_NAME, USER_CATALOG_URL,
65
                                 RADOS_STORAGE, RADOS_POOL_BLOCKS,
66
                                 RADOS_POOL_MAPS, TRANSLATE_UUIDS,
67
                                 PUBLIC_URL_SECURITY,
68
                                 PUBLIC_URL_ALPHABET)
69
from pithos.backends.base import (NotAllowedError, QuotaError, ItemNotExists,
70
                                  VersionNotExists)
71
from snf_django.lib.astakos import (get_user_uuid, get_displayname,
72
                                 get_uuids, get_displaynames)
73

    
74
import logging
75
import re
76
import hashlib
77
import uuid
78
import decimal
79

    
80
logger = logging.getLogger(__name__)
81

    
82

    
83
def json_encode_decimal(obj):
84
    if isinstance(obj, decimal.Decimal):
85
        return str(obj)
86
    raise TypeError(repr(obj) + " is not JSON serializable")
87

    
88

    
89
def rename_meta_key(d, old, new):
90
    if old not in d:
91
        return
92
    d[new] = d[old]
93
    del(d[old])
94

    
95

    
96
def printable_header_dict(d):
97
    """Format a meta dictionary for printing out json/xml.
98

99
    Convert all keys to lower case and replace dashes with underscores.
100
    Format 'last_modified' timestamp.
101
    """
102

    
103
    if 'last_modified' in d and d['last_modified']:
104
        d['last_modified'] = utils.isoformat(
105
            datetime.fromtimestamp(d['last_modified']))
106
    return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
107

    
108

    
109
def format_header_key(k):
110
    """Convert underscores to dashes and capitalize intra-dash strings."""
111
    return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
112

    
113

    
114
def get_header_prefix(request, prefix):
115
    """Get all prefix-* request headers in a dict. Reformat keys with format_header_key()."""
116

    
117
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
118
    # TODO: Document or remove '~' replacing.
119
    return dict([(format_header_key(k[5:]), v.replace('~', '')) for k, v in request.META.iteritems() if k.startswith(prefix) and len(k) > len(prefix)])
120

    
121

    
122
def check_meta_headers(meta):
123
    if len(meta) > 90:
124
        raise faults.BadRequest('Too many headers.')
125
    for k, v in meta.iteritems():
126
        if len(k) > 128:
127
            raise faults.BadRequest('Header name too large.')
128
        if len(v) > 256:
129
            raise faults.BadRequest('Header value too large.')
130

    
131

    
132
def get_account_headers(request):
133
    meta = get_header_prefix(request, 'X-Account-Meta-')
134
    check_meta_headers(meta)
135
    groups = {}
136
    for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
137
        n = k[16:].lower()
138
        if '-' in n or '_' in n:
139
            raise faults.BadRequest('Bad characters in group name')
140
        groups[n] = v.replace(' ', '').split(',')
141
        while '' in groups[n]:
142
            groups[n].remove('')
143
    return meta, groups
144

    
145

    
146
def put_account_headers(response, meta, groups, policy):
147
    if 'count' in meta:
148
        response['X-Account-Container-Count'] = meta['count']
149
    if 'bytes' in meta:
150
        response['X-Account-Bytes-Used'] = meta['bytes']
151
    response['Last-Modified'] = http_date(int(meta['modified']))
152
    for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
153
        response[smart_str(
154
            k, strings_only=True)] = smart_str(meta[k], strings_only=True)
155
    if 'until_timestamp' in meta:
156
        response['X-Account-Until-Timestamp'] = http_date(
157
            int(meta['until_timestamp']))
158
    for k, v in groups.iteritems():
159
        k = smart_str(k, strings_only=True)
160
        k = format_header_key('X-Account-Group-' + k)
161
        v = smart_str(','.join(v), strings_only=True)
162
        response[k] = v
163
    for k, v in policy.iteritems():
164
        response[smart_str(format_header_key('X-Account-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
165

    
166

    
167
def get_container_headers(request):
168
    meta = get_header_prefix(request, 'X-Container-Meta-')
169
    check_meta_headers(meta)
170
    policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in get_header_prefix(request, 'X-Container-Policy-').iteritems()])
171
    return meta, policy
172

    
173

    
174
def put_container_headers(request, response, meta, policy):
175
    if 'count' in meta:
176
        response['X-Container-Object-Count'] = meta['count']
177
    if 'bytes' in meta:
178
        response['X-Container-Bytes-Used'] = meta['bytes']
179
    response['Last-Modified'] = http_date(int(meta['modified']))
180
    for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
181
        response[smart_str(
182
            k, strings_only=True)] = smart_str(meta[k], strings_only=True)
183
    l = [smart_str(x, strings_only=True) for x in meta['object_meta']
184
         if x.startswith('X-Object-Meta-')]
185
    response['X-Container-Object-Meta'] = ','.join([x[14:] for x in l])
186
    response['X-Container-Block-Size'] = request.backend.block_size
187
    response['X-Container-Block-Hash'] = request.backend.hash_algorithm
188
    if 'until_timestamp' in meta:
189
        response['X-Container-Until-Timestamp'] = http_date(
190
            int(meta['until_timestamp']))
191
    for k, v in policy.iteritems():
192
        response[smart_str(format_header_key('X-Container-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
193

    
194

    
195
def get_object_headers(request):
196
    content_type = request.META.get('CONTENT_TYPE', None)
197
    meta = get_header_prefix(request, 'X-Object-Meta-')
198
    check_meta_headers(meta)
199
    if request.META.get('HTTP_CONTENT_ENCODING'):
200
        meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
201
    if request.META.get('HTTP_CONTENT_DISPOSITION'):
202
        meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
203
    if request.META.get('HTTP_X_OBJECT_MANIFEST'):
204
        meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
205
    return content_type, meta, get_sharing(request), get_public(request)
206

    
207

    
208
def put_object_headers(response, meta, restricted=False, token=None):
209
    response['ETag'] = meta['checksum']
210
    response['Content-Length'] = meta['bytes']
211
    response['Content-Type'] = meta.get('type', 'application/octet-stream')
212
    response['Last-Modified'] = http_date(int(meta['modified']))
213
    if not restricted:
214
        response['X-Object-Hash'] = meta['hash']
215
        response['X-Object-UUID'] = meta['uuid']
216
        if TRANSLATE_UUIDS:
217
            meta['modified_by'] = retrieve_displayname(token, meta['modified_by'])
218
        response['X-Object-Modified-By'] = smart_str(
219
            meta['modified_by'], strings_only=True)
220
        response['X-Object-Version'] = meta['version']
221
        response['X-Object-Version-Timestamp'] = http_date(
222
            int(meta['version_timestamp']))
223
        for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
224
            response[smart_str(
225
                k, strings_only=True)] = smart_str(meta[k], strings_only=True)
226
        for k in (
227
            'Content-Encoding', 'Content-Disposition', 'X-Object-Manifest',
228
            'X-Object-Sharing', 'X-Object-Shared-By', 'X-Object-Allowed-To',
229
                'X-Object-Public'):
230
            if k in meta:
231
                response[k] = smart_str(meta[k], strings_only=True)
232
    else:
233
        for k in ('Content-Encoding', 'Content-Disposition'):
234
            if k in meta:
235
                response[k] = smart_str(meta[k], strings_only=True)
236

    
237

    
238
def update_manifest_meta(request, v_account, meta):
239
    """Update metadata if the object has an X-Object-Manifest."""
240

    
241
    if 'X-Object-Manifest' in meta:
242
        etag = ''
243
        bytes = 0
244
        try:
245
            src_container, src_name = split_container_object_string(
246
                '/' + meta['X-Object-Manifest'])
247
            objects = request.backend.list_objects(
248
                request.user_uniq, v_account,
249
                src_container, prefix=src_name, virtual=False)
250
            for x in objects:
251
                src_meta = request.backend.get_object_meta(request.user_uniq,
252
                                                           v_account, src_container, x[0], 'pithos', x[1])
253
                etag += src_meta['checksum']
254
                bytes += src_meta['bytes']
255
        except:
256
            # Ignore errors.
257
            return
258
        meta['bytes'] = bytes
259
        md5 = hashlib.md5()
260
        md5.update(etag)
261
        meta['checksum'] = md5.hexdigest().lower()
262

    
263
def is_uuid(str):
264
    if str is None:
265
        return False
266
    try:
267
        uuid.UUID(str)
268
    except ValueError:
269
        return False
270
    else:
271
       return True
272

    
273
##########################
274
# USER CATALOG utilities #
275
##########################
276

    
277
def retrieve_displayname(token, uuid, fail_silently=True):
278
    displayname = get_displayname(token, uuid, USER_CATALOG_URL)
279
    if not displayname and not fail_silently:
280
        raise ItemNotExists(uuid)
281
    elif not displayname:
282
        # just return the uuid
283
        return uuid
284
    return displayname
285

    
286
def retrieve_displaynames(token, uuids, return_dict=False, fail_silently=True):
287
    catalog =  get_displaynames(token, uuids, USER_CATALOG_URL) or {}
288
    missing = list(set(uuids) - set(catalog))
289
    if missing and not fail_silently:
290
        raise ItemNotExists('Unknown displaynames: %s' % ', '.join(missing))
291
    return catalog if return_dict else [catalog.get(i) for i in uuids]
292

    
293
def retrieve_uuid(token, displayname):
294
    if is_uuid(displayname):
295
        return displayname
296

    
297
    uuid = get_user_uuid(token, displayname, USER_CATALOG_URL)
298
    if not uuid:
299
        raise ItemNotExists(displayname)
300
    return uuid
301

    
302
def retrieve_uuids(token, displaynames, return_dict=False, fail_silently=True):
303
    catalog = get_uuids(token, displaynames, USER_CATALOG_URL) or {}
304
    missing = list(set(displaynames) - set(catalog))
305
    if missing and not fail_silently:
306
        raise ItemNotExists('Unknown uuids: %s' % ', '.join(missing))
307
    return catalog if return_dict else [catalog.get(i) for i in displaynames]
308

    
309
def replace_permissions_displayname(token, holder):
310
    if holder == '*':
311
        return holder
312
    try:
313
        # check first for a group permission
314
        account, group = holder.split(':', 1)
315
    except ValueError:
316
        return retrieve_uuid(token, holder)
317
    else:
318
        return ':'.join([retrieve_uuid(token, account), group])
319

    
320
def replace_permissions_uuid(token, holder):
321
    if holder == '*':
322
        return holder
323
    try:
324
        # check first for a group permission
325
        account, group = holder.split(':', 1)
326
    except ValueError:
327
        return retrieve_displayname(token, holder)
328
    else:
329
        return ':'.join([retrieve_displayname(token, account), group])
330

    
331
def update_sharing_meta(request, permissions, v_account, v_container, v_object, meta):
332
    if permissions is None:
333
        return
334
    allowed, perm_path, perms = permissions
335
    if len(perms) == 0:
336
        return
337

    
338
    # replace uuid with displayname
339
    if TRANSLATE_UUIDS:
340
        perms['read'] = [replace_permissions_uuid(
341
                getattr(request, 'token', None), x) \
342
                    for x in perms.get('read', [])]
343
        perms['write'] = [replace_permissions_uuid(
344
                getattr(request, 'token', None), x) \
345
                    for x in perms.get('write', [])]
346

    
347
    ret = []
348

    
349
    r = ','.join(perms.get('read', []))
350
    if r:
351
        ret.append('read=' + r)
352
    w = ','.join(perms.get('write', []))
353
    if w:
354
        ret.append('write=' + w)
355
    meta['X-Object-Sharing'] = '; '.join(ret)
356
    if '/'.join((v_account, v_container, v_object)) != perm_path:
357
        meta['X-Object-Shared-By'] = perm_path
358
    if request.user_uniq != v_account:
359
        meta['X-Object-Allowed-To'] = allowed
360

    
361

    
362
def update_public_meta(public, meta):
363
    if not public:
364
        return
365
    meta['X-Object-Public'] = '/public/' + public
366

    
367

    
368
def validate_modification_preconditions(request, meta):
369
    """Check that the modified timestamp conforms with the preconditions set."""
370

    
371
    if 'modified' not in meta:
372
        return  # TODO: Always return?
373

    
374
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
375
    if if_modified_since is not None:
376
        if_modified_since = parse_http_date_safe(if_modified_since)
377
    if if_modified_since is not None and int(meta['modified']) <= if_modified_since:
378
        raise faults.NotModified('Resource has not been modified')
379

    
380
    if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
381
    if if_unmodified_since is not None:
382
        if_unmodified_since = parse_http_date_safe(if_unmodified_since)
383
    if if_unmodified_since is not None and int(meta['modified']) > if_unmodified_since:
384
        raise faults.PreconditionFailed('Resource has been modified')
385

    
386

    
387
def validate_matching_preconditions(request, meta):
388
    """Check that the ETag conforms with the preconditions set."""
389

    
390
    etag = meta['checksum']
391
    if not etag:
392
        etag = None
393

    
394
    if_match = request.META.get('HTTP_IF_MATCH')
395
    if if_match is not None:
396
        if etag is None:
397
            raise faults.PreconditionFailed('Resource does not exist')
398
        if if_match != '*' and etag not in [x.lower() for x in parse_etags(if_match)]:
399
            raise faults.PreconditionFailed('Resource ETag does not match')
400

    
401
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
402
    if if_none_match is not None:
403
        # TODO: If this passes, must ignore If-Modified-Since header.
404
        if etag is not None:
405
            if if_none_match == '*' or etag in [x.lower() for x in parse_etags(if_none_match)]:
406
                # TODO: Continue if an If-Modified-Since header is present.
407
                if request.method in ('HEAD', 'GET'):
408
                    raise faults.NotModified('Resource ETag matches')
409
                raise faults.PreconditionFailed('Resource exists or ETag matches')
410

    
411

    
412
def split_container_object_string(s):
413
    if not len(s) > 0 or s[0] != '/':
414
        raise ValueError
415
    s = s[1:]
416
    pos = s.find('/')
417
    if pos == -1 or pos == len(s) - 1:
418
        raise ValueError
419
    return s[:pos], s[(pos + 1):]
420

    
421

    
422
def copy_or_move_object(request, src_account, src_container, src_name, dest_account, dest_container, dest_name, move=False, delimiter=None):
423
    """Copy or move an object."""
424

    
425
    if 'ignore_content_type' in request.GET and 'CONTENT_TYPE' in request.META:
426
        del(request.META['CONTENT_TYPE'])
427
    content_type, meta, permissions, public = get_object_headers(request)
428
    src_version = request.META.get('HTTP_X_SOURCE_VERSION')
429
    try:
430
        if move:
431
            version_id = request.backend.move_object(
432
                request.user_uniq, src_account, src_container, src_name,
433
                dest_account, dest_container, dest_name,
434
                content_type, 'pithos', meta, False, permissions, delimiter)
435
        else:
436
            version_id = request.backend.copy_object(
437
                request.user_uniq, src_account, src_container, src_name,
438
                dest_account, dest_container, dest_name,
439
                content_type, 'pithos', meta, False, permissions, src_version, delimiter)
440
    except NotAllowedError:
441
        raise faults.Forbidden('Not allowed')
442
    except (ItemNotExists, VersionNotExists):
443
        raise faults.ItemNotFound('Container or object does not exist')
444
    except ValueError:
445
        raise faults.BadRequest('Invalid sharing header')
446
    except QuotaError, e:
447
        raise faults.RequestEntityTooLarge('Quota error: %s' % e)
448
    if public is not None:
449
        try:
450
            request.backend.update_object_public(request.user_uniq, dest_account, dest_container, dest_name, public)
451
        except NotAllowedError:
452
            raise faults.Forbidden('Not allowed')
453
        except ItemNotExists:
454
            raise faults.ItemNotFound('Object does not exist')
455
    return version_id
456

    
457

    
458
def get_int_parameter(p):
459
    if p is not None:
460
        try:
461
            p = int(p)
462
        except ValueError:
463
            return None
464
        if p < 0:
465
            return None
466
    return p
467

    
468

    
469
def get_content_length(request):
470
    content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
471
    if content_length is None:
472
        raise faults.LengthRequired('Missing or invalid Content-Length header')
473
    return content_length
474

    
475

    
476
def get_range(request, size):
477
    """Parse a Range header from the request.
478

479
    Either returns None, when the header is not existent or should be ignored,
480
    or a list of (offset, length) tuples - should be further checked.
481
    """
482

    
483
    ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
484
    if not ranges.startswith('bytes='):
485
        return None
486

    
487
    ret = []
488
    for r in (x.strip() for x in ranges[6:].split(',')):
489
        p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
490
        m = p.match(r)
491
        if not m:
492
            return None
493
        offset = m.group('offset')
494
        upto = m.group('upto')
495
        if offset == '' and upto == '':
496
            return None
497

    
498
        if offset != '':
499
            offset = int(offset)
500
            if upto != '':
501
                upto = int(upto)
502
                if offset > upto:
503
                    return None
504
                ret.append((offset, upto - offset + 1))
505
            else:
506
                ret.append((offset, size - offset))
507
        else:
508
            length = int(upto)
509
            ret.append((size - length, length))
510

    
511
    return ret
512

    
513

    
514
def get_content_range(request):
515
    """Parse a Content-Range header from the request.
516

517
    Either returns None, when the header is not existent or should be ignored,
518
    or an (offset, length, total) tuple - check as length, total may be None.
519
    Returns (None, None, None) if the provided range is '*/*'.
520
    """
521

    
522
    ranges = request.META.get('HTTP_CONTENT_RANGE', '')
523
    if not ranges:
524
        return None
525

    
526
    p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
527
    m = p.match(ranges)
528
    if not m:
529
        if ranges == 'bytes */*':
530
            return (None, None, None)
531
        return None
532
    offset = int(m.group('offset'))
533
    upto = m.group('upto')
534
    total = m.group('total')
535
    if upto != '':
536
        upto = int(upto)
537
    else:
538
        upto = None
539
    if total != '*':
540
        total = int(total)
541
    else:
542
        total = None
543
    if (upto is not None and offset > upto) or \
544
        (total is not None and offset >= total) or \
545
            (total is not None and upto is not None and upto >= total):
546
        return None
547

    
548
    if upto is None:
549
        length = None
550
    else:
551
        length = upto - offset + 1
552
    return (offset, length, total)
553

    
554

    
555
def get_sharing(request):
556
    """Parse an X-Object-Sharing header from the request.
557

558
    Raises BadRequest on error.
559
    """
560

    
561
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
562
    if permissions is None:
563
        return None
564

    
565
    # TODO: Document or remove '~' replacing.
566
    permissions = permissions.replace('~', '')
567

    
568
    ret = {}
569
    permissions = permissions.replace(' ', '')
570
    if permissions == '':
571
        return ret
572
    for perm in (x for x in permissions.split(';')):
573
        if perm.startswith('read='):
574
            ret['read'] = list(set(
575
                [v.replace(' ', '').lower() for v in perm[5:].split(',')]))
576
            if '' in ret['read']:
577
                ret['read'].remove('')
578
            if '*' in ret['read']:
579
                ret['read'] = ['*']
580
            if len(ret['read']) == 0:
581
                raise faults.BadRequest(
582
                    'Bad X-Object-Sharing header value: invalid length')
583
        elif perm.startswith('write='):
584
            ret['write'] = list(set(
585
                [v.replace(' ', '').lower() for v in perm[6:].split(',')]))
586
            if '' in ret['write']:
587
                ret['write'].remove('')
588
            if '*' in ret['write']:
589
                ret['write'] = ['*']
590
            if len(ret['write']) == 0:
591
                raise faults.BadRequest(
592
                    'Bad X-Object-Sharing header value: invalid length')
593
        else:
594
            raise faults.BadRequest(
595
                'Bad X-Object-Sharing header value: missing prefix')
596

    
597
    # replace displayname with uuid
598
    if TRANSLATE_UUIDS:
599
        try:
600
            ret['read'] = [replace_permissions_displayname(
601
                    getattr(request, 'token', None), x) \
602
                        for x in ret.get('read', [])]
603
            ret['write'] = [replace_permissions_displayname(
604
                    getattr(request, 'token', None), x) \
605
                        for x in ret.get('write', [])]
606
        except ItemNotExists, e:
607
            raise faults.BadRequest(
608
                'Bad X-Object-Sharing header value: unknown account: %s' % e)
609

    
610
    # Keep duplicates only in write list.
611
    dups = [x for x in ret.get(
612
        'read', []) if x in ret.get('write', []) and x != '*']
613
    if dups:
614
        for x in dups:
615
            ret['read'].remove(x)
616
        if len(ret['read']) == 0:
617
            del(ret['read'])
618

    
619
    return ret
620

    
621

    
622
def get_public(request):
623
    """Parse an X-Object-Public header from the request.
624

625
    Raises BadRequest on error.
626
    """
627

    
628
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
629
    if public is None:
630
        return None
631

    
632
    public = public.replace(' ', '').lower()
633
    if public == 'true':
634
        return True
635
    elif public == 'false' or public == '':
636
        return False
637
    raise faults.BadRequest('Bad X-Object-Public header value')
638

    
639

    
640
def raw_input_socket(request):
641
    """Return the socket for reading the rest of the request."""
642

    
643
    server_software = request.META.get('SERVER_SOFTWARE')
644
    if server_software and server_software.startswith('mod_python'):
645
        return request._req
646
    if 'wsgi.input' in request.environ:
647
        return request.environ['wsgi.input']
648
    raise NotImplemented('Unknown server software')
649

    
650
MAX_UPLOAD_SIZE = 5 * (1024 * 1024 * 1024)  # 5GB
651

    
652

    
653
def socket_read_iterator(request, length=0, blocksize=4096):
654
    """Return a maximum of blocksize data read from the socket in each iteration.
655

656
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
657
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
658
    """
659

    
660
    sock = raw_input_socket(request)
661
    if length < 0:  # Chunked transfers
662
        # Small version (server does the dechunking).
663
        if request.environ.get('mod_wsgi.input_chunked', None) or request.META['SERVER_SOFTWARE'].startswith('gunicorn'):
664
            while length < MAX_UPLOAD_SIZE:
665
                data = sock.read(blocksize)
666
                if data == '':
667
                    return
668
                yield data
669
            raise faults.BadRequest('Maximum size is reached')
670

    
671
        # Long version (do the dechunking).
672
        data = ''
673
        while length < MAX_UPLOAD_SIZE:
674
            # Get chunk size.
675
            if hasattr(sock, 'readline'):
676
                chunk_length = sock.readline()
677
            else:
678
                chunk_length = ''
679
                while chunk_length[-1:] != '\n':
680
                    chunk_length += sock.read(1)
681
                chunk_length.strip()
682
            pos = chunk_length.find(';')
683
            if pos >= 0:
684
                chunk_length = chunk_length[:pos]
685
            try:
686
                chunk_length = int(chunk_length, 16)
687
            except Exception:
688
                raise faults.BadRequest('Bad chunk size')
689
                                 # TODO: Change to something more appropriate.
690
            # Check if done.
691
            if chunk_length == 0:
692
                if len(data) > 0:
693
                    yield data
694
                return
695
            # Get the actual data.
696
            while chunk_length > 0:
697
                chunk = sock.read(min(chunk_length, blocksize))
698
                chunk_length -= len(chunk)
699
                if length > 0:
700
                    length += len(chunk)
701
                data += chunk
702
                if len(data) >= blocksize:
703
                    ret = data[:blocksize]
704
                    data = data[blocksize:]
705
                    yield ret
706
            sock.read(2)  # CRLF
707
        raise faults.BadRequest('Maximum size is reached')
708
    else:
709
        if length > MAX_UPLOAD_SIZE:
710
            raise faults.BadRequest('Maximum size is reached')
711
        while length > 0:
712
            data = sock.read(min(length, blocksize))
713
            if not data:
714
                raise faults.BadRequest()
715
            length -= len(data)
716
            yield data
717

    
718

    
719
class SaveToBackendHandler(FileUploadHandler):
720
    """Handle a file from an HTML form the django way."""
721

    
722
    def __init__(self, request=None):
723
        super(SaveToBackendHandler, self).__init__(request)
724
        self.backend = request.backend
725

    
726
    def put_data(self, length):
727
        if len(self.data) >= length:
728
            block = self.data[:length]
729
            self.file.hashmap.append(self.backend.put_block(block))
730
            self.md5.update(block)
731
            self.data = self.data[length:]
732

    
733
    def new_file(self, field_name, file_name, content_type, content_length, charset=None):
734
        self.md5 = hashlib.md5()
735
        self.data = ''
736
        self.file = UploadedFile(
737
            name=file_name, content_type=content_type, charset=charset)
738
        self.file.size = 0
739
        self.file.hashmap = []
740

    
741
    def receive_data_chunk(self, raw_data, start):
742
        self.data += raw_data
743
        self.file.size += len(raw_data)
744
        self.put_data(self.request.backend.block_size)
745
        return None
746

    
747
    def file_complete(self, file_size):
748
        l = len(self.data)
749
        if l > 0:
750
            self.put_data(l)
751
        self.file.etag = self.md5.hexdigest().lower()
752
        return self.file
753

    
754

    
755
class ObjectWrapper(object):
756
    """Return the object's data block-per-block in each iteration.
757

758
    Read from the object using the offset and length provided in each entry of the range list.
759
    """
760

    
761
    def __init__(self, backend, ranges, sizes, hashmaps, boundary):
762
        self.backend = backend
763
        self.ranges = ranges
764
        self.sizes = sizes
765
        self.hashmaps = hashmaps
766
        self.boundary = boundary
767
        self.size = sum(self.sizes)
768

    
769
        self.file_index = 0
770
        self.block_index = 0
771
        self.block_hash = -1
772
        self.block = ''
773

    
774
        self.range_index = -1
775
        self.offset, self.length = self.ranges[0]
776

    
777
    def __iter__(self):
778
        return self
779

    
780
    def part_iterator(self):
781
        if self.length > 0:
782
            # Get the file for the current offset.
783
            file_size = self.sizes[self.file_index]
784
            while self.offset >= file_size:
785
                self.offset -= file_size
786
                self.file_index += 1
787
                file_size = self.sizes[self.file_index]
788

    
789
            # Get the block for the current position.
790
            self.block_index = int(self.offset / self.backend.block_size)
791
            if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
792
                self.block_hash = self.hashmaps[
793
                    self.file_index][self.block_index]
794
                try:
795
                    self.block = self.backend.get_block(self.block_hash)
796
                except ItemNotExists:
797
                    raise faults.ItemNotFound('Block does not exist')
798

    
799
            # Get the data from the block.
800
            bo = self.offset % self.backend.block_size
801
            bs = self.backend.block_size
802
            if (self.block_index == len(self.hashmaps[self.file_index]) - 1 and
803
                    self.sizes[self.file_index] % self.backend.block_size):
804
                bs = self.sizes[self.file_index] % self.backend.block_size
805
            bl = min(self.length, bs - bo)
806
            data = self.block[bo:bo + bl]
807
            self.offset += bl
808
            self.length -= bl
809
            return data
810
        else:
811
            raise StopIteration
812

    
813
    def next(self):
814
        if len(self.ranges) == 1:
815
            return self.part_iterator()
816
        if self.range_index == len(self.ranges):
817
            raise StopIteration
818
        try:
819
            if self.range_index == -1:
820
                raise StopIteration
821
            return self.part_iterator()
822
        except StopIteration:
823
            self.range_index += 1
824
            out = []
825
            if self.range_index < len(self.ranges):
826
                # Part header.
827
                self.offset, self.length = self.ranges[self.range_index]
828
                self.file_index = 0
829
                if self.range_index > 0:
830
                    out.append('')
831
                out.append('--' + self.boundary)
832
                out.append('Content-Range: bytes %d-%d/%d' % (
833
                    self.offset, self.offset + self.length - 1, self.size))
834
                out.append('Content-Transfer-Encoding: binary')
835
                out.append('')
836
                out.append('')
837
                return '\r\n'.join(out)
838
            else:
839
                # Footer.
840
                out.append('')
841
                out.append('--' + self.boundary + '--')
842
                out.append('')
843
                return '\r\n'.join(out)
844

    
845

    
846
def object_data_response(request, sizes, hashmaps, meta, public=False):
847
    """Get the HttpResponse object for replying with the object's data."""
848

    
849
    # Range handling.
850
    size = sum(sizes)
851
    ranges = get_range(request, size)
852
    if ranges is None:
853
        ranges = [(0, size)]
854
        ret = 200
855
    else:
856
        check = [True for offset, length in ranges if
857
                 length <= 0 or length > size or
858
                 offset < 0 or offset >= size or
859
                 offset + length > size]
860
        if len(check) > 0:
861
            raise faults.RangeNotSatisfiable('Requested range exceeds object limits')
862
        ret = 206
863
        if_range = request.META.get('HTTP_IF_RANGE')
864
        if if_range:
865
            try:
866
                # Modification time has passed instead.
867
                last_modified = parse_http_date(if_range)
868
                if last_modified != meta['modified']:
869
                    ranges = [(0, size)]
870
                    ret = 200
871
            except ValueError:
872
                if if_range != meta['checksum']:
873
                    ranges = [(0, size)]
874
                    ret = 200
875

    
876
    if ret == 206 and len(ranges) > 1:
877
        boundary = uuid.uuid4().hex
878
    else:
879
        boundary = ''
880
    wrapper = ObjectWrapper(request.backend, ranges, sizes, hashmaps, boundary)
881
    response = HttpResponse(wrapper, status=ret)
882
    put_object_headers(
883
            response, meta, restricted=public, token=getattr(request, 'token', None))
884
    if ret == 206:
885
        if len(ranges) == 1:
886
            offset, length = ranges[0]
887
            response[
888
                'Content-Length'] = length  # Update with the correct length.
889
            response['Content-Range'] = 'bytes %d-%d/%d' % (
890
                offset, offset + length - 1, size)
891
        else:
892
            del(response['Content-Length'])
893
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (
894
                boundary,)
895
    return response
896

    
897

    
898
def put_object_block(request, hashmap, data, offset):
899
    """Put one block of data at the given offset."""
900

    
901
    bi = int(offset / request.backend.block_size)
902
    bo = offset % request.backend.block_size
903
    bl = min(len(data), request.backend.block_size - bo)
904
    if bi < len(hashmap):
905
        hashmap[bi] = request.backend.update_block(hashmap[bi], data[:bl], bo)
906
    else:
907
        hashmap.append(request.backend.put_block(('\x00' * bo) + data[:bl]))
908
    return bl  # Return ammount of data written.
909

    
910

    
911
def hashmap_md5(backend, hashmap, size):
912
    """Produce the MD5 sum from the data in the hashmap."""
913

    
914
    # TODO: Search backend for the MD5 of another object with the same hashmap and size...
915
    md5 = hashlib.md5()
916
    bs = backend.block_size
917
    for bi, hash in enumerate(hashmap):
918
        data = backend.get_block(hash)  # Blocks come in padded.
919
        if bi == len(hashmap) - 1:
920
            data = data[:size % bs]
921
        md5.update(data)
922
    return md5.hexdigest().lower()
923

    
924

    
925
def simple_list_response(request, l):
926
    if request.serialization == 'text':
927
        return '\n'.join(l) + '\n'
928
    if request.serialization == 'xml':
929
        return render_to_string('items.xml', {'items': l})
930
    if request.serialization == 'json':
931
        return json.dumps(l)
932

    
933

    
934
from pithos.backends.util import PithosBackendPool
935

    
936
if RADOS_STORAGE:
937
    BLOCK_PARAMS = { 'mappool': RADOS_POOL_MAPS,
938
                     'blockpool': RADOS_POOL_BLOCKS,
939
                   }
940
else:
941
    BLOCK_PARAMS = { 'mappool': None,
942
                     'blockpool': None,
943
                   }
944

    
945

    
946
_pithos_backend_pool = PithosBackendPool(
947
        size=BACKEND_POOL_SIZE,
948
        db_module=BACKEND_DB_MODULE,
949
        db_connection=BACKEND_DB_CONNECTION,
950
        block_module=BACKEND_BLOCK_MODULE,
951
        block_path=BACKEND_BLOCK_PATH,
952
        block_umask=BACKEND_BLOCK_UMASK,
953
        queue_module=BACKEND_QUEUE_MODULE,
954
        queue_hosts=BACKEND_QUEUE_HOSTS,
955
        queue_exchange=BACKEND_QUEUE_EXCHANGE,
956
        quotaholder_enabled=True,
957
        quotaholder_url=ASTAKOS_URL,
958
        quotaholder_token=SERVICE_TOKEN,
959
        quotaholder_client_poolsize=QUOTAHOLDER_POOLSIZE,
960
        free_versioning=BACKEND_FREE_VERSIONING,
961
        block_params=BLOCK_PARAMS,
962
        public_url_security=PUBLIC_URL_SECURITY,
963
        public_url_alphabet=PUBLIC_URL_ALPHABET,
964
        account_quota_policy=BACKEND_ACCOUNT_QUOTA,
965
        container_quota_policy=BACKEND_CONTAINER_QUOTA,
966
        container_versioning_policy=BACKEND_VERSIONING)
967

    
968

    
969
def get_backend():
970
    backend = _pithos_backend_pool.pool_get()
971
    backend.messages = []
972
    return backend
973

    
974

    
975
def update_request_headers(request):
976
    # Handle URL-encoded keys and values.
977
    meta = dict([(
978
        k, v) for k, v in request.META.iteritems() if k.startswith('HTTP_')])
979
    for k, v in meta.iteritems():
980
        try:
981
            k.decode('ascii')
982
            v.decode('ascii')
983
        except UnicodeDecodeError:
984
            raise faults.BadRequest('Bad character in headers.')
985
        if '%' in k or '%' in v:
986
            del(request.META[k])
987
            request.META[unquote(k)] = smart_unicode(unquote(
988
                v), strings_only=True)
989

    
990

    
991
def update_response_headers(request, response):
992
    if (not response.has_header('Content-Length') and
993
        not (response.has_header('Content-Type') and
994
             response['Content-Type'].startswith('multipart/byteranges'))):
995
        response['Content-Length'] = len(response.content)
996

    
997
    # URL-encode unicode in headers.
998
    meta = response.items()
999
    for k, v in meta:
1000
        if (k.startswith('X-Account-') or k.startswith('X-Container-') or
1001
                k.startswith('X-Object-') or k.startswith('Content-')):
1002
            del(response[k])
1003
            response[quote(k)] = quote(v, safe='/=,:@; ')
1004

    
1005

    
1006
def get_pithos_usage(token):
1007
    """Get Pithos Usage from astakos."""
1008
    astakos_url = ASTAKOS_URL + "im/authenticate"
1009
    user_info = user_for_token(token, astakos_url, usage=True)
1010
    usage = user_info.get("usage", [])
1011
    for u in usage:
1012
        if u.get('name') == 'pithos.diskspace':
1013
            return u
1014

    
1015

    
1016
def api_method(http_method=None, user_required=True, logger=None,
1017
               format_allowed=False):
1018
    def decorator(func):
1019
        @api.api_method(http_method=http_method, user_required=user_required,
1020
                        logger=logger, format_allowed=format_allowed,
1021
                        astakos_url=ASTAKOS_URL)
1022
        @wraps(func)
1023
        def wrapper(request, *args, **kwargs):
1024
            # The args variable may contain up to (account, container, object).
1025
            if len(args) > 1 and len(args[1]) > 256:
1026
                raise faults.BadRequest("Container name too large")
1027
            if len(args) > 2 and len(args[2]) > 1024:
1028
                raise faults.BadRequest('Object name too large.')
1029

    
1030
            try:
1031
                # Add a PithosBackend as attribute of the request object
1032
                request.backend = get_backend()
1033
                # Many API method expect thet X-Auth-Token in request,token
1034
                request.token = request.x_auth_token
1035
                update_request_headers(request)
1036
                response = func(request, *args, **kwargs)
1037
                update_response_headers(request, response)
1038
                return response
1039
            finally:
1040
                # Always close PithosBackend connection
1041
                if getattr(request, "backend", None) is not None:
1042
                    request.backend.close()
1043
        return wrapper
1044
    return decorator