Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / util.py @ 4ab620b6

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
                                 BACKEND_QUOTA, BACKEND_VERSIONING,
61
                                 BACKEND_FREE_VERSIONING, BACKEND_POOL_SIZE,
62
                                 COOKIE_NAME, USER_CATALOG_URL,
63
                                 RADOS_STORAGE, RADOS_POOL_BLOCKS,
64
                                 RADOS_POOL_MAPS, TRANSLATE_UUIDS,
65
                                 PUBLIC_URL_SECURITY,
66
                                 PUBLIC_URL_ALPHABET)
67
from pithos.backends.base import (NotAllowedError, QuotaError, ItemNotExists,
68
                                  VersionNotExists)
69
from snf_django.lib.astakos import (get_user_uuid, get_displayname,
70
                                 get_uuids, get_displaynames)
71

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

    
78
logger = logging.getLogger(__name__)
79

    
80

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

    
86

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

    
93

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

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

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

    
106

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

    
111

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

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

    
119

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

    
129

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

    
143

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

    
164

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

    
171

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

    
192

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

    
205

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

    
235

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

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

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

    
271
##########################
272
# USER CATALOG utilities #
273
##########################
274

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

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

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

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

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

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

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

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

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

    
345
    ret = []
346

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

    
359

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

    
365

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

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

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

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

    
384

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

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

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

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

    
409

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

    
419

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

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

    
455

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

    
466

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

    
473

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

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

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

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

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

    
509
    return ret
510

    
511

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

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

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

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

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

    
552

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

556
    Raises BadRequest on error.
557
    """
558

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

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

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

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

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

    
617
    return ret
618

    
619

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

623
    Raises BadRequest on error.
624
    """
625

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

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

    
637

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

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

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

    
650

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

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

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

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

    
716

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

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

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

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

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

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

    
752

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

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

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

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

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

    
775
    def __iter__(self):
776
        return self
777

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

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

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

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

    
843

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

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

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

    
895

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

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

    
908

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

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

    
922

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

    
931

    
932
from pithos.backends.util import PithosBackendPool
933

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

    
943

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

    
963

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

    
971

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

    
987

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

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

    
1002

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

    
1012

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

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