Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / util.py @ 4319c408

History | View | Annotate | Download (38.7 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 time import time
36
from traceback import format_exc
37
from wsgiref.handlers import format_date_time
38
from binascii import hexlify, unhexlify
39
from datetime import datetime, tzinfo, timedelta
40
from urllib import quote, unquote
41

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

    
51
from synnefo.lib.parsedate import parse_http_date_safe, parse_http_date
52
from synnefo.lib.astakos import get_user
53

    
54
from pithos.api.faults import (
55
    Fault, NotModified, BadRequest, Unauthorized, Forbidden, ItemNotFound,
56
    Conflict, LengthRequired, PreconditionFailed, RequestEntityTooLarge,
57
    RangeNotSatisfiable, InternalServerError, NotImplemented)
58
from pithos.api.short_url import encode_url
59
from pithos.api.settings import (BACKEND_DB_MODULE, BACKEND_DB_CONNECTION,
60
                                 BACKEND_BLOCK_MODULE, BACKEND_BLOCK_PATH,
61
                                 BACKEND_BLOCK_UMASK,
62
                                 BACKEND_QUEUE_MODULE, BACKEND_QUEUE_HOSTS,
63
                                 BACKEND_QUEUE_EXCHANGE,
64
                                 QUOTAHOLDER_URL, QUOTAHOLDER_TOKEN,
65
                                 BACKEND_QUOTA, BACKEND_VERSIONING,
66
                                 BACKEND_FREE_VERSIONING,
67
                                 AUTHENTICATION_URL, AUTHENTICATION_USERS,
68
                                 SERVICE_TOKEN, COOKIE_NAME, USER_INFO_URL)
69
from pithos.backends import connect_backend
70
from pithos.backends.base import (NotAllowedError, QuotaError, ItemNotExists,
71
                                  VersionNotExists)
72
from synnefo.lib.astakos import get_user_uuid, get_username
73

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

    
80

    
81
logger = logging.getLogger(__name__)
82

    
83

    
84
class UTC(tzinfo):
85
    def utcoffset(self, dt):
86
        return timedelta(0)
87

    
88
    def tzname(self, dt):
89
        return 'UTC'
90

    
91
    def dst(self, dt):
92
        return timedelta(0)
93

    
94

    
95
def json_encode_decimal(obj):
96
    if isinstance(obj, decimal.Decimal):
97
        return str(obj)
98
    raise TypeError(repr(obj) + " is not JSON serializable")
99

    
100

    
101
def isoformat(d):
102
    """Return an ISO8601 date string that includes a timezone."""
103

    
104
    return d.replace(tzinfo=UTC()).isoformat()
105

    
106

    
107
def rename_meta_key(d, old, new):
108
    if old not in d:
109
        return
110
    d[new] = d[old]
111
    del(d[old])
112

    
113

    
114
def printable_header_dict(d):
115
    """Format a meta dictionary for printing out json/xml.
116

117
    Convert all keys to lower case and replace dashes with underscores.
118
    Format 'last_modified' timestamp.
119
    """
120

    
121
    if 'last_modified' in d and d['last_modified']:
122
        d['last_modified'] = isoformat(
123
            datetime.fromtimestamp(d['last_modified']))
124
    return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
125

    
126

    
127
def format_header_key(k):
128
    """Convert underscores to dashes and capitalize intra-dash strings."""
129
    return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
130

    
131

    
132
def get_header_prefix(request, prefix):
133
    """Get all prefix-* request headers in a dict. Reformat keys with format_header_key()."""
134

    
135
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
136
    # TODO: Document or remove '~' replacing.
137
    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)])
138

    
139

    
140
def check_meta_headers(meta):
141
    if len(meta) > 90:
142
        raise BadRequest('Too many headers.')
143
    for k, v in meta.iteritems():
144
        if len(k) > 128:
145
            raise BadRequest('Header name too large.')
146
        if len(v) > 256:
147
            raise BadRequest('Header value too large.')
148

    
149

    
150
def get_account_headers(request):
151
    meta = get_header_prefix(request, 'X-Account-Meta-')
152
    check_meta_headers(meta)
153
    groups = {}
154
    for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
