Revision 8cb45c13

b/docs/source/devguide.rst
25 25
=========================  ================================
26 26
Revision                   Description
27 27
=========================  ================================
28
0.3 (June 10, 2011)        Allow for publicly available objects via ``https://hostname/public``.
28
0.3 (June 14, 2011)        Large object support with ``X-Object-Manifest``.
29
\                          Allow for publicly available objects via ``https://hostname/public``.
29 30
\                          Support time-variant account/container listings. 
30 31
\                          Add source version when duplicating with PUT/COPY/MOVE.
31 32
\                          Request version in object HEAD/GET requests (list versions with GET).
......
689 690
* Time-variant account/container listings via the ``until`` parameter.
690 691
* Object versions - parameter ``version`` in HEAD/GET (list versions with GET), ``X-Object-Version-*`` meta in replies, ``X-Source-Version`` in PUT/COPY/MOVE.
691 692
* Publicly accessible objects via ``https://hostname/public``. Control with ``X-Object-Public``.
693
* Large object support with ``X-Object-Manifest``.
692 694

  
693 695
Clarifications/suggestions:
694 696

  
......
700 702
* Container/object lists use a ``200`` return code if the reply is of type json/xml. The reply will include an empty json/xml.
701 703
* In headers, dates are formatted according to RFC 1123. In extended information listings, dates are formatted according to ISO 8601.
702 704
* The ``Last-Modified`` header value always reflects the actual latest change timestamp, regardless of time control parameters and version requests. Time precondition checks with ``If-Modified-Since`` and ``If-Unmodified-Since`` headers are applied to this value.
703
* While ``X-Object-Manifest`` can be set and unset, large object support is not yet implemented (**TBD**).
705
* A ``HEAD`` or ``GET`` for an ``X-Object-Manifest`` object, will include modified ``Content-Length`` and ``ETag`` headers, according to the characteristics of the objects under the specified prefix. The ``Etag`` will be the MD5 hash of the corresponding ETags concatenated. In extended container listings there is no metadata processing.
704 706

  
705 707
The Pithos Client
706 708
-----------------
b/pithos/api/functions.py
44 44
    LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity)
45 45
from pithos.api.util import (format_meta_key, printable_meta_dict, get_account_meta,
46 46
    put_account_meta, get_container_meta, put_container_meta, get_object_meta, put_object_meta,
47
    validate_modification_preconditions, validate_matching_preconditions, split_container_object_string,
48
    copy_or_move_object, get_int_parameter, get_content_length, get_content_range, raw_input_socket,
49
    socket_read_iterator, object_data_response, hashmap_hash, api_method)
47
    update_manifest_meta, validate_modification_preconditions, validate_matching_preconditions,
48
    split_container_object_string, copy_or_move_object, get_int_parameter, get_content_length,
49
    get_content_range, raw_input_socket, socket_read_iterator, object_data_response,
50
    hashmap_hash, api_method)
50 51
from pithos.backends import backend
51 52

  
52 53

  
......
374 375
    except IndexError:
375 376
        raise ItemNotFound('Version does not exist')
376 377
    
378
    update_manifest_meta(request, v_account, meta)
379
    
377 380
    response = HttpResponse(status=204)
378 381
    put_object_meta(response, meta)
379 382
    return response
......
397 400
    except IndexError:
398 401
        raise ItemNotFound('Version does not exist')
399 402
    
403
    update_manifest_meta(request, v_account, meta)
404
    
400 405
    # Evaluate conditions.
401 406
    validate_modification_preconditions(request, meta)
402 407
    try:
......
423 428
        response['Content-Length'] = len(data)
424 429
        return response
425 430
    
426
    try:
427
        size, hashmap = backend.get_object_hashmap(request.user, v_account, v_container, v_object, version)
428
    except NameError:
429
        raise ItemNotFound('Object does not exist')
430
    except IndexError:
431
        raise ItemNotFound('Version does not exist')
431
    sizes = []
432
    hashmaps = []
433
    if 'X-Object-Manifest' in meta:
434
        try:
435
            src_container, src_name = split_container_object_string(meta['X-Object-Manifest'])
436
            objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
437
        except ValueError:
438
            raise BadRequest('Invalid X-Object-Manifest header')
439
        except NameError:
440
            raise ItemNotFound('Container does not exist')
441
        
442
        try:
443
            for x in objects:
