Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ 1627c8ff

History | View | Annotate | Download (28.3 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
39

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

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

    
53
import datetime
54
import logging
55
import re
56
import hashlib
57
import uuid
58

    
59

    
60
logger = logging.getLogger(__name__)
61

    
62

    
63
def rename_meta_key(d, old, new):
64
    if old not in d:
65
        return
66
    d[new] = d[old]
67
    del(d[old])
68

    
69
def printable_header_dict(d):
70
    """Format a meta dictionary for printing out json/xml.
71
    
72
    Convert all keys to lower case and replace dashes with underscores.
73
    Format 'last_modified' timestamp.
74
    """
75
    
76
    d['last_modified'] = datetime.datetime.fromtimestamp(int(d['last_modified'])).isoformat()
77
    return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
78

    
79
def format_header_key(k):
80
    """Convert underscores to dashes and capitalize intra-dash strings."""
81
    return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
82

    
83
def get_header_prefix(request, prefix):
84
    """Get all prefix-* request headers in a dict. Reformat keys with format_header_key()."""
85
    
86
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
87
    # TODO: Document or remove '~' replacing.
88
    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)])
89

    
90
def get_account_headers(request):
91
    meta = get_header_prefix(request, 'X-Account-Meta-')
92
    groups = {}
93
    for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
94
        n = k[16:].lower()
95
        if '-' in n or '_' in n:
96
            raise BadRequest('Bad characters in group name')
97
        groups[n] = v.replace(' ', '').split(',')
98
        if '' in groups[n]:
99
            groups[n].remove('')
100
    return meta, groups
101

    
102
def put_account_headers(response, meta, groups):
103
    if 'count' in meta:
104
        response['X-Account-Container-Count'] = meta['count']
105
    if 'bytes' in meta:
106
        response['X-Account-Bytes-Used'] = meta['bytes']
107
    response['Last-Modified'] = http_date(int(meta['modified']))
108
    for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
109
        response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
110
    if 'until_timestamp' in meta:
111
        response['X-Account-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
112
    for k, v in groups.iteritems():
113
        k = smart_str(k, strings_only=True)
114
        k = format_header_key('X-Account-Group-' + k)
115
        v = smart_str(','.join(v), strings_only=True)
116
        response[k] = v
117
    
118
def get_container_headers(request):
119
    meta = get_header_prefix(request, 'X-Container-Meta-')
120
    policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in get_header_prefix(request, 'X-Container-Policy-').iteritems()])
121
    return meta, policy
122

    
123
def put_container_headers(response, meta, policy):
124
    if 'count' in meta:
125
        response['X-Container-Object-Count'] = meta['count']
126
    if 'bytes' in meta:
127
        response['X-Container-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-Container-Meta-')]:
130
        response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
131
    l = [smart_str(x, strings_only=True) for x in meta['object_meta'] if x.startswith('X-Object-Meta-')]
132
    response['X-Container-Object-Meta'] = ','.join([x[14:] for x in l])
133
    response['X-Container-Block-Size'] = backend.block_size
134
    response['X-Container-Block-Hash'] = backend.hash_algorithm
135
    if 'until_timestamp' in meta:
