Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / util.py @ 06e7d2f0

History | View | Annotate | Download (39 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 datetime import datetime
36
from urllib import quote, unquote
37

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

    
46
from snf_django.lib.api.parsedate import parse_http_date_safe, parse_http_date
47
from snf_django.lib import api
48
from snf_django.lib.api import faults, utils
49

    
50
from pithos.api.settings import (BACKEND_DB_MODULE, BACKEND_DB_CONNECTION,
51
                                 BACKEND_BLOCK_MODULE, BACKEND_BLOCK_PATH,
52
                                 BACKEND_BLOCK_UMASK,
53
                                 BACKEND_QUEUE_MODULE, BACKEND_QUEUE_HOSTS,
54
                                 BACKEND_QUEUE_EXCHANGE,
55
                                 ASTAKOSCLIENT_POOLSIZE,
56
                                 SERVICE_TOKEN,
57
                                 ASTAKOS_BASE_URL,
58
                                 BACKEND_ACCOUNT_QUOTA, BACKEND_CONTAINER_QUOTA,
59
                                 BACKEND_VERSIONING,
60
                                 BACKEND_FREE_VERSIONING, BACKEND_POOL_SIZE,
61
                                 RADOS_STORAGE, RADOS_POOL_BLOCKS,
62
                                 RADOS_POOL_MAPS, TRANSLATE_UUIDS,
63
                                 PUBLIC_URL_SECURITY,
64
                                 PUBLIC_URL_ALPHABET)
65
from pithos.api.resources import resources
66
from pithos.backends.base import (NotAllowedError, QuotaError, ItemNotExists,
67
                                  VersionNotExists)
68
from astakosclient import AstakosClient
69
from astakosclient.errors import NoUserName, NoUUID
70

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

    
77
logger = logging.getLogger(__name__)
78

    
79

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

    
85

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

    
92

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

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

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

    
105

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

    
110

    
111
def get_header_prefix(request, prefix):
112
    """Get all prefix-* request headers in a dict.
113
       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('~', ''))
118
                for k, v in request.META.iteritems()
119
                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),
165
                 strings_only=True)] = smart_str(v, strings_only=True)
166

    
167

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

    
176

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

    
198

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

    
211

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

    
242

    
243
def update_manifest_meta(request, v_account, meta):
244
    """Update metadata if the object has an X-Object-Manifest."""
245

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

    
270

    
271
def is_uuid(str):
272
    if str is None:
273
        return False
274
    try:
275
        uuid.UUID(str)
276
    except ValueError:
277
        return False
278
    else:
279
        return True
280

    
281

    
282
##########################
283
# USER CATALOG utilities #
284
##########################
285

    
286
def retrieve_displayname(token, uuid, fail_silently=True):
287
    astakos = AstakosClient(ASTAKOS_BASE_URL, retry=2, use_pool=True,
288
                            logger=logger)
289
    try:
290
        displayname = astakos.get_username(token, uuid)
291
    except NoUserName:
292
        if not fail_silently:
293
            raise ItemNotExists(uuid)
294
        else:
295
            # just return the uuid
296
            return uuid
297
    return displayname
298

    
299

    
300
def retrieve_displaynames(token, uuids, return_dict=False, fail_silently=True):
301
    astakos = AstakosClient(ASTAKOS_BASE_URL, retry=2, use_pool=True,
302
                            logger=logger)
303
    catalog = astakos.get_usernames(token, uuids) or {}
304
    missing = list(set(uuids) - set(catalog))
305
    if missing and not fail_silently:
306
        raise ItemNotExists('Unknown displaynames: %s' % ', '.join(missing))
307
    return catalog if return_dict else [catalog.get(i) for i in uuids]
308

    
309

    
310
def retrieve_uuid(token, displayname):
311
    if is_uuid(displayname):
312
        return displayname
313

    
314
    astakos = AstakosClient(ASTAKOS_BASE_URL, retry=2, use_pool=True,
315
                            logger=logger)
316
    try:
317
        uuid = astakos.get_uuid(token, displayname)
318
    except NoUUID:
319
        raise ItemNotExists(displayname)
320
    return uuid
321

    
322

    
323
def retrieve_uuids(token, displaynames, return_dict=False, fail_silently=True):
324
    astakos = AstakosClient(ASTAKOS_BASE_URL, retry=2, use_pool=True,
325
                            logger=logger)
326
    catalog = astakos.get_uuids(token, displaynames) or {}
327
    missing = list(set(displaynames) - set(catalog))
328
    if missing and not fail_silently:
329
        raise ItemNotExists('Unknown uuids: %s' % ', '.join(missing))
330
    return catalog if return_dict else [catalog.get(i) for i in displaynames]
331

    
332

    
333
def replace_permissions_displayname(token, holder):
334
    if holder == '*':
335
        return holder
336
    try:
337
        # check first for a group permission
338
        account, group = holder.split(':', 1)
339
    except ValueError:
340
        return retrieve_uuid(token, holder)
341
    else:
342
        return ':'.join([retrieve_uuid(token, account), group])
343

    
344

    
345
def replace_permissions_uuid(token, holder):
346
    if holder == '*':
347
        return holder
348
    try:
349
        # check first for a group permission
350
        account, group = holder.split(':', 1)
351
    except ValueError:
352
        return retrieve_displayname(token, holder)
353
    else:
354
        return ':'.join([retrieve_displayname(token, account), group])
355

    
356

    
357
def update_sharing_meta(request, permissions, v_account,
358
                        v_container, v_object, meta):
359
    if permissions is None:
360
        return
361
    allowed, perm_path, perms = permissions
362
    if len(perms) == 0:
363
        return
364

    
365
    # replace uuid with displayname
366
    if TRANSLATE_UUIDS:
367
        perms['read'] = [replace_permissions_uuid(
368
            getattr(request, 'token', None), x)
369
            for x in perms.get('read', [])]
370
        perms['write'] = [replace_permissions_uuid(
371
            getattr(request, 'token', None), x)
372
            for x in perms.get('write', [])]
373

    
374
    ret = []
375

    
376
    r = ','.join(perms.get('read', []))
377
    if r:
378
        ret.append('read=' + r)
379
    w = ','.join(perms.get('write', []))
380
    if w:
381
        ret.append('write=' + w)
382
    meta['X-Object-Sharing'] = '; '.join(ret)
383
    if '/'.join((v_account, v_container, v_object)) != perm_path:
384
        meta['X-Object-Shared-By'] = perm_path
385
    if request.user_uniq != v_account:
386
        meta['X-Object-Allowed-To'] = allowed
387

    
388

    
389
def update_public_meta(public, meta):
390
    if not public:
391
        return
392
    meta['X-Object-Public'] = '/public/' + public
393

    
394

    
395
def validate_modification_preconditions(request, meta):
396
    """Check that the modified timestamp conforms with the preconditions set."""
397

    
398
    if 'modified' not in meta:
399
        return  # TODO: Always return?
400

    
401
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
402
    if if_modified_since is not None:
403
        if_modified_since = parse_http_date_safe(if_modified_since)
404
    if (if_modified_since is not None
405
            and int(meta['modified']) <= if_modified_since):
406
        raise faults.NotModified('Resource has not been modified')
407

    
408
    if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
409
    if if_unmodified_since is not None:
410
        if_unmodified_since = parse_http_date_safe(if_unmodified_since)
411
    if (if_unmodified_since is not None
412
            and int(meta['modified']) > if_unmodified_since):
413
        raise faults.PreconditionFailed('Resource has been modified')
414

    
415

    
416
def validate_matching_preconditions(request, meta):
417
    """Check that the ETag conforms with the preconditions set."""
418

    
419
    etag = meta['checksum']
420
    if not etag:
421
        etag = None
422

    
423
    if_match = request.META.get('HTTP_IF_MATCH')
424
    if if_match is not None:
425
        if etag is None:
426
            raise faults.PreconditionFailed('Resource does not exist')
427
        if (if_match != '*'
428
                and etag not in [x.lower() for x in parse_etags(if_match)]):
429
            raise faults.PreconditionFailed('Resource ETag does not match')
430

    
431
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
432
    if if_none_match is not None:
433
        # TODO: If this passes, must ignore If-Modified-Since header.
434
        if etag is not None:
435
            if (if_none_match == '*'
436
                    or etag in [x.lower() for x in parse_etags(if_none_match)]):
437
                # TODO: Continue if an If-Modified-Since header is present.
438
                if request.method in ('HEAD', 'GET'):
439
                    raise faults.NotModified('Resource ETag matches')
440
                raise faults.PreconditionFailed(
441
                    'Resource exists or ETag matches')
442

    
443

    
444
def split_container_object_string(s):
445
    if not len(s) > 0 or s[0] != '/':
446
        raise ValueError
447
    s = s[1:]
448
    pos = s.find('/')
449
    if pos == -1 or pos == len(s) - 1:
450
        raise ValueError
451
    return s[:pos], s[(pos + 1):]
452

    
453

    
454
def copy_or_move_object(request, src_account, src_container, src_name,
455
                        dest_account, dest_container, dest_name,
456
                        move=False, delimiter=None):
457
    """Copy or move an object."""
458

    
459
    if 'ignore_content_type' in request.GET and 'CONTENT_TYPE' in request.META:
460
        del(request.META['CONTENT_TYPE'])
461
    content_type, meta, permissions, public = get_object_headers(request)
462
    src_version = request.META.get('HTTP_X_SOURCE_VERSION')
463
    try:
464
        if move:
465
            version_id = request.backend.move_object(
466
                request.user_uniq, src_account, src_container, src_name,
467
                dest_account, dest_container, dest_name,
468
                content_type, 'pithos', meta, False, permissions, delimiter)
469
        else:
470
            version_id = request.backend.copy_object(
471
                request.user_uniq, src_account, src_container, src_name,
472
                dest_account, dest_container, dest_name,
473
                content_type, 'pithos', meta, False, permissions,
474
                src_version, delimiter)
475
    except NotAllowedError:
476
        raise faults.Forbidden('Not allowed')
477
    except (ItemNotExists, VersionNotExists):
478
        raise faults.ItemNotFound('Container or object does not exist')
479
    except ValueError:
480
        raise faults.BadRequest('Invalid sharing header')
481
    except QuotaError, e:
482
        raise faults.RequestEntityTooLarge('Quota error: %s' % e)
483
    if public is not None:
484
        try:
485
            request.backend.update_object_public(
486
                request.user_uniq, dest_account,
487
                dest_container, dest_name, public)
488
        except NotAllowedError:
489
            raise faults.Forbidden('Not allowed')
490
        except ItemNotExists:
491
            raise faults.ItemNotFound('Object does not exist')
492
    return version_id
493

    
494

    
495
def get_int_parameter(p):
496
    if p is not None:
497
        try:
498
            p = int(p)
499
        except ValueError:
500
            return None
501
        if p < 0:
502
            return None
503
    return p
504

    
505

    
506
def get_content_length(request):
507
    content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
508
    if content_length is None:
509
        raise faults.LengthRequired('Missing or invalid Content-Length header')
510
    return content_length
511

    
512

    
513
def get_range(request, size):
514
    """Parse a Range header from the request.
515

516
    Either returns None, when the header is not existent or should be ignored,
517
    or a list of (offset, length) tuples - should be further checked.
518
    """
519

    
520
    ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
521
    if not ranges.startswith('bytes='):
522
        return None
523

    
524
    ret = []
525
    for r in (x.strip() for x in ranges[6:].split(',')):
526
        p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
527
        m = p.match(r)
528
        if not m:
529
            return None
530
        offset = m.group('offset')
531
        upto = m.group('upto')
532
        if offset == '' and upto == '':
533
            return None
534

    
535
        if offset != '':
536
            offset = int(offset)
537
            if upto != '':
538
                upto = int(upto)
539
                if offset > upto:
540
                    return None
541
                ret.append((offset, upto - offset + 1))
542
            else:
543
                ret.append((offset, size - offset))
544
        else:
545
            length = int(upto)
546
            ret.append((size - length, length))
547

    
548
    return ret
549

    
550

    
551
def get_content_range(request):
552
    """Parse a Content-Range header from the request.
553

554
    Either returns None, when the header is not existent or should be ignored,
555
    or an (offset, length, total) tuple - check as length, total may be None.
556
    Returns (None, None, None) if the provided range is '*/*'.
557
    """
558

    
559
    ranges = request.META.get('HTTP_CONTENT_RANGE', '')
560
    if not ranges:
561
        return None
562

    
563
    p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
564
    m = p.match(ranges)
565
    if not m:
566
        if ranges == 'bytes */*':
567
            return (None, None, None)
568
        return None
569
    offset = int(m.group('offset'))
570
    upto = m.group('upto')
571
    total = m.group('total')
572
    if upto != '':
573
        upto = int(upto)
574
    else:
575
        upto = None
576
    if total != '*':
577
        total = int(total)
578
    else:
579
        total = None
580
    if (upto is not None and offset > upto) or \
581
        (total is not None and offset >= total) or \
582
            (total is not None and upto is not None and upto >= total):
583
        return None
584

    
585
    if upto is None:
586
        length = None
587
    else:
588
        length = upto - offset + 1
589
    return (offset, length, total)
590

    
591

    
592
def get_sharing(request):
593
    """Parse an X-Object-Sharing header from the request.
594

595
    Raises BadRequest on error.
596
    """
597

    
598
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
599
    if permissions is None:
600
        return None
601

    
602
    # TODO: Document or remove '~' replacing.
603
    permissions = permissions.replace('~', '')
604

    
605
    ret = {}
606
    permissions = permissions.replace(' ', '')
607
    if permissions == '':
608
        return ret
609
    for perm in (x for x in permissions.split(';')):
610
        if perm.startswith('read='):
611
            ret['read'] = list(set(
612
                [v.replace(' ', '').lower() for v in perm[5:].split(',')]))
613
            if '' in ret['read']:
614
                ret['read'].remove('')
615
            if '*' in ret['read']:
616
                ret['read'] = ['*']
617
            if len(ret['read']) == 0:
618
                raise faults.BadRequest(
619
                    'Bad X-Object-Sharing header value: invalid length')
620
        elif perm.startswith('write='):
621
            ret['write'] = list(set(
622
                [v.replace(' ', '').lower() for v in perm[6:].split(',')]))
623
            if '' in ret['write']:
624
                ret['write'].remove('')
625
            if '*' in ret['write']:
626
                ret['write'] = ['*']
627
            if len(ret['write']) == 0:
628
                raise faults.BadRequest(
629
                    'Bad X-Object-Sharing header value: invalid length')
630
        else:
631
            raise faults.BadRequest(
632
                'Bad X-Object-Sharing header value: missing prefix')
633

    
634
    # replace displayname with uuid
635
    if TRANSLATE_UUIDS:
636
        try:
637
            ret['read'] = [replace_permissions_displayname(
638
                getattr(request, 'token', None), x)
639
                for x in ret.get('read', [])]
640
            ret['write'] = [replace_permissions_displayname(
641
                getattr(request, 'token', None), x)
642
                for x in ret.get('write', [])]
643
        except ItemNotExists, e:
644
            raise faults.BadRequest(
645
                'Bad X-Object-Sharing header value: unknown account: %s' % e)
646

    
647
    # Keep duplicates only in write list.
648
    dups = [x for x in ret.get(
649
        'read', []) if x in ret.get('write', []) and x != '*']
650
    if dups:
651
        for x in dups:
652
            ret['read'].remove(x)
653
        if len(ret['read']) == 0:
654
            del(ret['read'])
655

    
656
    return ret
657

    
658

    
659
def get_public(request):
660
    """Parse an X-Object-Public header from the request.
661

662
    Raises BadRequest on error.
663
    """
664

    
665
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
666
    if public is None:
667
        return None
668

    
669
    public = public.replace(' ', '').lower()
670
    if public == 'true':
671
        return True
672
    elif public == 'false' or public == '':
673
        return False
674
    raise faults.BadRequest('Bad X-Object-Public header value')
675

    
676

    
677
def raw_input_socket(request):
678
    """Return the socket for reading the rest of the request."""
679

    
680
    server_software = request.META.get('SERVER_SOFTWARE')
681
    if server_software and server_software.startswith('mod_python'):
682
        return request._req
683
    if 'wsgi.input' in request.environ:
684
        return request.environ['wsgi.input']
685
    raise NotImplemented('Unknown server software')
686

    
687
MAX_UPLOAD_SIZE = 5 * (1024 * 1024 * 1024)  # 5GB
688

    
689

    
690
def socket_read_iterator(request, length=0, blocksize=4096):
691
    """Return a maximum of blocksize data read from the socket in each iteration
692

693
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
694
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
695
    """
696

    
697
    sock = raw_input_socket(request)
698
    if length < 0:  # Chunked transfers
699
        # Small version (server does the dechunking).
700
        if (request.environ.get('mod_wsgi.input_chunked', None)
701
                or request.META['SERVER_SOFTWARE'].startswith('gunicorn')):
702
            while length < MAX_UPLOAD_SIZE:
703
                data = sock.read(blocksize)
704
                if data == '':
705
                    return
706
                yield data
707
            raise faults.BadRequest('Maximum size is reached')
708

    
709
        # Long version (do the dechunking).
710
        data = ''
711
        while length < MAX_UPLOAD_SIZE:
712
            # Get chunk size.
713
            if hasattr(sock, 'readline'):
714
                chunk_length = sock.readline()
715
            else:
716
                chunk_length = ''
717
                while chunk_length[-1:] != '\n':
718
                    chunk_length += sock.read(1)
719
                chunk_length.strip()
720
            pos = chunk_length.find(';')
721
            if pos >= 0:
722
                chunk_length = chunk_length[:pos]
723
            try:
724
                chunk_length = int(chunk_length, 16)
725
            except Exception:
726
                raise faults.BadRequest('Bad chunk size')
727
                                 # TODO: Change to something more appropriate.
728
            # Check if done.
729
            if chunk_length == 0:
730
                if len(data) > 0:
731
                    yield data
732
                return
733
            # Get the actual data.
734
            while chunk_length > 0:
735
                chunk = sock.read(min(chunk_length, blocksize))
736
                chunk_length -= len(chunk)
737
                if length > 0:
738
                    length += len(chunk)
739
                data += chunk
740
                if len(data) >= blocksize:
741
                    ret = data[:blocksize]
742
                    data = data[blocksize:]
743
                    yield ret
744
            sock.read(2)  # CRLF
745
        raise faults.BadRequest('Maximum size is reached')
746
    else:
747
        if length > MAX_UPLOAD_SIZE:
748
            raise faults.BadRequest('Maximum size is reached')
749
        while length > 0:
750
            data = sock.read(min(length, blocksize))
751
            if not data:
752
                raise faults.BadRequest()
753
            length -= len(data)
754
            yield data
755

    
756

    
757
class SaveToBackendHandler(FileUploadHandler):
758
    """Handle a file from an HTML form the django way."""
759

    
760
    def __init__(self, request=None):
761
        super(SaveToBackendHandler, self).__init__(request)
762
        self.backend = request.backend
763

    
764
    def put_data(self, length):
765
        if len(self.data) >= length:
766
            block = self.data[:length]
767
            self.file.hashmap.append(self.backend.put_block(block))
768
            self.md5.update(block)
769
            self.data = self.data[length:]
770

    
771
    def new_file(self, field_name, file_name, content_type,
772
                 content_length, charset=None):
773
        self.md5 = hashlib.md5()
774
        self.data = ''
775
        self.file = UploadedFile(
776
            name=file_name, content_type=content_type, charset=charset)
777
        self.file.size = 0
778
        self.file.hashmap = []
779

    
780
    def receive_data_chunk(self, raw_data, start):
781
        self.data += raw_data
782
        self.file.size += len(raw_data)
783
        self.put_data(self.request.backend.block_size)
784
        return None
785

    
786
    def file_complete(self, file_size):
787
        l = len(self.data)
788
        if l > 0:
789
            self.put_data(l)
790
        self.file.etag = self.md5.hexdigest().lower()
791
        return self.file
792

    
793

    
794
class ObjectWrapper(object):
795
    """Return the object's data block-per-block in each iteration.
796

797
    Read from the object using the offset and length provided
798
    in each entry of the range list.
799
    """
800

    
801
    def __init__(self, backend, ranges, sizes, hashmaps, boundary):
802
        self.backend = backend
803
        self.ranges = ranges
804
        self.sizes = sizes
805
        self.hashmaps = hashmaps
806
        self.boundary = boundary
807
        self.size = sum(self.sizes)
808

    
809
        self.file_index = 0
810
        self.block_index = 0
811
        self.block_hash = -1
812
        self.block = ''
813

    
814
        self.range_index = -1
815
        self.offset, self.length = self.ranges[0]
816

    
817
    def __iter__(self):
818
        return self
819

    
820
    def part_iterator(self):
821
        if self.length > 0:
822
            # Get the file for the current offset.
823
            file_size = self.sizes[self.file_index]
824
            while self.offset >= file_size:
825
                self.offset -= file_size
826
                self.file_index += 1
827
                file_size = self.sizes[self.file_index]
828

    
829
            # Get the block for the current position.
830
            self.block_index = int(self.offset / self.backend.block_size)
831
            if self.block_hash != \
832
                    self.hashmaps[self.file_index][self.block_index]:
833
                self.block_hash = self.hashmaps[
834
                    self.file_index][self.block_index]
835
                try:
836
                    self.block = self.backend.get_block(self.block_hash)
837
                except ItemNotExists:
838
                    raise faults.ItemNotFound('Block does not exist')
839

    
840
            # Get the data from the block.
841
            bo = self.offset % self.backend.block_size
842
            bs = self.backend.block_size
843
            if (self.block_index == len(self.hashmaps[self.file_index]) - 1 and
844
                    self.sizes[self.file_index] % self.backend.block_size):
845
                bs = self.sizes[self.file_index] % self.backend.block_size
846
            bl = min(self.length, bs - bo)
847
            data = self.block[bo:bo + bl]
848
            self.offset += bl
849
            self.length -= bl
850
            return data
851
        else:
852
            raise StopIteration
853

    
854
    def next(self):
855
        if len(self.ranges) == 1:
856
            return self.part_iterator()
857
        if self.range_index == len(self.ranges):
858
            raise StopIteration
859
        try:
860
            if self.range_index == -1:
861
                raise StopIteration
862
            return self.part_iterator()
863
        except StopIteration:
864
            self.range_index += 1
865
            out = []
866
            if self.range_index < len(self.ranges):
867
                # Part header.
868
                self.offset, self.length = self.ranges[self.range_index]
869
                self.file_index = 0
870
                if self.range_index > 0:
871
                    out.append('')
872
                out.append('--' + self.boundary)
873
                out.append('Content-Range: bytes %d-%d/%d' % (
874
                    self.offset, self.offset + self.length - 1, self.size))
875
                out.append('Content-Transfer-Encoding: binary')
876
                out.append('')
877
                out.append('')
878
                return '\r\n'.join(out)
879
            else:
880
                # Footer.
881
                out.append('')
882
                out.append('--' + self.boundary + '--')
883
                out.append('')
884
                return '\r\n'.join(out)
885

    
886

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

    
890
    # Range handling.
891
    size = sum(sizes)
892
    ranges = get_range(request, size)
893
    if ranges is None:
894
        ranges = [(0, size)]
895
        ret = 200
896
    else:
897
        check = [True for offset, length in ranges if
898
                 length <= 0 or length > size or
899
                 offset < 0 or offset >= size or
900
                 offset + length > size]
901
        if len(check) > 0:
902
            raise faults.RangeNotSatisfiable(
903
                'Requested range exceeds object limits')
904
        ret = 206
905
        if_range = request.META.get('HTTP_IF_RANGE')
906
        if if_range:
907
            try:
908
                # Modification time has passed instead.
909
                last_modified = parse_http_date(if_range)
910
                if last_modified != meta['modified']:
911
                    ranges = [(0, size)]
912
                    ret = 200
913
            except ValueError:
914
                if if_range != meta['checksum']:
915
                    ranges = [(0, size)]
916
                    ret = 200
917

    
918
    if ret == 206 and len(ranges) > 1:
919
        boundary = uuid.uuid4().hex
920
    else:
921
        boundary = ''
922
    wrapper = ObjectWrapper(request.backend, ranges, sizes, hashmaps, boundary)
923
    response = HttpResponse(wrapper, status=ret)
924
    response.override_serialization = True
925
    put_object_headers(
926
        response, meta, restricted=public,
927
        token=getattr(request, 'token', None))
928
    if ret == 206:
929
        if len(ranges) == 1:
930
            offset, length = ranges[0]
931
            response[
932
                'Content-Length'] = length  # Update with the correct length.
933
            response['Content-Range'] = 'bytes %d-%d/%d' % (
934
                offset, offset + length - 1, size)
935
        else:
936
            del(response['Content-Length'])
937
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (
938
                boundary,)
939
    return response
940

    
941

    
942
def put_object_block(request, hashmap, data, offset):
943
    """Put one block of data at the given offset."""
944

    
945
    bi = int(offset / request.backend.block_size)
946
    bo = offset % request.backend.block_size
947
    bl = min(len(data), request.backend.block_size - bo)
948
    if bi < len(hashmap):
949
        hashmap[bi] = request.backend.update_block(hashmap[bi], data[:bl], bo)
950
    else:
951
        hashmap.append(request.backend.put_block(('\x00' * bo) + data[:bl]))
952
    return bl  # Return ammount of data written.
953

    
954

    
955
def hashmap_md5(backend, hashmap, size):
956
    """Produce the MD5 sum from the data in the hashmap."""
957

    
958
    # TODO: Search backend for the MD5 of another object
959
    #       with the same hashmap and size...
960
    md5 = hashlib.md5()
961
    bs = backend.block_size
962
    for bi, hash in enumerate(hashmap):
963
        data = backend.get_block(hash)  # Blocks come in padded.
964
        if bi == len(hashmap) - 1:
965
            data = data[:size % bs]
966
        md5.update(data)
967
    return md5.hexdigest().lower()
968

    
969

    
970
def simple_list_response(request, l):
971
    if request.serialization == 'text':
972
        return '\n'.join(l) + '\n'
973
    if request.serialization == 'xml':
974
        return render_to_string('items.xml', {'items': l})
975
    if request.serialization == 'json':
976
        return json.dumps(l)
977

    
978

    
979
from pithos.backends.util import PithosBackendPool
980

    
981
if RADOS_STORAGE:
982
    BLOCK_PARAMS = {'mappool': RADOS_POOL_MAPS,
983
                    'blockpool': RADOS_POOL_BLOCKS, }
984
else:
985
    BLOCK_PARAMS = {'mappool': None,
986
                    'blockpool': None, }
987

    
988

    
989
_pithos_backend_pool = PithosBackendPool(
990
        size=BACKEND_POOL_SIZE,
991
        db_module=BACKEND_DB_MODULE,
992
        db_connection=BACKEND_DB_CONNECTION,
993
        block_module=BACKEND_BLOCK_MODULE,
994
        block_path=BACKEND_BLOCK_PATH,
995
        block_umask=BACKEND_BLOCK_UMASK,
996
        queue_module=BACKEND_QUEUE_MODULE,
997
        queue_hosts=BACKEND_QUEUE_HOSTS,
998
        queue_exchange=BACKEND_QUEUE_EXCHANGE,
999
        astakos_url=ASTAKOS_BASE_URL,
1000
        service_token=SERVICE_TOKEN,
1001
        astakosclient_poolsize=ASTAKOSCLIENT_POOLSIZE,
1002
        free_versioning=BACKEND_FREE_VERSIONING,
1003
        block_params=BLOCK_PARAMS,
1004
        public_url_security=PUBLIC_URL_SECURITY,
1005
        public_url_alphabet=PUBLIC_URL_ALPHABET,
1006
        account_quota_policy=BACKEND_ACCOUNT_QUOTA,
1007
        container_quota_policy=BACKEND_CONTAINER_QUOTA,
1008
        container_versioning_policy=BACKEND_VERSIONING)
1009

    
1010

    
1011
def get_backend():
1012
    backend = _pithos_backend_pool.pool_get()
1013
    backend.messages = []
1014
    return backend
1015

    
1016

    
1017
def update_request_headers(request):
1018
    # Handle URL-encoded keys and values.
1019
    meta = dict([(
1020
        k, v) for k, v in request.META.iteritems() if k.startswith('HTTP_')])
1021
    for k, v in meta.iteritems():
1022
        try:
1023
            k.decode('ascii')
1024
            v.decode('ascii')
1025
        except UnicodeDecodeError:
1026
            raise faults.BadRequest('Bad character in headers.')
1027
        if '%' in k or '%' in v:
1028
            del(request.META[k])
1029
            request.META[unquote(k)] = smart_unicode(unquote(
1030
                v), strings_only=True)
1031

    
1032

    
1033
def update_response_headers(request, response):
1034
    if (not response.has_header('Content-Length') and
1035
        not (response.has_header('Content-Type') and
1036
             response['Content-Type'].startswith('multipart/byteranges'))):
1037
        response['Content-Length'] = len(response.content)
1038

    
1039
    # URL-encode unicode in headers.
1040
    meta = response.items()
1041
    for k, v in meta:
1042
        if (k.startswith('X-Account-') or k.startswith('X-Container-') or
1043
                k.startswith('X-Object-') or k.startswith('Content-')):
1044
            del(response[k])
1045
            response[quote(k)] = quote(v, safe='/=,:@; ')
1046

    
1047

    
1048
def get_pithos_usage(token):
1049
    """Get Pithos Usage from astakos."""
1050
    astakos = AstakosClient(ASTAKOS_BASE_URL, retry=2, use_pool=True,
1051
                            logger=logger)
1052
    quotas = astakos.get_quotas(token)['system']
1053
    pithos_resources = [r['name'] for r in resources]
1054
    map(quotas.pop, filter(lambda k: k not in pithos_resources, quotas.keys()))
1055
    return quotas.popitem()[-1] # assume only one resource
1056

    
1057

    
1058
def api_method(http_method=None, user_required=True, logger=None,
1059
               format_allowed=False, default_serialization="json"):
1060
    def decorator(func):
1061
        @api.api_method(http_method=http_method, user_required=user_required,
1062
                        logger=logger, format_allowed=format_allowed,
1063
                        astakos_url=ASTAKOS_BASE_URL,
1064
                        default_serialization=default_serialization)
1065
        @wraps(func)
1066
        def wrapper(request, *args, **kwargs):
1067
            # The args variable may contain up to (account, container, object).
1068
            if len(args) > 1 and len(args[1]) > 256:
1069
                raise faults.BadRequest("Container name too large")
1070
            if len(args) > 2 and len(args[2]) > 1024:
1071
                raise faults.BadRequest('Object name too large.')
1072

    
1073
            try:
1074
                # Add a PithosBackend as attribute of the request object
1075
                request.backend = get_backend()
1076
                # Many API method expect thet X-Auth-Token in request,token
1077
                request.token = request.x_auth_token
1078
                update_request_headers(request)
1079
                response = func(request, *args, **kwargs)
1080
                update_response_headers(request, response)
1081
                return response
1082
            finally:
1083
                # Always close PithosBackend connection
1084
                if getattr(request, "backend", None) is not None:
1085
                    request.backend.close()
1086
        return wrapper
1087
    return decorator