from wsgiref.handlers import format_date_time
from binascii import hexlify, unhexlify
from datetime import datetime, tzinfo, timedelta
+from urllib import quote, unquote
from django.conf import settings
from django.http import HttpResponse
from django.utils import simplejson as json
from django.utils.http import http_date, parse_etags
-from django.utils.encoding import smart_str
+from django.utils.encoding import smart_unicode, smart_str
+from django.core.files.uploadhandler import FileUploadHandler
+from django.core.files.uploadedfile import UploadedFile
+
+from pithos.lib.compat import parse_http_date_safe, parse_http_date
+from pithos.lib.hashmap import HashMap
-from pithos.api.compat import parse_http_date_safe, parse_http_date
from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, Forbidden, ItemNotFound,
Conflict, LengthRequired, PreconditionFailed, RequestEntityTooLarge,
RangeNotSatisfiable, ServiceUnavailable)
+from pithos.api.short_url import encode_url
from pithos.backends import connect_backend
from pithos.backends.base import NotAllowedError, QuotaError
Format 'last_modified' timestamp.
"""
- d['last_modified'] = isoformat(datetime.fromtimestamp(d['last_modified']))
+ if 'last_modified' in d:
+ d['last_modified'] = isoformat(datetime.fromtimestamp(d['last_modified']))
return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
def format_header_key(k):
return meta, get_sharing(request), get_public(request)
def put_object_headers(response, meta, restricted=False):
- response['ETag'] = meta['ETag']
+ response['ETag'] = meta['ETag'] if 'ETag' in meta else meta['hash']
response['Content-Length'] = meta['bytes']
response['Content-Type'] = meta.get('Content-Type', 'application/octet-stream')
response['Last-Modified'] = http_date(int(meta['modified']))
else:
for k in ('Content-Encoding', 'Content-Disposition'):
if k in meta:
- response[k] = meta[k]
+ response[k] = smart_str(meta[k], strings_only=True)
def update_manifest_meta(request, v_account, meta):
"""Update metadata if the object has an X-Object-Manifest."""
def update_public_meta(public, meta):
if not public:
return
- meta['X-Object-Public'] = public
+ meta['X-Object-Public'] = '/public/' + encode_url(public)
def validate_modification_preconditions(request, meta):
"""Check that the modified timestamp conforms with the preconditions set."""
raise ValueError
s = s[1:]
pos = s.find('/')
- if pos == -1:
+ if pos == -1 or pos == len(s) - 1:
raise ValueError
return s[:pos], s[(pos + 1):]
if move:
version_id = request.backend.move_object(request.user_uniq, src_account, src_container, src_name,
dest_account, dest_container, dest_name,
- meta, False, permissions)
+ 'pithos', meta, False, permissions)
else:
version_id = request.backend.copy_object(request.user_uniq, src_account, src_container, src_name,
dest_account, dest_container, dest_name,
- meta, False, permissions, src_version)
+ 'pithos', meta, False, permissions, src_version)
except NotAllowedError:
raise Forbidden('Not allowed')
except (NameError, IndexError):
length -= len(data)
yield data
+class SaveToBackendHandler(FileUploadHandler):
+ """Handle a file from an HTML form the django way."""
+
+ def __init__(self, request=None):
+ super(SaveToBackendHandler, self).__init__(request)
+ self.backend = request.backend
+
+ def put_data(self, length):
+ if len(self.data) >= length:
+ block = self.data[:length]
+ self.file.hashmap.append(self.backend.put_block(block))
+ self.md5.update(block)
+ self.data = self.data[length:]
+
+ def new_file(self, field_name, file_name, content_type, content_length, charset=None):
+ self.md5 = hashlib.md5()
+ self.data = ''
+ self.file = UploadedFile(name=file_name, content_type=content_type, charset=charset)
+ self.file.size = 0
+ self.file.hashmap = []
+
+ def receive_data_chunk(self, raw_data, start):
+ self.data += raw_data
+ self.file.size += len(raw_data)
+ self.put_data(self.request.backend.block_size)
+ return None
+
+ def file_complete(self, file_size):
+ l = len(self.data)
+ if l > 0:
+ self.put_data(l)
+ self.file.etag = self.md5.hexdigest().lower()
+ return self.file
+
class ObjectWrapper(object):
"""Return the object's data block-per-block in each iteration.
def hashmap_hash(request, hashmap):
"""Produce the root hash, treating the hashmap as a Merkle-like tree."""
- def subhash(d):
- h = hashlib.new(request.backend.hash_algorithm)
- h.update(d)
- return h.digest()
-
- if len(hashmap) == 0:
- return hexlify(subhash(''))
- if len(hashmap) == 1:
- return hashmap[0]
-
- s = 2
- while s < len(hashmap):
- s = s * 2
- h = [unhexlify(x) for x in hashmap]
- h += [('\x00' * len(h[0]))] * (s - len(hashmap))
- while len(h) > 1:
- h = [subhash(h[x] + h[x + 1]) for x in range(0, len(h), 2)]
- return hexlify(h[0])
+ map = HashMap(request.backend.block_size, request.backend.hash_algorithm)
+ map.extend([unhexlify(x) for x in hashmap])
+ return hexlify(map.hash())
+
+def update_request_headers(request):
+ # Handle URL-encoded keys and values.
+ # Handle URL-encoded keys and values.
+ meta = dict([(k, v) for k, v in request.META.iteritems() if k.startswith('HTTP_')])
+ if len(meta) > 90:
+ raise BadRequest('Too many headers.')
+ for k, v in meta.iteritems():
+ if len(k) > 128:
+ raise BadRequest('Header name too large.')
+ if len(v) > 256:
+ raise BadRequest('Header value too large.')
+ try:
+ k.decode('ascii')
+ v.decode('ascii')
+ except UnicodeDecodeError:
+ raise BadRequest('Bad character in headers.')
+ if '%' in k or '%' in v:
+ del(request.META[k])
+ request.META[unquote(k)] = smart_unicode(unquote(v), strings_only=True)
def update_response_headers(request, response):
if request.serialization == 'xml':
elif not response['Content-Type']:
response['Content-Type'] = 'text/plain; charset=UTF-8'
- if not response.has_header('Content-Length') and not (response.has_header('Content-Type') and response['Content-Type'].startswith('multipart/byteranges')):
+ if (not response.has_header('Content-Length') and
+ not (response.has_header('Content-Type') and
+ response['Content-Type'].startswith('multipart/byteranges'))):
response['Content-Length'] = len(response.content)
+ # URL-encode unicode in headers.
+ meta = response.items()
+ for k, v in meta:
+ if (k.startswith('X-Account-') or k.startswith('X-Container-') or
+ k.startswith('X-Object-') or k.startswith('Content-')):
+ del(response[k])
+ response[quote(k)] = quote(v, safe='/=,:@; ')
+
if settings.TEST:
response['Date'] = format_date_time(time())
if len(args) > 2 and len(args[2]) > 1024:
raise BadRequest('Object name too large.')
+ # Format and check headers.
+ update_request_headers(request)
+
# Fill in custom request variables.
request.serialization = request_serialization(request, format_allowed)
request.backend = connect_backend()