136
        response['X-Container-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
137
    for k, v in policy.iteritems():
138
        response[smart_str(format_header_key('X-Container-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
139

    
140
def get_object_headers(request):
141
    meta = get_header_prefix(request, 'X-Object-Meta-')
142
    if request.META.get('CONTENT_TYPE'):
143
        meta['Content-Type'] = request.META['CONTENT_TYPE']
144
    if request.META.get('HTTP_CONTENT_ENCODING'):
145
        meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
146
    if request.META.get('HTTP_CONTENT_DISPOSITION'):
147
        meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
148
    if request.META.get('HTTP_X_OBJECT_MANIFEST'):
149
        meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
150
    return meta, get_sharing(request), get_public(request)
151

    
152
def put_object_headers(response, meta, restricted=False):
153
    response['ETag'] = meta['hash']
154
    response['Content-Length'] = meta['bytes']
155
    response['Content-Type'] = meta.get('Content-Type', 'application/octet-stream')
156
    response['Last-Modified'] = http_date(int(meta['modified']))
157
    if not restricted:
158
        response['X-Object-Modified-By'] = meta['modified_by']
159
        response['X-Object-Version'] = meta['version']
160
        response['X-Object-Version-Timestamp'] = http_date(int(meta['version_timestamp']))
161
        for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
162
            response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
163
        for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest', 'X-Object-Sharing', 'X-Object-Shared-By', 'X-Object-Public'):
164
            if k in meta:
165
                response[k] = smart_str(meta[k], strings_only=True)
166
    else:
167
        for k in ('Content-Encoding', 'Content-Disposition'):
168
            if k in meta:
169
                response[k] = meta[k]
170

    
171
def update_manifest_meta(request, v_account, meta):
172
    """Update metadata if the object has an X-Object-Manifest."""
173
    
174
    if 'X-Object-Manifest' in meta:
175
        hash = ''
176
        bytes = 0
177
        try:
178
            src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
179
            objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
180
            for x in objects:
181
                src_meta = backend.get_object_meta(request.user, v_account, src_container, x[0], x[1])
182
                hash += src_meta['hash']
183
                bytes += src_meta['bytes']
184
        except:
185
            # Ignore errors.
186
            return
187
        meta['bytes'] = bytes
188
        md5 = hashlib.md5()
189
        md5.update(hash)
190
        meta['hash'] = md5.hexdigest().lower()
191

    
192
def update_sharing_meta(permissions, v_account, v_container, v_object, meta):
193
    if permissions is None:
194
        return
195
    perm_path, perms = permissions
196
    if len(perms) == 0:
197
        return
198
    ret = []
199
    r = ','.join(perms.get('read', []))
200
    if r:
201
        ret.append('read=' + r)
202
    w = ','.join(perms.get('write', []))
203
    if w:
204
        ret.append('write=' + w)
205
    meta['X-Object-Sharing'] = '; '.join(ret)
206
    if '/'.join((v_account, v_container, v_object)) != perm_path:
207
        meta['X-Object-Shared-By'] = perm_path
208

    
209
def update_public_meta(public, meta):
210
    if not public:
211
        return
212
    meta['X-Object-Public'] = public
213

    
214
def validate_modification_preconditions(request, meta):
215
    """Check that the modified timestamp conforms with the preconditions set."""
216
    
217
    if 'modified' not in meta:
218
        return # TODO: Always return?
219
    
220
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
221
    if if_modified_since is not None:
222
        if_modified_since = parse_http_date_safe(if_modified_since)
223
    if if_modified_since is not None and int(meta['modified']) <= if_modified_since:
224
        raise NotModified('Resource has not been modified')
225
    
226
    if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
227
    if if_unmodified_since is not None:
228
        if_unmodified_since = parse_http_date_safe(if_unmodified_since)
229
    if if_unmodified_since is not None and int(meta['modified']) > if_unmodified_since:
230
        raise PreconditionFailed('Resource has been modified')
231

    
232
def validate_matching_preconditions(request, meta):
233
    """Check that the ETag conforms with the preconditions set."""
234
    
235
    hash = meta.get('hash', None)
236
    
237
    if_match = request.META.get('HTTP_IF_MATCH')
238
    if if_match is not None:
239
        if hash is None:
240
            raise PreconditionFailed('Resource does not exist')
241
        if if_match != '*' and hash not in [x.lower() for x in parse_etags(if_match)]:
242
            raise PreconditionFailed('Resource ETag does not match')
243
    
244
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
245
    if if_none_match is not None:
246
        # TODO: If this passes, must ignore If-Modified-Since header.
247
        if hash is not None:
248
            if if_none_match == '*' or hash in [x.lower() for x in parse_etags(if_none_match)]:
249
                # TODO: Continue if an If-Modified-Since header is present.
250
                if request.method in ('HEAD', 'GET'):
251
                    raise NotModified('Resource ETag matches')
252
                raise PreconditionFailed('Resource exists or ETag matches')
253

    
254
def split_container_object_string(s):
255
    if not len(s) > 0 or s[0] != '/':
256
        raise ValueError
257
    s = s[1:]
258
    pos = s.find('/')
259
    if pos == -1:
260
        raise ValueError
261
    return s[:pos], s[(pos + 1):]
262

    
263
def copy_or_move_object(request, v_account, src_container, src_name, dest_container, dest_name, move=False):
264
    """Copy or move an object."""
265
    
266
    meta, permissions, public = get_object_headers(request)
267
    src_version = request.META.get('HTTP_X_SOURCE_VERSION')    
268
    try:
269
        if move:
270
            backend.move_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions)
271
        else:
272
            backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions, src_version)
273
    except NotAllowedError:
274
        raise Unauthorized('Access denied')
275
    except (NameError, IndexError):
276
        raise ItemNotFound('Container or object does not exist')
277
    except ValueError:
278
        raise BadRequest('Invalid sharing header')
279
    except AttributeError, e:
280
        raise Conflict(json.dumps(e.data))
281
    if public is not None:
282
        try:
283
            backend.update_object_public(request.user, v_account, dest_container, dest_name, public)
284
        except NotAllowedError:
285
            raise Unauthorized('Access denied')
286
        except NameError:
287
            raise ItemNotFound('Object does not exist')
288

    
289
def get_int_parameter(p):
290
    if p is not None:
291
        try:
292
            p = int(p)
293
        except ValueError:
294
            return None
295
        if p < 0:
296
            return None
297
    return p
298

    
299
def get_content_length(request):
300
    content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
301
    if content_length is None:
302
        raise LengthRequired('Missing or invalid Content-Length header')
303
    return content_length
304

    
305
def get_range(request, size):
306
    """Parse a Range header from the request.
307
    
308
    Either returns None, when the header is not existent or should be ignored,
309
    or a list of (offset, length) tuples - should be further checked.
310
    """
311
    
312
    ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
313
    if not ranges.startswith('bytes='):
314
        return None
315
    
316
    ret = []
317
    for r in (x.strip() for x in ranges[6:].split(',')):
318
        p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
319
        m = p.match(r)
320
        if not m:
321
            return None
322
        offset = m.group('offset')
323
        upto = m.group('upto')
324
        if offset == '' and upto == '':
325
            return None
326
        
327
        if offset != '':
328
            offset = int(offset)
329
            if upto != '':
330
                upto = int(upto)
331
                if offset > upto:
332
                    return None
333
                ret.append((offset, upto - offset + 1))
334
            else:
335
                ret.append((offset, size - offset))
336
        else:
337
            length = int(upto)
338
            ret.append((size - length, length))
339
    
340
    return ret
341

    
342
def get_content_range(request):
343
    """Parse a Content-Range header from the request.
344
    
345
    Either returns None, when the header is not existent or should be ignored,
346
    or an (offset, length, total) tuple - check as length, total may be None.
347
    Returns (None, None, None) if the provided range is '*/*'.
348
    """
349
    
350
    ranges = request.META.get('HTTP_CONTENT_RANGE', '')
351
    if not ranges:
352
        return None
353
    
354
    p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
355
    m = p.match(ranges)
356
    if not m:
357
        if ranges == 'bytes */*':
358
            return (None, None, None)
359
        return None
360
    offset = int(m.group('offset'))
361
    upto = m.group('upto')
362
    total = m.group('total')
363
    if upto != '':
364
        upto = int(upto)
365
    else:
366
        upto = None
367
    if total != '*':
368
        total = int(total)
369
    else:
370
        total = None
371
    if (upto is not None and offset > upto) or \
372
        (total is not None and offset >= total) or \
373
        (total is not None and upto is not None and upto >= total):
374
        return None
375
    
376
    if upto is None:
377
        length = None
378
    else:
379
        length = upto - offset + 1
380
    return (offset, length, total)
381

    
382
def get_sharing(request):
383
    """Parse an X-Object-Sharing header from the request.
384
    
385
    Raises BadRequest on error.
386
    """
387
    
388
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
389
    if permissions is None:
390
        return None
391
    
392
    ret = {}
393
    permissions = permissions.replace(' ', '')
394
    if permissions == '':
395
        return ret
396
    for perm in (x for x in permissions.split(';')):
397
        if perm.startswith('read='):
398
            ret['read'] = list(set([v.replace(' ','').lower() for v in perm[5:].split(',')]))
399
            if '' in ret['read']:
400
                ret['read'].remove('')
401
            if '*' in ret['read']:
402
                ret['read'] = ['*']
403
            if len(ret['read']) == 0:
404
                raise BadRequest('Bad X-Object-Sharing header value')
405
        elif perm.startswith('write='):
406
            ret['write'] = list(set([v.replace(' ','').lower() for v in perm[6:].split(',')]))
407
            if '' in ret['write']:
408
                ret['write'].remove('')
409
            if '*' in ret['write']:
410
                ret['write'] = ['*']
411
            if len(ret['write']) == 0:
412
                raise BadRequest('Bad X-Object-Sharing header value')
413
        else:
414
            raise BadRequest('Bad X-Object-Sharing header value')
415
    return ret
416

    
417
def get_public(request):
418
    """Parse an X-Object-Public header from the request.
419
    
420
    Raises BadRequest on error.
421
    """
422
    
423
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
424
    if public is None:
425
        return None
426
    
427
    public = public.replace(' ', '').lower()
428
    if public == 'true':
429
        return True
430
    elif public == 'false' or public == '':
431
        return False
432
    raise BadRequest('Bad X-Object-Public header value')
433

    
434
def raw_input_socket(request):
435
    """Return the socket for reading the rest of the request."""
436
    
437
    server_software = request.META.get('SERVER_SOFTWARE')
438
    if server_software and server_software.startswith('mod_python'):
439
        return request._req
440
    if 'wsgi.input' in request.environ:
441
        return request.environ['wsgi.input']
442
    raise ServiceUnavailable('Unknown server software')
443

    
444
MAX_UPLOAD_SIZE = 10 * (1024 * 1024) # 10MB
445

    
446
def socket_read_iterator(request, length=0, blocksize=4096):
447
    """Return a maximum of blocksize data read from the socket in each iteration.
448
    
449
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
450
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
451
    """
452
    
453
    sock = raw_input_socket(request)
454
    if length < 0: # Chunked transfers
455
        # Small version (server does the dechunking).
456
        if request.environ.get('mod_wsgi.input_chunked', None):
457
            while length < MAX_UPLOAD_SIZE:
458
                data = sock.read(blocksize)
459
                if data == '':
460
                    return
461
                yield data
462
            raise BadRequest('Maximum size is reached')
463
        
464
        # Long version (do the dechunking).
465
        data = ''
466
        while length < MAX_UPLOAD_SIZE:
467
            # Get chunk size.
468
            if hasattr(sock, 'readline'):
469
                chunk_length = sock.readline()
470
            else:
471
                chunk_length = ''
472
                while chunk_length[-1:] != '\n':
473
                    chunk_length += sock.read(1)
474
                chunk_length.strip()
475
            pos = chunk_length.find(';')
476
            if pos >= 0:
477
                chunk_length = chunk_length[:pos]
478
            try:
479
                chunk_length = int(chunk_length, 16)
480
            except Exception, e:
481
                raise BadRequest('Bad chunk size') # TODO: Change to something more appropriate.
482
            # Check if done.
483
            if chunk_length == 0:
484
                if len(data) > 0:
485
                    yield data
486
                return
487
            # Get the actual data.
488
            while chunk_length > 0:
489
                chunk = sock.read(min(chunk_length, blocksize))
490
                chunk_length -= len(chunk)
491
                if length > 0:
492
                    length += len(chunk)
493
                data += chunk
494
                if len(data) >= blocksize:
495
                    ret = data[:blocksize]
496
                    data = data[blocksize:]
497
                    yield ret
498
            sock.read(2) # CRLF
499
        raise BadRequest('Maximum size is reached')
500
    else:
501
        if length > MAX_UPLOAD_SIZE:
502
            raise BadRequest('Maximum size is reached')
503
        while length > 0:
504
            data = sock.read(min(length, blocksize))
505
            length -= len(data)
506
            yield data
507

    
508
class ObjectWrapper(object):
509
    """Return the object's data block-per-block in each iteration.
510
    
511
    Read from the object using the offset and length provided in each entry of the range list.
512
    """
513
    
514
    def __init__(self, ranges, sizes, hashmaps, boundary):
515
        self.ranges = ranges
516
        self.sizes = sizes
517
        self.hashmaps = hashmaps
518
        self.boundary = boundary
519
        self.size = sum(self.sizes)
520
        
521
        self.file_index = 0
522
        self.block_index = 0
523
        self.block_hash = -1
524
        self.block = ''
525
        
526
        self.range_index = -1
527
        self.offset, self.length = self.ranges[0]
528
    
529
    def __iter__(self):
530
        return self
531
    
532
    def part_iterator(self):
533
        if self.length > 0:
534
            # Get the file for the current offset.
535
            file_size = self.sizes[self.file_index]
536
            while self.offset >= file_size:
537
                self.offset -= file_size
538
                self.file_index += 1
539
                file_size = self.sizes[self.file_index]
540
            
541
            # Get the block for the current position.
542
            self.block_index = int(self.offset / backend.block_size)
543
            if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
544
                self.block_hash = self.hashmaps[self.file_index][self.block_index]
545
                try:
546
                    self.block = backend.get_block(self.block_hash)
547
                except NameError:
548
                    raise ItemNotFound('Block does not exist')
549
            
550
            # Get the data from the block.
551
            bo = self.offset % backend.block_size
552
            bl = min(self.length, len(self.block) - bo)
553
            data = self.block[bo:bo + bl]
554
            self.offset += bl
555
            self.length -= bl
556
            return data
557
        else:
558
            raise StopIteration
559
    
560
    def next(self):
561
        if len(self.ranges) == 1:
562
            return self.part_iterator()
563
        if self.range_index == len(self.ranges):
564
            raise StopIteration
565
        try:
566
            if self.range_index == -1:
567
                raise StopIteration
568
            return self.part_iterator()
569
        except StopIteration:
570
            self.range_index += 1
571
            out = []
572
            if self.range_index < len(self.ranges):
573
                # Part header.
574
                self.offset, self.length = self.ranges[self.range_index]
575
                self.file_index = 0
576
                if self.range_index > 0:
577
                    out.append('')
578
                out.append('--' + self.boundary)
579
                out.append('Content-Range: bytes %d-%d/%d' % (self.offset, self.offset + self.length - 1, self.size))
580
                out.append('Content-Transfer-Encoding: binary')
581
                out.append('')
582
                out.append('')
583
                return '\r\n'.join(out)
584
            else:
585
                # Footer.
586
                out.append('')
587
                out.append('--' + self.boundary + '--')
588
                out.append('')
589
                return '\r\n'.join(out)
590

    
591
def object_data_response(request, sizes, hashmaps, meta, public=False):
592
    """Get the HttpResponse object for replying with the object's data."""
593
    
594
    # Range handling.
595
    size = sum(sizes)
596
    ranges = get_range(request, size)
597
    if ranges is None:
598
        ranges = [(0, size)]
599
        ret = 200
600
    else:
601
        check = [True for offset, length in ranges if
602
                    length <= 0 or length > size or
603
                    offset < 0 or offset >= size or
604
                    offset + length > size]
605
        if len(check) > 0:
606
            raise RangeNotSatisfiable('Requested range exceeds object limits')
607
        ret = 206
608
        if_range = request.META.get('HTTP_IF_RANGE')
609
        if if_range:
610
            try:
611
                # Modification time has passed instead.
612
                last_modified = parse_http_date(if_range)
613
                if last_modified != meta['modified']:
614
                    ranges = [(0, size)]
615
                    ret = 200
616
            except ValueError:
617
                if if_range != meta['hash']:
618
                    ranges = [(0, size)]
619
                    ret = 200
620
    
621
    if ret == 206 and len(ranges) > 1:
622
        boundary = uuid.uuid4().hex
623
    else:
624
        boundary = ''
625
    wrapper = ObjectWrapper(ranges, sizes, hashmaps, boundary)
626
    response = HttpResponse(wrapper, status=ret)
627
    put_object_headers(response, meta, public)
628
    if ret == 206:
629
        if len(ranges) == 1:
630
            offset, length = ranges[0]
631
            response['Content-Length'] = length # Update with the correct length.
632
            response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
633
        else:
634
            del(response['Content-Length'])
635
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
636
    return response
637

    
638
def put_object_block(hashmap, data, offset):
639
    """Put one block of data at the given offset."""
640
    
641
    bi = int(offset / backend.block_size)
642
    bo = offset % backend.block_size
643
    bl = min(len(data), backend.block_size - bo)
644
    if bi < len(hashmap):
645
        hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], bo)