444
                s, h = backend.get_object_hashmap(request.user, v_account, src_container, x[0], x[1])
445
                sizes.append(s)
446
                hashmaps.append(h)
447
        except NameError:
448
            raise ItemNotFound('Object does not exist')
449
        except IndexError:
450
            raise ItemNotFound('Version does not exist')
451
    else:
452
        try:
453
            s, h = backend.get_object_hashmap(request.user, v_account, v_container, v_object, version)
454
            sizes.append(s)
455
            hashmaps.append(h)
456
        except NameError:
457
            raise ItemNotFound('Object does not exist')
458
        except IndexError:
459
            raise ItemNotFound('Version does not exist')
432 460
    
433 461
    # Reply with the hashmap.
434 462
    if request.serialization != 'text':
463
        size = sum(sizes)
464
        hashmap = sum(hashmaps, [])
435 465
        d = {'block_size': backend.block_size, 'block_hash': backend.hash_algorithm, 'bytes': size, 'hashes': hashmap}
436 466
        if request.serialization == 'xml':
437 467
            d['object'] = v_object
......
444 474
        response['Content-Length'] = len(data)
445 475
        return response
446 476
    
447
    return object_data_response(request, size, hashmap, meta)
477
    return object_data_response(request, sizes, hashmaps, meta)
448 478

  
449 479
@api_method('PUT')
450 480
def object_write(request, v_account, v_container, v_object):
b/pithos/api/util.py
153 153
            if k in meta:
154 154
                response[k] = meta[k]
155 155

  
156
def update_manifest_meta(request, v_account, meta):
157
    """Update metadata if the object has an X-Object-Manifest."""
158
    
159
    if 'X-Object-Manifest' in meta:
160
        hash = ''
161
        bytes = 0
162
        try:
163
            src_container, src_name = split_container_object_string(meta['X-Object-Manifest'])
164
            objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
165
            for x in objects:
166
                src_meta = backend.get_object_meta(request.user, v_account, src_container, x[0], x[1])
167
                hash += src_meta['hash']
168
                bytes += src_meta['bytes']
169
        except:
170
            # Ignore errors.
171
            return
172
        meta['bytes'] = bytes
173
        md5 = hashlib.md5()
174
        md5.update(hash)
175
        meta['hash'] = md5.hexdigest().lower()
176

  
156 177
def validate_modification_preconditions(request, meta):
157 178
    """Check that the modified timestamp conforms with the preconditions set."""
158 179
    
......
188 209
            raise NotModified('Resource Etag matches')
189 210

  
190 211
def split_container_object_string(s):
191
    parts = s.split('/')
192
    if len(parts) < 3 or parts[0] != '':
212
    pos = s.find('/')
213
    if pos == -1:
193 214
        raise ValueError
194
    return parts[1], '/'.join(parts[2:])
215
    return s[:pos], s[(pos + 1):]
195 216

  
196 217
def copy_or_move_object(request, v_account, src_container, src_name, dest_container, dest_name, move=False):
197 218
    """Copy or move an object."""
......
391 412
    Read from the object using the offset and length provided in each entry of the range list.
392 413
    """
393 414
    
394
    def __init__(self, ranges, size, hashmap, boundary):
415
    def __init__(self, ranges, sizes, hashmaps, boundary):
395 416
        self.ranges = ranges
396
        self.size = size
397
        self.hashmap = hashmap
417
        self.sizes = sizes
418
        self.hashmaps = hashmaps
398 419
        self.boundary = boundary
420
        self.size = sum(self.sizes)
399 421
        
400
        self.block_index = -1
422
        self.file_index = 0
423
        self.block_index = 0
424
        self.block_hash = -1
401 425
        self.block = ''
402 426
        
403 427
        self.range_index = -1
......
408 432
    
409 433
    def part_iterator(self):
410 434
        if self.length > 0:
411
            # Get the block for the current offset.
412
            bi = int(self.offset / backend.block_size)
413
            if self.block_index != bi:
435
            # Get the file for the current offset.
436
            file_size = self.sizes[self.file_index]
437
            while self.offset >= file_size:
438
                self.offset -= file_size
439
                self.file_index += 1
440
                file_size = self.sizes[self.file_index]
441
            
442
            # Get the block for the current position.
443
            self.block_index = int(self.offset / backend.block_size)
444
            if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
445
                self.block_hash = self.hashmaps[self.file_index][self.block_index]
414 446
                try:
415
                    self.block = backend.get_block(self.hashmap[bi])
447
                    self.block = backend.get_block(self.block_hash)
416 448
                except NameError:
417 449
                    raise ItemNotFound('Block does not exist')
418
                self.block_index = bi
450
            
419 451
            # Get the data from the block.
420 452
            bo = self.offset % backend.block_size
421
            bl = min(self.length, backend.block_size - bo)
453
            bl = min(self.length, len(self.block) - bo)
422 454
            data = self.block[bo:bo + bl]
423 455
            self.offset += bl
424 456
            self.length -= bl
......
441 473
            if self.range_index < len(self.ranges):
442 474
                # Part header.
443 475
                self.offset, self.length = self.ranges[self.range_index]
476
                self.file_index = 0
444 477
                if self.range_index > 0:
445 478
                    out.append('')
446 479
                out.append('--' + self.boundary)
......
456 489
                out.append('')
457 490
                return '\r\n'.join(out)
458 491

  
459
def object_data_response(request, size, hashmap, meta, public=False):
492
def object_data_response(request, sizes, hashmaps, meta, public=False):
460 493
    """Get the HttpResponse object for replying with the object's data."""
