Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ 4a1c29ea

History | View | Annotate | Download (30.6 kB)

1
# Copyright 2011 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

    
41
from django.conf import settings
42
from django.http import HttpResponse
43
from django.utils import simplejson as json
44
from django.utils.http import http_date, parse_etags
45
from django.utils.encoding import smart_str
46

    
47
from pithos.api.compat import parse_http_date_safe, parse_http_date
48
from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, Forbidden, ItemNotFound,
49
                                Conflict, LengthRequired, PreconditionFailed, RequestEntityTooLarge,
50
                                RangeNotSatisfiable, ServiceUnavailable)
51
from pithos.backends import connect_backend
52
from pithos.backends.base import NotAllowedError, QuotaError
53

    
54
import logging
55
import re
56
import hashlib
57
import uuid
58
import decimal
59

    
60

    
61
logger = logging.getLogger(__name__)
62

    
63

    
64
class UTC(tzinfo):
65
   def utcoffset(self, dt):
66
       return timedelta(0)
67

    
68
   def tzname(self, dt):
69
       return 'UTC'
70

    
71
   def dst(self, dt):
72
       return timedelta(0)
73

    
74
def json_encode_decimal(obj):
75
    if isinstance(obj, decimal.Decimal):
76
        return str(obj)
77
    raise TypeError(repr(obj) + " is not JSON serializable")
78

    
79
def isoformat(d):
80
   """Return an ISO8601 date string that includes a timezone."""
81

    
82
   return d.replace(tzinfo=UTC()).isoformat()
83

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

    
90
def printable_header_dict(d):
91
    """Format a meta dictionary for printing out json/xml.
92
    
93
    Convert all keys to lower case and replace dashes with underscores.
94
    Format 'last_modified' timestamp.
95
    """
96
    
97
    d['last_modified'] = isoformat(datetime.fromtimestamp(d['last_modified']))
98
    return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
99

    
100
def format_header_key(k):
101
    """Convert underscores to dashes and capitalize intra-dash strings."""
102
    return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
103

    
104
def get_header_prefix(request, prefix):
105
    """Get all prefix-* request headers in a dict. Reformat keys with format_header_key()."""
106
    
107
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
108
    # TODO: Document or remove '~' replacing.
109
    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)])
110

    
111
def get_account_headers(request):
112
    meta = get_header_prefix(request, 'X-Account-Meta-')
113
    groups = {}
114
    for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
115
        n = k[16:].lower()
116
        if '-' in n or '_' in n:
117
            raise BadRequest('Bad characters in group name')
118
        groups[n] = v.replace(' ', '').split(',')
119
        while '' in groups[n]:
120
            groups[n].remove('')
121
    return meta, groups
122

    
123
def put_account_headers(response, meta, groups, policy):
124
    if 'count' in meta:
125
        response['X-Account-Container-Count'] = meta['count']
126
    if 'bytes' in meta:
127
        response['X-Account-Bytes-Used'] = meta['bytes']
128
    response['Last-Modified'] = http_date(int(meta['modified']))
129
    for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
130
        response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
131
    if 'until_timestamp' in meta:
132
        response['X-Account-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
133
    for k, v in groups.iteritems():
134
        k = smart_str(k, strings_only=True)
135
        k = format_header_key('X-Account-Group-' + k)
136
        v = smart_str(','.join(v), strings_only=True)
137
        response[k] = v
138
    for k, v in policy.iteritems():
139
        response[smart_str(format_header_key('X-Account-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
140

    
141
def get_container_headers(request):
142
    meta = get_header_prefix(request, 'X-Container-Meta-')
143
    policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in get_header_prefix(request, 'X-Container-Policy-').iteritems()])
144
    return meta, policy
145

    
146
def put_container_headers(request, response, meta, policy):
147
    if 'count' in meta:
148
        response['X-Container-Object-Count'] = meta['count']
149
    if 'bytes' in meta:
150
        response['X-Container-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-Container-Meta-')]:
153
        response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
154
    l = [smart_str(x, strings_only=True) for x in meta['object_meta'] if x.startswith('X-Object-Meta-')]
155
    response['X-Container-Object-Meta'] = ','.join([x[14:] for x in l])
156
    response['X-Container-Block-Size'] = request.backend.block_size
157
    response['X-Container-Block-Hash'] = request.backend.hash_algorithm
158
    if 'until_timestamp' in meta:
159
        response['X-Container-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
160
    for k, v in policy.iteritems():
161
        response[smart_str(format_header_key('X-Container-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
162

    
163
def get_object_headers(request):
164
    meta = get_header_prefix(request, 'X-Object-Meta-')
165
    if request.META.get('CONTENT_TYPE'):
166
        meta['Content-Type'] = request.META['CONTENT_TYPE']
167
    if request.META.get('HTTP_CONTENT_ENCODING'):
168
        meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
169
    if request.META.get('HTTP_CONTENT_DISPOSITION'):
170
        meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
171
    if request.META.get('HTTP_X_OBJECT_MANIFEST'):
172
        meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
173
    return meta, get_sharing(request), get_public(request)
174

    
175
def put_object_headers(response, meta, restricted=False):
176
    response['ETag'] = meta['ETag']
177
    response['Content-Length'] = meta['bytes']
178
    response['Content-Type'] = meta.get('Content-Type', 'application/octet-stream')
179
    response['Last-Modified'] = http_date(int(meta['modified']))
180
    if not restricted:
181
        response['X-Object-Hash'] = meta['hash']
182
        response['X-Object-Modified-By'] = smart_str(meta['modified_by'], strings_only=True)
183
        response['X-Object-Version'] = meta['version']
184
        response['X-Object-Version-Timestamp'] = http_date(int(meta['version_timestamp']))
185
        for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
186
            response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
187
        for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest',
188
                  'X-Object-Sharing', 'X-Object-Shared-By', 'X-Object-Allowed-To',
189
                  'X-Object-Public'):
190
            if k in meta:
191
                response[k] = smart_str(meta[k], strings_only=True)
192
    else:
193
        for k in ('Content-Encoding', 'Content-Disposition'):
194
            if k in meta:
195
                response[k] = meta[k]
196

    
197
def update_manifest_meta(request, v_account, meta):
198
    """Update metadata if the object has an X-Object-Manifest."""
199
    
200
    if 'X-Object-Manifest' in meta:
201
        etag = ''
202
        bytes = 0
203
        try:
204
            src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
205
            objects = request.backend.list_objects(request.user_uniq, v_account,
206
                                src_container, prefix=src_name, virtual=False)
207
            for x in objects:
208
                src_meta = request.backend.get_object_meta(request.user_uniq,
209
                                        v_account, src_container, x[0], x[1])
210
                etag += src_meta['ETag']
211
                bytes += src_meta['bytes']
212
        except:
213
            # Ignore errors.
214
            return
215
        meta['bytes'] = bytes
216
        md5 = hashlib.md5()
217
        md5.update(etag)
218
        meta['ETag'] = md5.hexdigest().lower()
219

    
220
def update_sharing_meta(request, permissions, v_account, v_container, v_object, meta):
221
    if permissions is None:
222
        return
223
    allowed, perm_path, perms = permissions
224
    if len(perms) == 0:
225
        return
226
    ret = []
227
    r = ','.join(perms.get('read', []))
228
    if r:
229
        ret.append('read=' + r)
230
    w = ','.join(perms.get('write', []))
231
    if w:
232
        ret.append('write=' + w)
233
    meta['X-Object-Sharing'] = '; '.join(ret)
234
    if '/'.join((v_account, v_container, v_object)) != perm_path:
235
        meta['X-Object-Shared-By'] = perm_path
236
    if request.user_uniq != v_account:
237
        meta['X-Object-Allowed-To'] = allowed
238

    
239
def update_public_meta(public, meta):
240
    if not public:
241
        return
242
    meta['X-Object-Public'] = public
243

    
244
def validate_modification_preconditions(request, meta):
245
    """Check that the modified timestamp conforms with the preconditions set."""
246
    
247
    if 'modified' not in meta:
248
        return # TODO: Always return?
249
    
250
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
251
    if if_modified_since is not None:
252
        if_modified_since = parse_http_date_safe(if_modified_since)
253
    if if_modified_since is not None and int(meta['modified']) <= if_modified_since:
254
        raise NotModified('Resource has not been modified')
255
    
256
    if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
257
    if if_unmodified_since is not None:
258
        if_unmodified_since = parse_http_date_safe(if_unmodified_since)
259
    if if_unmodified_since is not None and int(meta['modified']) > if_unmodified_since:
260
        raise PreconditionFailed('Resource has been modified')
261

    
262
def validate_matching_preconditions(request, meta):
263
    """Check that the ETag conforms with the preconditions set."""
264
    
265
    etag = meta.get('ETag', None)
266
    
267
    if_match = request.META.get('HTTP_IF_MATCH')
268
    if if_match is not None:
269
        if etag is None:
270
            raise PreconditionFailed('Resource does not exist')
271
        if if_match != '*' and etag not in [x.lower() for x in parse_etags(if_match)]:
272
            raise PreconditionFailed('Resource ETag does not match')
273
    
274
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
275
    if if_none_match is not None:
276
        # TODO: If this passes, must ignore If-Modified-Since header.
277
        if etag is not None:
278
            if if_none_match == '*' or etag in [x.lower() for x in parse_etags(if_none_match)]:
279
                # TODO: Continue if an If-Modified-Since header is present.
280
                if request.method in ('HEAD', 'GET'):
281
                    raise NotModified('Resource ETag matches')
282
                raise PreconditionFailed('Resource exists or ETag matches')
283

    
284
def split_container_object_string(s):
285
    if not len(s) > 0 or s[0] != '/':
286
        raise ValueError
287
    s = s[1:]
288
    pos = s.find('/')
289
    if pos == -1:
290
        raise ValueError
291
    return s[:pos], s[(pos + 1):]
292

    
293
def copy_or_move_object(request, src_account, src_container, src_name, dest_account, dest_container, dest_name, move=False):
294
    """Copy or move an object."""
295
    
296
    meta, permissions, public = get_object_headers(request)
297
    src_version = request.META.get('HTTP_X_SOURCE_VERSION')
298
    try:
299
        if move:
300
            version_id = request.backend.move_object(request.user_uniq, src_account, src_container, src_name,
301
                                                        dest_account, dest_container, dest_name,
302
                                                        meta, False, permissions)
303
        else:
304
            version_id = request.backend.copy_object(request.user_uniq, src_account, src_container, src_name,
305
                                                        dest_account, dest_container, dest_name,
306
                                                        meta, False, permissions, src_version)
307
    except NotAllowedError:
308
        raise Forbidden('Not allowed')
309
    except (NameError, IndexError):
310
        raise ItemNotFound('Container or object does not exist')
311
    except ValueError:
312
        raise BadRequest('Invalid sharing header')
313
    except AttributeError, e:
314
        raise Conflict('\n'.join(e.data) + '\n')
315
    except QuotaError:
316
        raise RequestEntityTooLarge('Quota exceeded')
317
    if public is not None:
318
        try:
319
            request.backend.update_object_public(request.user_uniq, dest_account, dest_container, dest_name, public)
320
        except NotAllowedError:
321
            raise Forbidden('Not allowed')
322
        except NameError:
323
            raise ItemNotFound('Object does not exist')
324
    return version_id
325

    
326
def get_int_parameter(p):
327
    if p is not None:
328
        try:
329
            p = int(p)
330
        except ValueError:
331
            return None
332
        if p < 0:
333
            return None
334
    return p
335

    
336
def get_content_length(request):
337
    content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
338
    if content_length is None:
339
        raise LengthRequired('Missing or invalid Content-Length header')
340
    return content_length
341

    
342
def get_range(request, size):
343
    """Parse a Range header from the request.
344
    
345
    Either returns None, when the header is not existent or should be ignored,
346
    or a list of (offset, length) tuples - should be further checked.
347
    """
348
    
349
    ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
350
    if not ranges.startswith('bytes='):
351
        return None
352
    
353
    ret = []
354
    for r in (x.strip() for x in ranges[6:].split(',')):
355
        p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
356
        m = p.match(r)
357
        if not m:
358
            return None
359
        offset = m.group('offset')
360
        upto = m.group('upto')
361
        if offset == '' and upto == '':
362
            return None
363
        
364
        if offset != '':
365
            offset = int(offset)
366
            if upto != '':
367
                upto = int(upto)
368
                if offset > upto:
369
                    return None
370
                ret.append((offset, upto - offset + 1))
371
            else:
372
                ret.append((offset, size - offset))
373
        else:
374
            length = int(upto)
375
            ret.append((size - length, length))
376
    
377
    return ret
378

    
379
def get_content_range(request):
380
    """Parse a Content-Range header from the request.
381
    
382
    Either returns None, when the header is not existent or should be ignored,
383
    or an (offset, length, total) tuple - check as length, total may be None.
384
    Returns (None, None, None) if the provided range is '*/*'.
385
    """
386
    
387
    ranges = request.META.get('HTTP_CONTENT_RANGE', '')
388
    if not ranges:
389
        return None
390
    
391
    p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
392
    m = p.match(ranges)
393
    if not m:
394
        if ranges == 'bytes */*':
395
            return (None, None, None)
396
        return None
397
    offset = int(m.group('offset'))
398
    upto = m.group('upto')
399
    total = m.group('total')
400
    if upto != '':
401
        upto = int(upto)
402
    else:
403
        upto = None
404
    if total != '*':
405
        total = int(total)
406
    else:
407
        total = None
408
    if (upto is not None and offset > upto) or \
409
        (total is not None and offset >= total) or \
410
        (total is not None and upto is not None and upto >= total):
411
        return None
412
    
413
    if upto is None:
414
        length = None
415
    else:
416
        length = upto - offset + 1
417
    return (offset, length, total)
418

    
419
def get_sharing(request):
420
    """Parse an X-Object-Sharing header from the request.
421
    
422
    Raises BadRequest on error.
423
    """
424
    
425
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
426
    if permissions is None:
427
        return None
428
    
429
    # TODO: Document or remove '~' replacing.
430
    permissions = permissions.replace('~', '')
431
    
432
    ret = {}
433
    permissions = permissions.replace(' ', '')
434
    if permissions == '':
435
        return ret
436
    for perm in (x for x in permissions.split(';')):
437
        if perm.startswith('read='):
438
            ret['read'] = list(set([v.replace(' ','').lower() for v in perm[5:].split(',')]))
439
            if '' in ret['read']:
440
                ret['read'].remove('')
441
            if '*' in ret['read']:
442
                ret['read'] = ['*']
443
            if len(ret['read']) == 0:
444
                raise BadRequest('Bad X-Object-Sharing header value')
445
        elif perm.startswith('write='):
446
            ret['write'] = list(set([v.replace(' ','').lower() for v in perm[6:].split(',')]))
447
            if '' in ret['write']:
448
                ret['write'].remove('')
449
            if '*' in ret['write']:
450
                ret['write'] = ['*']
451
            if len(ret['write']) == 0:
452
                raise BadRequest('Bad X-Object-Sharing header value')
453
        else:
454
            raise BadRequest('Bad X-Object-Sharing header value')
455
    
456
    # Keep duplicates only in write list.
457
    dups = [x for x in ret.get('read', []) if x in ret.get('write', []) and x != '*']
458
    if dups:
459
        for x in dups:
460
            ret['read'].remove(x)
461
        if len(ret['read']) == 0:
462
            del(ret['read'])
463
    
464
    return ret
465

    
466
def get_public(request):
467
    """Parse an X-Object-Public header from the request.
468
    
469
    Raises BadRequest on error.
470
    """
471
    
472
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
473
    if public is None:
474
        return None
475
    
476
    public = public.replace(' ', '').lower()
477
    if public == 'true':
478
        return True
479
    elif public == 'false' or public == '':
480
        return False
481
    raise BadRequest('Bad X-Object-Public header value')
482

    
483
def raw_input_socket(request):
484
    """Return the socket for reading the rest of the request."""
485
    
486
    server_software = request.META.get('SERVER_SOFTWARE')
487
    if server_software and server_software.startswith('mod_python'):
488
        return request._req
489
    if 'wsgi.input' in request.environ:
490
        return request.environ['wsgi.input']
491
    raise ServiceUnavailable('Unknown server software')
492

    
493
MAX_UPLOAD_SIZE = 5 * (1024 * 1024 * 1024) # 5GB
494

    
495
def socket_read_iterator(request, length=0, blocksize=4096):
496
    """Return a maximum of blocksize data read from the socket in each iteration.
497
    
498
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
499
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
500
    """
501
    
502
    sock = raw_input_socket(request)
503
    if length < 0: # Chunked transfers
504
        # Small version (server does the dechunking).
505
        if request.environ.get('mod_wsgi.input_chunked', None) or request.META['SERVER_SOFTWARE'].startswith('gunicorn'):
506
            while length < MAX_UPLOAD_SIZE:
507
                data = sock.read(blocksize)
508
                if data == '':
509
                    return
510
                yield data
511
            raise BadRequest('Maximum size is reached')
512
        
513
        # Long version (do the dechunking).
514
        data = ''
515
        while length < MAX_UPLOAD_SIZE:
516
            # Get chunk size.
517
            if hasattr(sock, 'readline'):
518
                chunk_length = sock.readline()
519
            else:
520
                chunk_length = ''
521
                while chunk_length[-1:] != '\n':
522
                    chunk_length += sock.read(1)
523
                chunk_length.strip()
524
            pos = chunk_length.find(';')
525
            if pos >= 0:
526
                chunk_length = chunk_length[:pos]
527
            try:
528
                chunk_length = int(chunk_length, 16)
529
            except Exception, e:
530
                raise BadRequest('Bad chunk size') # TODO: Change to something more appropriate.
531
            # Check if done.
532
            if chunk_length == 0:
533
                if len(data) > 0:
534
                    yield data
535
                return
536
            # Get the actual data.
537
            while chunk_length > 0:
538
                chunk = sock.read(min(chunk_length, blocksize))
539
                chunk_length -= len(chunk)
540
                if length > 0:
541
                    length += len(chunk)
542
                data += chunk
543
                if len(data) >= blocksize:
544
                    ret = data[:blocksize]
545
                    data = data[blocksize:]
546
                    yield ret
547
            sock.read(2) # CRLF
548
        raise BadRequest('Maximum size is reached')
549
    else:
550
        if length > MAX_UPLOAD_SIZE:
551
            raise BadRequest('Maximum size is reached')
552
        while length > 0:
553
            data = sock.read(min(length, blocksize))
554
            if not data:
555
                raise BadRequest()
556
            length -= len(data)
557
            yield data
558

    
559
class ObjectWrapper(object):
560
    """Return the object's data block-per-block in each iteration.
561
    
562
    Read from the object using the offset and length provided in each entry of the range list.
563
    """
564
    
565
    def __init__(self, backend, ranges, sizes, hashmaps, boundary):
566
        self.backend = backend
567
        self.ranges = ranges
568
        self.sizes = sizes
569
        self.hashmaps = hashmaps
570
        self.boundary = boundary
571
        self.size = sum(self.sizes)
572
        
573
        self.file_index = 0
574
        self.block_index = 0
575
        self.block_hash = -1
576
        self.block = ''
577
        
578
        self.range_index = -1
579
        self.offset, self.length = self.ranges[0]
580
    
581
    def __iter__(self):
582
        return self
583
    
584
    def part_iterator(self):
585
        if self.length > 0:
586
            # Get the file for the current offset.
587
            file_size = self.sizes[self.file_index]
588
            while self.offset >= file_size:
589
                self.offset -= file_size
590
                self.file_index += 1
591
                file_size = self.sizes[self.file_index]
592
            
593
            # Get the block for the current position.
594
            self.block_index = int(self.offset / self.backend.block_size)
595
            if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
596
                self.block_hash = self.hashmaps[self.file_index][self.block_index]
597
                try:
598
                    self.block = self.backend.get_block(self.block_hash)
599
                except NameError:
600
                    raise ItemNotFound('Block does not exist')
601
            
602
            # Get the data from the block.
603
            bo = self.offset % self.backend.block_size
604
            bl = min(self.length, len(self.block) - bo)
605
            data = self.block[bo:bo + bl]
606
            self.offset += bl
607
            self.length -= bl
608
            return data
609
        else:
610
            raise StopIteration
611
    
612
    def next(self):
613
        if len(self.ranges) == 1:
614
            return self.part_iterator()
615
        if self.range_index == len(self.ranges):
616
            raise StopIteration
617
        try:
618
            if self.range_index == -1:
619
                raise StopIteration
620
            return self.part_iterator()
621
        except StopIteration:
622
            self.range_index += 1
623
            out = []
624
            if self.range_index < len(self.ranges):
625
                # Part header.
626
                self.offset, self.length = self.ranges[self.range_index]
627
                self.file_index = 0
628
                if self.range_index > 0:
629
                    out.append('')
630
                out.append('--' + self.boundary)
631
                out.append('Content-Range: bytes %d-%d/%d' % (self.offset, self.offset + self.length - 1, self.size))
632
                out.append('Content-Transfer-Encoding: binary')
633
                out.append('')
634
                out.append('')
635
                return '\r\n'.join(out)
636
            else:
637
                # Footer.
638
                out.append('')
639
                out.append('--' + self.boundary + '--')
640
                out.append('')
641
                return '\r\n'.join(out)
642

    
643
def object_data_response(request, sizes, hashmaps, meta, public=False):
644
    """Get the HttpResponse object for replying with the object's data."""
645
    
646
    # Range handling.
647
    size = sum(sizes)
648
    ranges = get_range(request, size)
649
    if ranges is None:
650
        ranges = [(0, size)]
651
        ret = 200
652
    else:
653
        check = [True for offset, length in ranges if
654
                    length <= 0 or length > size or
655
                    offset < 0 or offset >= size or
656
                    offset + length > size]
657
        if len(check) > 0:
658
            raise RangeNotSatisfiable('Requested range exceeds object limits')
659
        ret = 206
660
        if_range = request.META.get('HTTP_IF_RANGE')
661
        if if_range:
662
            try:
663
                # Modification time has passed instead.
664
                last_modified = parse_http_date(if_range)
665
                if last_modified != meta['modified']:
666
                    ranges = [(0, size)]
667
                    ret = 200
668
            except ValueError:
669
                if if_range != meta['ETag']:
670
                    ranges = [(0, size)]
671
                    ret = 200
672
    
673
    if ret == 206 and len(ranges) > 1:
674
        boundary = uuid.uuid4().hex
675
    else:
676
        boundary = ''
677
    wrapper = ObjectWrapper(request.backend, ranges, sizes, hashmaps, boundary)
678
    response = HttpResponse(wrapper, status=ret)
679
    put_object_headers(response, meta, public)
680
    if ret == 206:
681
        if len(ranges) == 1:
682
            offset, length = ranges[0]
683
            response['Content-Length'] = length # Update with the correct length.
684
            response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
685
        else:
686
            del(response['Content-Length'])
687
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
688
    return response
689

    
690
def put_object_block(request, hashmap, data, offset):
691
    """Put one block of data at the given offset."""
692
    
693
    bi = int(offset / request.backend.block_size)
694
    bo = offset % request.backend.block_size
695
    bl = min(len(data), request.backend.block_size - bo)
696
    if bi < len(hashmap):
697
        hashmap[bi] = request.backend.update_block(hashmap[bi], data[:bl], bo)
698
    else:
699
        hashmap.append(request.backend.put_block(('\x00' * bo) + data[:bl]))
700
    return bl # Return ammount of data written.
701

    
702
def hashmap_hash(request, hashmap):
703
    """Produce the root hash, treating the hashmap as a Merkle-like tree."""
704
    
705
    def subhash(d):
706
        h = hashlib.new(request.backend.hash_algorithm)
707
        h.update(d)
708
        return h.digest()
709
    
710
    if len(hashmap) == 0:
711
        return hexlify(subhash(''))
712
    if len(hashmap) == 1:
713
        return hashmap[0]
714
    
715
    s = 2
716
    while s < len(hashmap):
717
        s = s * 2
718
    h = [unhexlify(x) for x in hashmap]
719
    h += [('\x00' * len(h[0]))] * (s - len(hashmap))
720
    while len(h) > 1:
721
        h = [subhash(h[x] + h[x + 1]) for x in range(0, len(h), 2)]
722
    return hexlify(h[0])
723

    
724
def update_response_headers(request, response):
725
    if request.serialization == 'xml':
726
        response['Content-Type'] = 'application/xml; charset=UTF-8'
727
    elif request.serialization == 'json':
728
        response['Content-Type'] = 'application/json; charset=UTF-8'
729
    elif not response['Content-Type']:
730
        response['Content-Type'] = 'text/plain; charset=UTF-8'
731
    
732
    if not response.has_header('Content-Length') and not (response.has_header('Content-Type') and response['Content-Type'].startswith('multipart/byteranges')):
733
        response['Content-Length'] = len(response.content)
734
    
735
    if settings.TEST:
736
        response['Date'] = format_date_time(time())
737

    
738
def render_fault(request, fault):
739
    if settings.DEBUG or settings.TEST:
740
        fault.details = format_exc(fault)
741
    
742
    request.serialization = 'text'
743
    data = '\n'.join((fault.message, fault.details)) + '\n'
744
    response = HttpResponse(data, status=fault.code)
745
    update_response_headers(request, response)
746
    return response
747

    
748
def request_serialization(request, format_allowed=False):
749
    """Return the serialization format requested.
750
    
751
    Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
752
    """
753
    
754
    if not format_allowed:
755
        return 'text'
756
    
757
    format = request.GET.get('format')
758
    if format == 'json':
759
        return 'json'
760
    elif format == 'xml':
761
        return 'xml'
762
    
763
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
764
        accept, sep, rest = item.strip().partition(';')
765
        if accept == 'application/json':
766
            return 'json'
767
        elif accept == 'application/xml' or accept == 'text/xml':
768
            return 'xml'
769
    
770
    return 'text'
771

    
772
def api_method(http_method=None, format_allowed=False, user_required=True):
773
    """Decorator function for views that implement an API method."""
774
    
775
    def decorator(func):
776
        @wraps(func)
777
        def wrapper(request, *args, **kwargs):
778
            try:
779
                if http_method and request.method != http_method:
780
                    raise BadRequest('Method not allowed.')
781
                if user_required and getattr(request, 'user', None) is None:
782
                    raise Unauthorized('Access denied')
783
                
784
                # The args variable may contain up to (account, container, object).
785
                if len(args) > 1 and len(args[1]) > 256:
786
                    raise BadRequest('Container name too large.')
787
                if len(args) > 2 and len(args[2]) > 1024:
788
                    raise BadRequest('Object name too large.')
789
                
790
                # Fill in custom request variables.
791
                request.serialization = request_serialization(request, format_allowed)
792
                request.backend = connect_backend()
793
                
794
                response = func(request, *args, **kwargs)
795
                update_response_headers(request, response)
796
                return response
797
            except Fault, fault:
798
                return render_fault(request, fault)
799
            except BaseException, e:
800
                logger.exception('Unexpected error: %s' % e)
801
                fault = ServiceUnavailable('Unexpected error')
802
                return render_fault(request, fault)
803
            finally:
804
                if getattr(request, 'backend', None) is not None:
805
                    request.backend.close()
806
        return wrapper
807
    return decorator