646
    else:
647
        hashmap.append(backend.put_block(('\x00' * bo) + data[:bl]))
648
    return bl # Return ammount of data written.
649

    
650
def hashmap_hash(hashmap):
651
    """Produce the root hash, treating the hashmap as a Merkle-like tree."""
652
    
653
    def subhash(d):
654
        h = hashlib.new(backend.hash_algorithm)
655
        h.update(d)
656
        return h.digest()
657
    
658
    if len(hashmap) == 0:
659
        return hexlify(subhash(''))
660
    if len(hashmap) == 1:
661
        return hexlify(hashmap[0])
662
    s = 2
663
    while s < len(hashmap):
664
        s = s * 2
665
    h = hashmap + ([('\x00' * len(hashmap[0]))] * (s - len(hashmap)))
666
    h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
667
    while len(h) > 1:
668
        h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
669
    return hexlify(h[0])
670

    
671
def update_response_headers(request, response):
672
    if request.serialization == 'xml':
673
        response['Content-Type'] = 'application/xml; charset=UTF-8'
674
    elif request.serialization == 'json':
675
        response['Content-Type'] = 'application/json; charset=UTF-8'
676
    elif not response['Content-Type']:
677
        response['Content-Type'] = 'text/plain; charset=UTF-8'
678
    