155
        n = k[16:].lower()
156
        if '-' in n or '_' in n:
157
            raise BadRequest('Bad characters in group name')
158
        groups[n] = v.replace(' ', '').split(',')
159
        while '' in groups[n]:
160
            groups[n].remove('')
161
    return meta, groups
162

    
163

    
164
def put_account_headers(response, meta, groups, policy):
165
    if 'count' in meta:
166
        response['X-Account-Container-Count'] = meta['count']
167
    if 'bytes' in meta:
168
        response['X-Account-Bytes-Used'] = meta['bytes']
169
    response['Last-Modified'] = http_date(int(meta['modified']))
170
    for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
171
        response[smart_str(
172
            k, strings_only=True)] = smart_str(meta[k], strings_only=True)
173
    if 'until_timestamp' in meta:
174
        response['X-Account-Until-Timestamp'] = http_date(
175
            int(meta['until_timestamp']))
176
    for k, v in groups.iteritems():
177
        k = smart_str(k, strings_only=True)
178
        k = format_header_key('X-Account-Group-' + k)
179
        v = smart_str(','.join(v), strings_only=True)
180
        response[k] = v
181
    for k, v in policy.iteritems():
182
        response[smart_str(format_header_key('X-Account-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
183

    
184

    
185
def get_container_headers(request):
186
    meta = get_header_prefix(request, 'X-Container-Meta-')
187
    check_meta_headers(meta)
188
    policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in get_header_prefix(request, 'X-Container-Policy-').iteritems()])
189
    return meta, policy
190

    
191

    
192
def put_container_headers(request, response, meta, policy):
193
    if 'count' in meta:
194
        response['X-Container-Object-Count'] = meta['count']
195
    if 'bytes' in meta:
196
        response['X-Container-Bytes-Used'] = meta['bytes']
197
    response['Last-Modified'] = http_date(int(meta['modified']))
198
    for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
199
        response[smart_str(
200
            k, strings_only=True)] = smart_str(meta[k], strings_only=True)
201
    l = [smart_str(x, strings_only=True) for x in meta['object_meta']
202
         if x.startswith('X-Object-Meta-')]
203
    response['X-Container-Object-Meta'] = ','.join([x[14:] for x in l])
204
    response['X-Container-Block-Size'] = request.backend.block_size
205
    response['X-Container-Block-Hash'] = request.backend.hash_algorithm
206
    if 'until_timestamp' in meta:
207
        response['X-Container-Until-Timestamp'] = http_date(
208
            int(meta['until_timestamp']))
209
    for k, v in policy.iteritems():
210
        response[smart_str(format_header_key('X-Container-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
211

    
212

    
213
def get_object_headers(request):
214
    content_type = request.META.get('CONTENT_TYPE', None)
215
    meta = get_header_prefix(request, 'X-Object-Meta-')
216
    check_meta_headers(meta)
217
    if request.META.get('HTTP_CONTENT_ENCODING'):
218
        meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
219
    if request.META.get('HTTP_CONTENT_DISPOSITION'):
220
        meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
221
    if request.META.get('HTTP_X_OBJECT_MANIFEST'):
222
        meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
223
    return content_type, meta, get_sharing(request), get_public(request)
224

    
225

    
226
def put_object_headers(response, meta, restricted=False):
227
    response['ETag'] = meta['checksum']
228
    response['Content-Length'] = meta['bytes']
229
    response['Content-Type'] = meta.get('type', 'application/octet-stream')
230
    response['Last-Modified'] = http_date(int(meta['modified']))
231
    if not restricted:
232
        response['X-Object-Hash'] = meta['hash']
233
        response['X-Object-UUID'] = meta['uuid']
234
        response['X-Object-Modified-By'] = smart_str(
235
            meta['modified_by'], strings_only=True)
236
        response['X-Object-Version'] = meta['version']
237
        response['X-Object-Version-Timestamp'] = http_date(
238
            int(meta['version_timestamp']))
239
        for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
240
            response[smart_str(
241
                k, strings_only=True)] = smart_str(meta[k], strings_only=True)
242
        for k in (
243
            'Content-Encoding', 'Content-Disposition', 'X-Object-Manifest',
244
            'X-Object-Sharing', 'X-Object-Shared-By', 'X-Object-Allowed-To',
245
                'X-Object-Public'):
246
            if k in meta:
247
                response[k] = smart_str(meta[k], strings_only=True)
248
    else:
249
        for k in ('Content-Encoding', 'Content-Disposition'):
250
            if k in meta:
251
                response[k] = smart_str(meta[k], strings_only=True)
252

    
253

    
254
def update_manifest_meta(request, v_account, meta):
255
    """Update metadata if the object has an X-Object-Manifest."""
256

    
257
    if 'X-Object-Manifest' in meta:
258
        etag = ''
259
        bytes = 0
260
        try:
261
            src_container, src_name = split_container_object_string(
262
                '/' + meta['X-Object-Manifest'])
263
            objects = request.backend.list_objects(
264
                request.user_uniq, v_account,
265
                src_container, prefix=src_name, virtual=False)
266
            for x in objects:
267
                src_meta = request.backend.get_object_meta(request.user_uniq,
268
                                                           v_account, src_container, x[0], 'pithos', x[1])
269
                etag += src_meta['checksum']
270
                bytes += src_meta['bytes']
271
        except:
272
            # Ignore errors.
273
            return
274
        meta['bytes'] = bytes
275
        md5 = hashlib.md5()
276
        md5.update(etag)
277
        meta['checksum'] = md5.hexdigest().lower()
278

    
279
def is_uuid(str):
280
    try:
281
        uuid.UUID(str)
282
    except ValueError:
283
       return False
284
    else:
285
       return True
286

    
287
def retrieve_username(uuid):
288
    try:
289
        return get_username(
290
            SERVICE_TOKEN, uuid, USER_INFO_URL, AUTHENTICATION_USERS)
291
    except:
292
        # if it fails just leave the metadata intact
293
        return uuid
294

    
295
def retrieve_uuid(username):
296
    if is_uuid(username):
297
        return username
298
    try:
299
        return get_user_uuid(
300
            SERVICE_TOKEN, username, USER_INFO_URL, AUTHENTICATION_USERS)
301
    except Exception, e:
302
        if e.args:
303
            status = e.args[-1]
304
            if status == 404:
305
                raise ItemNotExists(username)
306
        raise
307

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

    
317
def replace_permissions_uuid(holder):
318
    try:
319
        # check first for a group permission
320
        account, group = holder.split(':')
321
    except ValueError:
322
        return retrieve_username(holder)
323
    else:
324
        return ':'.join([retrieve_username(account), group])
325

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

    
333
    perms['read'] = [replace_permissions_uuid(x) for x in perms.get('read', [])]
334
    perms['write'] = \
335
        [replace_permissions_uuid(x) for x in perms.get('write', [])]
336

    
337
    ret = []
338

    
339
    r = ','.join(perms.get('read', []))
340
    if r:
341
        ret.append('read=' + r)
342
    w = ','.join(perms.get('write', []))
343
    if w:
344
        ret.append('write=' + w)
345
    meta['X-Object-Sharing'] = '; '.join(ret)
346
    if '/'.join((v_account, v_container, v_object)) != perm_path:
347
        meta['X-Object-Shared-By'] = perm_path
348
    if request.user_uniq != v_account:
349
        meta['X-Object-Allowed-To'] = allowed
350

    
351

    
352
def update_public_meta(public, meta):
353
    if not public:
354
        return
355
    meta['X-Object-Public'] = '/public/' + encode_url(public)
356

    
357

    
358
def validate_modification_preconditions(request, meta):
359
    """Check that the modified timestamp conforms with the preconditions set."""
360

    
361
    if 'modified' not in meta:
362
        return  # TODO: Always return?
363

    
364
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
365
    if if_modified_since is not None:
366
        if_modified_since = parse_http_date_safe(if_modified_since)
367
    if if_modified_since is not None and int(meta['modified']) <= if_modified_since:
368
        raise NotModified('Resource has not been modified')
369

    
370
    if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
371
    if if_unmodified_since is not None:
372
        if_unmodified_since = parse_http_date_safe(if_unmodified_since)
373
    if if_unmodified_since is not None and int(meta['modified']) > if_unmodified_since:
374
        raise PreconditionFailed('Resource has been modified')
375

    
376

    
377
def validate_matching_preconditions(request, meta):
378
    """Check that the ETag conforms with the preconditions set."""
379

    
380
    etag = meta['checksum']
381
    if not etag:
382
        etag = None
383

    
384
    if_match = request.META.get('HTTP_IF_MATCH')
385
    if if_match is not None:
386
        if etag is None:
387
            raise PreconditionFailed('Resource does not exist')
388
        if if_match != '*' and etag not in [x.lower() for x in parse_etags(if_match)]:
389
            raise PreconditionFailed('Resource ETag does not match')
390

    
391
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
392
    if if_none_match is not None:
393
        # TODO: If this passes, must ignore If-Modified-Since header.
394
        if etag is not None:
395
            if if_none_match == '*' or etag in [x.lower() for x in parse_etags(if_none_match)]:
396
                # TODO: Continue if an If-Modified-Since header is present.
397
                if request.method in ('HEAD', 'GET'):
398
                    raise NotModified('Resource ETag matches')
399
                raise PreconditionFailed('Resource exists or ETag matches')
400

    
401

    
402
def split_container_object_string(s):
403
    if not len(s) > 0 or s[0] != '/':
404
        raise ValueError
405
    s = s[1:]
406
    pos = s.find('/')
407
    if pos == -1 or pos == len(s) - 1:
408
        raise ValueError
409
    return s[:pos], s[(pos + 1):]
410

    
411

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

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

    
447

    
448
def get_int_parameter(p):
449
    if p is not None:
450
        try:
451
            p = int(p)
452
        except ValueError:
453
            return None
454
        if p < 0:
455
            return None
456
    return p
457

    
458

    
459
def get_content_length(request):
460
    content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
461
    if content_length is None:
462
        raise LengthRequired('Missing or invalid Content-Length header')
463
    return content_length
464

    
465

    
466
def get_range(request, size):
467
    """Parse a Range header from the request.
468

469
    Either returns None, when the header is not existent or should be ignored,
470
    or a list of (offset, length) tuples - should be further checked.
471
    """
472

    
473
    ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
474
    if not ranges.startswith('bytes='):
475
        return None
476

    
477
    ret = []
478
    for r in (x.strip() for x in ranges[6:].split(',')):
479
        p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
480
        m = p.match(r)
481
        if not m:
482
            return None
483
        offset = m.group('offset')
484
        upto = m.group('upto')
485
        if offset == '' and upto == '':
486
            return None
487

    
488
        if offset != '':
489
            offset = int(offset)
490
            if upto != '':
491
                upto = int(upto)
492
                if offset > upto:
493
                    return None
494
                ret.append((offset, upto - offset + 1))
495
            else:
496
                ret.append((offset, size - offset))
497
        else:
498
            length = int(upto)
499
            ret.append((size - length, length))
500

    
501
    return ret
502

    
503

    
504
def get_content_range(request):
505
    """Parse a Content-Range header from the request.
506

507
    Either returns None, when the header is not existent or should be ignored,
508
    or an (offset, length, total) tuple - check as length, total may be None.
509
    Returns (None, None, None) if the provided range is '*/*'.
510
    """
511

    
512
    ranges = request.META.get('HTTP_CONTENT_RANGE', '')
513
    if not ranges:
514
        return None
515

    
516
    p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
517
    m = p.match(ranges)
518
    if not m:
519
        if ranges == 'bytes */*':
520
            return (None, None, None)
521
        return None
522
    offset = int(m.group('offset'))
523
    upto = m.group('upto')
524
    total = m.group('total')
525
    if upto != '':
526
        upto = int(upto)
527
    else:
528
        upto = None
529
    if total != '*':
530
        total = int(total)
531
    else:
532
        total = None
533
    if (upto is not None and offset > upto) or \
534
        (total is not None and offset >= total) or \
535
            (total is not None and upto is not None and upto >= total):
536
        return None
537

    
538
    if upto is None:
539
        length = None
540
    else:
541
        length = upto - offset + 1
542
    return (offset, length, total)
543

    
544

    
545
def get_sharing(request):
546
    """Parse an X-Object-Sharing header from the request.
547

548
    Raises BadRequest on error.
549
    """
550

    
551
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
552
    if permissions is None:
553
        return None
554

    
555
    # TODO: Document or remove '~' replacing.
556
    permissions = permissions.replace('~', '')
557

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

    
587
    # replace username with uuid
588
    try:
589
        ret['read'] = \
590
            [replace_permissions_username(x) for x in ret.get('read', [])]
591
        ret['write'] = \
592
            [replace_permissions_username(x) for x in ret.get('write', [])]
593
    except ItemNotExists, e:
594
        raise BadRequest(
595
            'Bad X-Object-Sharing header value: unknown account: %s' % e)
596

    
597
    # Keep duplicates only in write list.
598
    dups = [x for x in ret.get(
599
        'read', []) if x in ret.get('write', []) and x != '*']
600
    if dups:
601
        for x in dups:
602
            ret['read'].remove(x)
603
        if len(ret['read']) == 0:
604
            del(ret['read'])
605

    
606
    return ret
607

    
608

    
609
def get_public(request):
610
    """Parse an X-Object-Public header from the request.
611

612
    Raises BadRequest on error.
613
    """
614

    
615
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
616
    if public is None:
617
        return None
618

    
619
    public = public.replace(' ', '').lower()
620
    if public == 'true':
621
        return True
622
    elif public == 'false' or public == '':
623
        return False
624
    raise BadRequest('Bad X-Object-Public header value')
625

    
626

    
627
def raw_input_socket(request):
628
    """Return the socket for reading the rest of the request."""
629

    
630
    server_software = request.META.get('SERVER_SOFTWARE')
631
    if server_software and server_software.startswith('mod_python'):
632
        return request._req
633
    if 'wsgi.input' in request.environ:
634
        return request.environ['wsgi.input']
635
    raise NotImplemented('Unknown server software')
636

    
637
MAX_UPLOAD_SIZE = 5 * (1024 * 1024 * 1024)  # 5GB
638

    
639

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

643
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
644
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
645
    """
646

    
647
    sock = raw_input_socket(request)
648
    if length < 0:  # Chunked transfers
649
        # Small version (server does the dechunking).
650
        if request.environ.get('mod_wsgi.input_chunked', None) or request.META['SERVER_SOFTWARE'].startswith('gunicorn'):
651
            while length < MAX_UPLOAD_SIZE:
652
                data = sock.read(blocksize)
653
                if data == '':
654
                    return
655
                yield data
656
            raise BadRequest('Maximum size is reached')
657

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

    
705

    
706
class SaveToBackendHandler(FileUploadHandler):
707
    """Handle a file from an HTML form the django way."""
708

    
709
    def __init__(self, request=None):
710
        super(SaveToBackendHandler, self).__init__(request)
711
        self.backend = request.backend
712

    
713
    def put_data(self, length):
714
        if len(self.data) >= length:
715
            block = self.data[:length]
716
            self.file.hashmap.append(self.backend.put_block(block))
717
            self.md5.update(block)
718
            self.data = self.data[length:]
719

    
720
    def new_file(self, field_name, file_name, content_type, content_length, charset=None):
721
        self.md5 = hashlib.md5()
722
        self.data = ''
723
        self.file = UploadedFile(
724
            name=file_name, content_type=content_type, charset=charset)
725
        self.file.size = 0
726
        self.file.hashmap = []
727

    
728
    def receive_data_chunk(self, raw_data, start):
729
        self.data += raw_data
730
        self.file.size += len(raw_data)
731
        self.put_data(self.request.backend.block_size)
732
        return None
733

    
734
    def file_complete(self, file_size):
735
        l = len(self.data)
736
        if l > 0:
737
            self.put_data(l)
738
        self.file.etag = self.md5.hexdigest().lower()
739
        return self.file
740

    
741

    
742
class ObjectWrapper(object):
743
    """Return the object's data block-per-block in each iteration.
744

745
    Read from the object using the offset and length provided in each entry of the range list.
746
    """
747

    
748
    def __init__(self, backend, ranges, sizes, hashmaps, boundary):
749
        self.backend = backend
750
        self.ranges = ranges
751
        self.sizes = sizes
752
        self.hashmaps = hashmaps
753
        self.boundary = boundary
754
        self.size = sum(self.sizes)
755

    
756
        self.file_index = 0
757
        self.block_index = 0
758
        self.block_hash = -1
759
        self.block = ''
760

    
761
        self.range_index = -1
762
        self.offset, self.length = self.ranges[0]
763

    
764
    def __iter__(self):
765
        return self
766

    
767
    def part_iterator(self):
768
        if self.length > 0:
769
            # Get the file for the current offset.
770
            file_size = self.sizes[self.file_index]
771
            while self.offset >= file_size:
772
                self.offset -= file_size
773
                self.file_index += 1
774
                file_size = self.sizes[self.file_index]
775

    
776
            # Get the block for the current position.
777
            self.block_index = int(self.offset / self.backend.block_size)
778
            if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
779
                self.block_hash = self.hashmaps[
780
                    self.file_index][self.block_index]
781
                try:
782
                    self.block = self.backend.get_block(self.block_hash)
783
                except ItemNotExists:
784
                    raise ItemNotFound('Block does not exist')
785

    
786
            # Get the data from the block.
787
            bo = self.offset % self.backend.block_size
788
            bs = self.backend.block_size
789
            if (self.block_index == len(self.hashmaps[self.file_index]) - 1 and
790
                    self.sizes[self.file_index] % self.backend.block_size):
791
                bs = self.sizes[self.file_index] % self.backend.block_size
792
            bl = min(self.length, bs - bo)
793
            data = self.block[bo:bo + bl]
794
            self.offset += bl
795
            self.length -= bl
796
            return data
797
        else:
798
            raise StopIteration
799

    
800
    def next(self):
801
        if len(self.ranges) == 1:
802
            return self.part_iterator()
803
        if self.range_index == len(self.ranges):
804
            raise StopIteration
805
        try:
806
            if self.range_index == -1:
807
                raise StopIteration
808
            return self.part_iterator()
809
        except StopIteration:
810
            self.range_index += 1
811
            out = []
812
            if self.range_index < len(self.ranges):
813
                # Part header.
814
                self.offset, self.length = self.ranges[self.range_index]
815
                self.file_index = 0
816
                if self.range_index > 0:
817
                    out.append('')
818
                out.append('--' + self.boundary)
819
                out.append('Content-Range: bytes %d-%d/%d' % (
820
                    self.offset, self.offset + self.length - 1, self.size))
821
                out.append('Content-Transfer-Encoding: binary')
822
                out.append('')
823
                out.append('')
824
                return '\r\n'.join(out)
825
            else:
826
                # Footer.
827
                out.append('')
828
                out.append('--' + self.boundary + '--')
829
                out.append('')
830
                return '\r\n'.join(out)
831

    
832

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

    
836
    # Range handling.
837
    size = sum(sizes)
838
    ranges = get_range(request, size)
839
    if ranges is None:
840
        ranges = [(0, size)]
841
        ret = 200
842
    else:
843
        check = [True for offset, length in ranges if
844
                 length <= 0 or length > size or
845
                 offset < 0 or offset >= size or
846
                 offset + length > size]
847
        if len(check) > 0:
848
            raise RangeNotSatisfiable('Requested range exceeds object limits')
849
        ret = 206
850
        if_range = request.META.get('HTTP_IF_RANGE')
851
        if if_range:
852
            try:
853
                # Modification time has passed instead.
854
                last_modified = parse_http_date(if_range)
855
                if last_modified != meta['modified']:
856
                    ranges = [(0, size)]
857
                    ret = 200
858
            except ValueError:
859
                if if_range != meta['checksum']:
860
                    ranges = [(0, size)]
861
                    ret = 200
862

    
863
    if ret == 206 and len(ranges) > 1:
864
        boundary = uuid.uuid4().hex
865
    else:
866
        boundary = ''
867
    wrapper = ObjectWrapper(request.backend, ranges, sizes, hashmaps, boundary)
868
    response = HttpResponse(wrapper, status=ret)
869
    put_object_headers(response, meta, public)
870
    if ret == 206:
871
        if len(ranges) == 1:
872
            offset, length = ranges[0]
873
            response[
874
                'Content-Length'] = length  # Update with the correct length.
875
            response['Content-Range'] = 'bytes %d-%d/%d' % (
876
                offset, offset + length - 1, size)
877
        else:
878
            del(response['Content-Length'])
879
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (
880
                boundary,)
881
    return response
882

    
883

    
884
def put_object_block(request, hashmap, data, offset):
885
    """Put one block of data at the given offset."""
886

    
887
    bi = int(offset / request.backend.block_size)
888
    bo = offset % request.backend.block_size
889
    bl = min(len(data), request.backend.block_size - bo)
890
    if bi < len(hashmap):
891
        hashmap[bi] = request.backend.update_block(hashmap[bi], data[:bl], bo)
892
    else:
893
        hashmap.append(request.backend.put_block(('\x00' * bo) + data[:bl]))
894
    return bl  # Return ammount of data written.
895

    
896

    
897
def hashmap_md5(backend, hashmap, size):
898
    """Produce the MD5 sum from the data in the hashmap."""
899

    
900
    # TODO: Search backend for the MD5 of another object with the same hashmap and size...
901
    md5 = hashlib.md5()
902
    bs = backend.block_size
903
    for bi, hash in enumerate(hashmap):
904
        data = backend.get_block(hash)  # Blocks come in padded.
905
        if bi == len(hashmap) - 1:
906
            data = data[:size % bs]
907
        md5.update(data)
908
    return md5.hexdigest().lower()
909

    
910

    
911
def simple_list_response(request, l):
912
    if request.serialization == 'text':
913
        return '\n'.join(l) + '\n'
914
    if request.serialization == 'xml':
915
        return render_to_string('items.xml', {'items': l})
916
    if request.serialization == 'json':
917
        return json.dumps(l)
918

    
919

    
920
from pithos.backends.util import PithosBackendPool
921
POOL_SIZE = 5
922

    
923

    
924
_pithos_backend_pool = PithosBackendPool(size=POOL_SIZE,
925
                                         db_module=BACKEND_DB_MODULE,
926
                                         db_connection=BACKEND_DB_CONNECTION,
927
                                         block_module=BACKEND_BLOCK_MODULE,
928
                                         block_path=BACKEND_BLOCK_PATH,
929
                                         block_umask=BACKEND_BLOCK_UMASK,
930
                                         queue_module=BACKEND_QUEUE_MODULE,
931
                                         queue_hosts=BACKEND_QUEUE_HOSTS,
932
                                         queue_exchange=BACKEND_QUEUE_EXCHANGE,
933
                                         quotaholder_url=QUOTAHOLDER_URL,
934
                                         quotaholder_token=QUOTAHOLDER_TOKEN,
935
                                         free_versioning=BACKEND_FREE_VERSIONING)
936

    
937

    
938
def get_backend():
939
    backend = _pithos_backend_pool.pool_get()
940
    backend.default_policy['quota'] = BACKEND_QUOTA
941
    backend.default_policy['versioning'] = BACKEND_VERSIONING
942
    backend.messages = []
943
    return backend
944

    
945

    
946
def update_request_headers(request):
947
    # Handle URL-encoded keys and values.
948
    meta = dict([(
949
        k, v) for k, v in request.META.iteritems() if k.startswith('HTTP_')])
950
    for k, v in meta.iteritems():
951
        try:
952
            k.decode('ascii')
953
            v.decode('ascii')
954
        except UnicodeDecodeError:
955
            raise BadRequest('Bad character in headers.')
956
        if '%' in k or '%' in v:
957
            del(request.META[k])
958
            request.META[unquote(k)] = smart_unicode(unquote(
959
                v), strings_only=True)
960

    
961

    
962
def update_response_headers(request, response):
963
    if request.serialization == 'xml':
964
        response['Content-Type'] = 'application/xml; charset=UTF-8'
965
    elif request.serialization == 'json':
966
        response['Content-Type'] = 'application/json; charset=UTF-8'
967
    elif not response['Content-Type']:
968
        response['Content-Type'] = 'text/plain; charset=UTF-8'
969

    
970
    if (not response.has_header('Content-Length') and
971
        not (response.has_header('Content-Type') and
972
             response['Content-Type'].startswith('multipart/byteranges'))):
973
        response['Content-Length'] = len(response.content)
974

    
975
    # URL-encode unicode in headers.
976
    meta = response.items()
977
    for k, v in meta:
978
        if (k.startswith('X-Account-') or k.startswith('X-Container-') or
979
                k.startswith('X-Object-') or k.startswith('Content-')):
980
            del(response[k])
981
            response[quote(k)] = quote(v, safe='/=,:@; ')
982

    
983

    
984
def render_fault(request, fault):
985
    if isinstance(fault, InternalServerError) and settings.DEBUG:
986
        fault.details = format_exc(fault)
987

    
988
    request.serialization = 'text'
989
    data = fault.message + '\n'
990
    if fault.details:
991
        data += '\n' + fault.details
992
    response = HttpResponse(data, status=fault.code)
993
    update_response_headers(request, response)
994
    return response
995

    
996

    
997
def request_serialization(request, format_allowed=False):
998
    """Return the serialization format requested.
999

1000
    Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
1001
    """
1002

    
1003
    if not format_allowed:
1004
        return 'text'
1005

    
1006
    format = request.GET.get('format')
1007
    if format == 'json':
1008
        return 'json'
1009
    elif format == 'xml':
1010
        return 'xml'
1011

    
1012
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
1013
        accept, sep, rest = item.strip().partition(';')
1014
        if accept == 'application/json':
1015
            return 'json'
1016
        elif accept == 'application/xml' or accept == 'text/xml':
1017
            return 'xml'
1018

    
1019
    return 'text'
1020

    
1021
class User(unicode):
1022
    pass
1023

    
1024
def api_method(http_method=None, format_allowed=False, user_required=True):
1025
    """Decorator function for views that implement an API method."""
1026

    
1027
    def decorator(func):
1028
        @wraps(func)
1029
        def wrapper(request, *args, **kwargs):
1030
            try:
1031
                if http_method and request.method != http_method:
1032
                    raise BadRequest('Method not allowed.')
1033

    
1034
                if user_required:
1035
                    token = None
1036
                    if request.method in ('HEAD', 'GET') and COOKIE_NAME in request.COOKIES:
1037
                        cookie_value = unquote(
1038
                            request.COOKIES.get(COOKIE_NAME, ''))
1039
                        account, sep, token = cookie_value.partition('|')
1040
                    get_user(request,
1041
                             AUTHENTICATION_URL, AUTHENTICATION_USERS, token)
1042
                    if  getattr(request, 'user', None) is None:
1043
                        raise Unauthorized('Access denied')
1044
                    assert getattr(request, 'user_uniq', None) != None
1045
                    request.user_uniq = User(request.user_uniq)
1046
                    request.user_uniq.uuid = request.user.get('uuid')
1047
                
1048
                # The args variable may contain up to (account, container, object).
1049
                if len(args) > 1 and len(args[1]) > 256:
1050
                    raise BadRequest('Container name too large.')
1051
                if len(args) > 2 and len(args[2]) > 1024:
1052
                    raise BadRequest('Object name too large.')
1053

    
1054
                # Format and check headers.
1055
                update_request_headers(request)
1056

    
1057
                # Fill in custom request variables.
1058
                request.serialization = request_serialization(
1059
                    request, format_allowed)
1060
                request.backend = get_backend()
1061

    
1062
                response = func(request, *args, **kwargs)
1063
                update_response_headers(request, response)
1064
                return response
1065
            except Fault, fault:
1066
                if fault.code >= 500:
1067
                    logger.exception("API Fault")
1068
                return render_fault(request, fault)
1069
            except BaseException, e:
1070
                logger.exception('Unexpected error: %s' % e)
1071
                fault = InternalServerError('Unexpected error: %s' % e)
1072
                return render_fault(request, fault)
1073
            finally:
1074
                if getattr(request, 'backend', None) is not None:
1075
                    request.backend.close()
1076
        return wrapper
1077
    return decorator