461 494
    
462 495
    # Range handling.
496
    size = sum(sizes)
463 497
    ranges = get_range(request, size)
464 498
    if ranges is None:
465 499
        ranges = [(0, size)]
......
477 511
        boundary = uuid.uuid4().hex
478 512
    else:
479 513
        boundary = ''
480
    wrapper = ObjectWrapper(ranges, size, hashmap, boundary)
514
    wrapper = ObjectWrapper(ranges, sizes, hashmaps, boundary)
481 515
    response = HttpResponse(wrapper, status=ret)
482 516
    put_object_meta(response, meta, public)
483 517
    if ret == 206:
b/pithos/public/functions.py
36 36
from django.http import HttpResponse
37 37

  
38 38
from pithos.api.faults import (Fault, BadRequest, ItemNotFound)
39
from pithos.api.util import (put_object_meta, validate_modification_preconditions,
40
    validate_matching_preconditions, object_data_response, api_method)
39
from pithos.api.util import (put_object_meta, update_manifest_meta,
40
    validate_modification_preconditions, validate_matching_preconditions,
41
    object_data_response, api_method)
41 42
from pithos.backends import backend
42 43

  
43 44

  
......
69 70
    
70 71
    if 'X-Object-Public' not in meta:
71 72
        raise ItemNotFound('Object does not exist')
73
    update_manifest_meta(request, v_account, meta)
72 74
    
73 75
    response = HttpResponse(status=204)
74 76
    put_object_meta(response, meta, True)
......
92 94
    
93 95
    if 'X-Object-Public' not in meta:
94 96
        raise ItemNotFound('Object does not exist')
97
    update_manifest_meta(request, v_account, meta)
95 98
    
96 99
    # Evaluate conditions.
97 100
    validate_modification_preconditions(request, meta)
......
102 105
        response['ETag'] = meta['hash']
103 106
        return response
104 107
    
105
    try:
106
        size, hashmap = backend.get_object_hashmap(request.user, v_account, v_container, v_object)
107
    except NameError:
108
        raise ItemNotFound('Object does not exist')
108
    sizes = []
109
    hashmaps = []
110
    if 'X-Object-Manifest' in meta:
111
        try:
112
            src_container, src_name = split_container_object_string(meta['X-Object-Manifest'])
113
            objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
114
        except ValueError:
115
            raise ItemNotFound('Object does not exist')
116
        except NameError:
117
            raise ItemNotFound('Object does not exist')
118
        
119
        try:
120
            for x in objects:
121
                s, h = backend.get_object_hashmap(request.user, v_account, src_container, x[0], x[1])
122
                sizes.append(s)
123
                hashmaps.append(h)
124
        except NameError:
125
            raise ItemNotFound('Object does not exist')
126
    else:
127
        try:
128
            s, h = backend.get_object_hashmap(request.user, v_account, v_container, v_object, version)
129
            sizes.append(s)
130
            hashmaps.append(h)
131
        except NameError:
132
            raise ItemNotFound('Object does not exist')
109 133
    
110
    return object_data_response(request, size, hashmap, meta, True)
134
    return object_data_response(request, sizes, hashmaps, meta, True)
111 135

  
112 136
@api_method()
113 137
def method_not_allowed(request):

Also available in: Unified diff