679
    if not response.has_header('Content-Length') and not (response.has_header('Content-Type') and response['Content-Type'].startswith('multipart/byteranges')):
680
        response['Content-Length'] = len(response.content)
681
    
682
    if settings.TEST:
683
        response['Date'] = format_date_time(time())
684

    
685
def render_fault(request, fault):
686
    if settings.DEBUG or settings.TEST:
687
        fault.details = format_exc(fault)
688
    
689
    request.serialization = 'text'
690
    data = '\n'.join((fault.message, fault.details)) + '\n'
691
    response = HttpResponse(data, status=fault.code)
692
    update_response_headers(request, response)
693
    return response
694

    
695
def request_serialization(request, format_allowed=False):
696
    """Return the serialization format requested.
697
    
698
    Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
699
    """
700
    
701
    if not format_allowed:
702
        return 'text'
703
    
704
    format = request.GET.get('format')
705
    if format == 'json':
706
        return 'json'
707
    elif format == 'xml':
708
        return 'xml'
709
    
710
#     for item in request.META.get('HTTP_ACCEPT', '').split(','):
711
#         accept, sep, rest = item.strip().partition(';')
712
#         if accept == 'application/json':
713
#             return 'json'
714
#         elif accept == 'application/xml' or accept == 'text/xml':
715
#             return 'xml'
716
    
717
    return 'text'
718

    
719
def api_method(http_method=None, format_allowed=False):
720
    """Decorator function for views that implement an API method."""
721
    
722
    def decorator(func):
723
        @wraps(func)
724
        def wrapper(request, *args, **kwargs):
725
            try:
726
                if http_method and request.method != http_method:
727
                    raise BadRequest('Method not allowed.')
728
                
729
                # The args variable may contain up to (account, container, object).
730
                if len(args) > 1 and len(args[1]) > 256:
731
                    raise BadRequest('Container name too large.')
732
                if len(args) > 2 and len(args[2]) > 1024:
733
                    raise BadRequest('Object name too large.')
734
                
735
                # Fill in custom request variables.
736
                request.serialization = request_serialization(request, format_allowed)
737
                
738
                response = func(request, *args, **kwargs)
739
                update_response_headers(request, response)
740
                return response
741
            except Fault, fault:
742
                return render_fault(request, fault)
743
            except BaseException, e:
744
                logger.exception('Unexpected error: %s' % e)
745
                fault = ServiceUnavailable('Unexpected error')
746
                return render_fault(request, fault)
747
        return wrapper
748
    return decorator