Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / util.py @ 6dd0fc7c

History | View | Annotate | Download (38 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, USE_QUOTAHOLDER,
58
                                 QUOTAHOLDER_URL, QUOTAHOLDER_TOKEN,
59
                                 QUOTAHOLDER_POOLSIZE,
60
                                 ASTAKOS_URL,
61
                                 BACKEND_QUOTA, BACKEND_VERSIONING,
62
                                 BACKEND_FREE_VERSIONING, BACKEND_POOL_SIZE,
63
                                 COOKIE_NAME, USER_CATALOG_URL,
64
                                 RADOS_STORAGE, RADOS_POOL_BLOCKS,
65
                                 RADOS_POOL_MAPS, TRANSLATE_UUIDS,
66
                                 PUBLIC_URL_SECURITY,
67
                                 PUBLIC_URL_ALPHABET)
68
from pithos.backends.base import (NotAllowedError, QuotaError, ItemNotExists,
69
                                  VersionNotExists)
70
from snf_django.lib.astakos import (get_user_uuid, get_displayname,
71
                                 get_uuids, get_displaynames)
72

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

    
79
logger = logging.getLogger(__name__)
80

    
81

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

    
87

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

    
94

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

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

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

    
107

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

    
112

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

    
116
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
117
    # TODO: Document or remove '~' replacing.
118
    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)])
119

    
120

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

    
130

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

    
144

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

    
165

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

    
172

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

    
193

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

    
206

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

    
236

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

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

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

    
272
##########################
273
# USER CATALOG utilities #
274
##########################
275

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

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

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

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

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

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

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

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

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

    
346
    ret = []
347

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

    
360

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

    
366

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

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

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

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

    
385

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

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

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

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

    
410

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

    
420

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

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

    
456

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

    
467

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

    
474

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

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

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

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

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

    
510
    return ret
511

    
512

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

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

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

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

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

    
553

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

557
    Raises BadRequest on error.
558
    """
559

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

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

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

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

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

    
618
    return ret
619

    
620

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

624
    Raises BadRequest on error.
625
    """
626

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

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

    
638

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

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

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

    
651

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

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

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

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

    
717

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

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

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

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

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

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

    
753

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

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

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

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

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

    
776
    def __iter__(self):
777
        return self
778

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

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

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

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

    
844

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

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

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

    
896

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

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

    
909

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

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

    
923

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

    
932

    
933
from pithos.backends.util import PithosBackendPool
934

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

    
944

    
945
_pithos_backend_pool = PithosBackendPool(
946
        size=BACKEND_POOL_SIZE,
947
        db_module=BACKEND_DB_MODULE,
948
        db_connection=BACKEND_DB_CONNECTION,
949
        block_module=BACKEND_BLOCK_MODULE,
950
        block_path=BACKEND_BLOCK_PATH,
951
        block_umask=BACKEND_BLOCK_UMASK,
952
        queue_module=BACKEND_QUEUE_MODULE,
953
        queue_hosts=BACKEND_QUEUE_HOSTS,
954
        queue_exchange=BACKEND_QUEUE_EXCHANGE,
955
        quotaholder_enabled=USE_QUOTAHOLDER,
956
        quotaholder_url=QUOTAHOLDER_URL,
957
        quotaholder_token=QUOTAHOLDER_TOKEN,
958
        quotaholder_client_poolsize=QUOTAHOLDER_POOLSIZE,
959
        free_versioning=BACKEND_FREE_VERSIONING,
960
        block_params=BLOCK_PARAMS,
961
        public_url_security=PUBLIC_URL_SECURITY,
962
        public_url_alphabet=PUBLIC_URL_ALPHABET)
963

    
964

    
965
def get_backend():
966
    backend = _pithos_backend_pool.pool_get()
967
    backend.default_policy['quota'] = BACKEND_QUOTA
968
    backend.default_policy['versioning'] = BACKEND_VERSIONING
969
    backend.messages = []
970
    return backend
971

    
972

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

    
988

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

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

    
1003

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

    
1013

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

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