1 from functools import wraps
3 from traceback import format_exc
4 from wsgiref.handlers import format_date_time
6 from django.conf import settings
7 from django.http import HttpResponse
8 from django.utils.http import http_date
10 from pithos.api.compat import parse_http_date_safe
11 from pithos.api.faults import (Fault, NotModified, BadRequest, ItemNotFound, PreconditionFailed,
13 from pithos.backends import backend
19 logger = logging.getLogger(__name__)
22 def printable_meta_dict(d):
23 """Format a meta dictionary for printing out json/xml.
25 Convert all keys to lower case and replace dashes to underscores.
26 Change 'modified' key from backend to 'last_modified' and format date.
29 d['last_modified'] = datetime.datetime.fromtimestamp(int(d['modified'])).isoformat()
31 return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
33 def format_meta_key(k):
34 """Convert underscores to dashes and capitalize intra-dash strings"""
35 return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
37 def get_meta_prefix(request, prefix):
38 """Get all prefix-* request headers in a dict. Reformat keys with format_meta_key()"""
39 prefix = 'HTTP_' + prefix.upper().replace('-', '_')
40 return dict([(format_meta_key(k[5:]), v) for k, v in request.META.iteritems() if k.startswith(prefix)])
42 def get_account_meta(request):
43 """Get metadata from an account request"""
44 meta = get_meta_prefix(request, 'X-Account-Meta-')
47 def put_account_meta(response, meta):
48 """Put metadata in an account response"""
49 response['X-Account-Container-Count'] = meta['count']
50 response['X-Account-Bytes-Used'] = meta['bytes']
51 if 'modified' in meta:
52 response['Last-Modified'] = http_date(int(meta['modified']))
53 for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
54 response[k.encode('utf-8')] = meta[k].encode('utf-8')
56 def get_container_meta(request):
57 """Get metadata from a container request"""
58 meta = get_meta_prefix(request, 'X-Container-Meta-')
61 def put_container_meta(response, meta):
62 """Put metadata in a container response"""
63 response['X-Container-Object-Count'] = meta['count']
64 response['X-Container-Bytes-Used'] = meta['bytes']
65 if 'modified' in meta:
66 response['Last-Modified'] = http_date(int(meta['modified']))
67 for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
68 response[k.encode('utf-8')] = meta[k].encode('utf-8')
70 def get_object_meta(request):
71 """Get metadata from an object request"""
72 meta = get_meta_prefix(request, 'X-Object-Meta-')
73 if request.META.get('CONTENT_TYPE'):
74 meta['Content-Type'] = request.META['CONTENT_TYPE']
75 if request.META.get('HTTP_CONTENT_ENCODING'):
76 meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
77 if request.META.get('HTTP_X_OBJECT_MANIFEST'):
78 meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
81 def put_object_meta(response, meta):
82 """Put metadata in an object response"""
83 response['ETag'] = meta['hash']
84 response['Content-Length'] = meta['bytes']
85 response['Content-Type'] = meta.get('Content-Type', 'application/octet-stream')
86 response['Last-Modified'] = http_date(int(meta['modified']))
87 for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
88 response[k.encode('utf-8')] = meta[k].encode('utf-8')
89 for k in ('Content-Encoding', 'X-Object-Manifest'):
93 def validate_modification_preconditions(request, meta):
94 """Check that the modified timestamp conforms with the preconditions set"""
95 if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
96 if if_modified_since is not None:
97 if_modified_since = parse_http_date_safe(if_modified_since)
98 if if_modified_since is not None and 'modified' in meta and int(meta['modified']) <= if_modified_since:
99 raise NotModified('Object has not been modified')
101 if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
102 if if_unmodified_since is not None:
103 if_unmodified_since = parse_http_date_safe(if_unmodified_since)
104 if if_unmodified_since is not None and 'modified' in meta and int(meta['modified']) > if_unmodified_since:
105 raise PreconditionFailed('Object has been modified')
107 def copy_or_move_object(request, src_path, dest_path, move=False):
108 """Copy or move an object"""
109 if type(src_path) == str:
110 parts = src_path.split('/')
111 if len(parts) < 3 or parts[0] != '':
112 raise BadRequest('Invalid X-Copy-From or X-Move-From header')
113 src_container = parts[1]
114 src_name = '/'.join(parts[2:])
115 elif type(src_path) == tuple and len(src_path) == 2:
116 src_container, src_name = src_path
118 if type(dest_path) == str:
119 parts = dest_path.split('/')
120 if len(parts) < 3 or parts[0] != '':
121 raise BadRequest('Invalid Destination header')
122 dest_container = parts[1]
123 dest_name = '/'.join(parts[2:])
124 elif type(dest_path) == tuple and len(dest_path) == 2:
125 dest_container, dest_name = dest_path
127 meta = get_object_meta(request)
130 backend.move_object(request.user, src_container, src_name, dest_container, dest_name, meta)
132 backend.copy_object(request.user, src_container, src_name, dest_container, dest_name, meta)
134 raise ItemNotFound('Container or object does not exist')
136 def get_range(request):
137 """Parse a Range header from the request
139 Either returns None, or an (offset, length) tuple.
140 If no length is defined length is None.
141 May return a negative offset (offset from the end).
143 range = request.META.get('HTTP_RANGE', '').replace(' ', '')
144 if not range.startswith('bytes='):
147 parts = range[6:].split('-')
152 if offset == '' and upto == '':
166 return (offset, None)
170 return (offset, upto - offset + 1)
176 return (offset, None)
178 def raw_input_socket(request):
179 """Return the socket for reading the rest of the request"""
180 server_software = request.META.get('SERVER_SOFTWARE')
181 if not server_software:
182 if 'wsgi.input' in request.environ:
183 return request.environ['wsgi.input']
184 raise ServiceUnavailable('Unknown server software')
185 if server_software.startswith('WSGIServer'):
186 return request.environ['wsgi.input']
187 elif server_software.startswith('mod_python'):
189 raise ServiceUnavailable('Unknown server software')
191 MAX_UPLOAD_SIZE = 10 * (1024 * 1024) # 10MB
193 def socket_read_iterator(sock, length=-1, blocksize=4096):
194 """Return a maximum of blocksize data read from the socket in each iteration
196 Read up to 'length'. If no 'length' is defined, will attempt a chunked read.
197 The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
199 if length < 0: # Chunked transfers
200 while length < MAX_UPLOAD_SIZE:
201 chunk_length = sock.readline()
202 pos = chunk_length.find(';')
204 chunk_length = chunk_length[:pos]
206 chunk_length = int(chunk_length, 16)
208 raise BadRequest('Bad chunk size') # TODO: Change to something more appropriate.
209 if chunk_length == 0:
211 while chunk_length > 0:
212 data = sock.read(min(chunk_length, blocksize))
213 chunk_length -= len(data)
216 data = sock.read(2) # CRLF
217 # TODO: Raise something to note that maximum size is reached.
219 if length > MAX_UPLOAD_SIZE:
220 # TODO: Raise something to note that maximum size is reached.
223 data = sock.read(min(length, blocksize))
227 def update_response_headers(request, response):
228 if request.serialization == 'xml':
229 response['Content-Type'] = 'application/xml; charset=UTF-8'
230 elif request.serialization == 'json':
231 response['Content-Type'] = 'application/json; charset=UTF-8'
233 response['Content-Type'] = 'text/plain; charset=UTF-8'
236 response['Date'] = format_date_time(time())
238 def render_fault(request, fault):
239 if settings.DEBUG or settings.TEST:
240 fault.details = format_exc(fault)
242 request.serialization = 'text'
243 data = '\n'.join((fault.message, fault.details)) + '\n'
244 response = HttpResponse(data, status=fault.code)
245 update_response_headers(request, response)
248 def request_serialization(request, format_allowed=False):
249 """Return the serialization format requested
251 Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
253 if not format_allowed:
256 format = request.GET.get('format')
259 elif format == 'xml':
262 for item in request.META.get('HTTP_ACCEPT', '').split(','):
263 accept, sep, rest = item.strip().partition(';')
264 if accept == 'text/plain':
266 elif accept == 'application/json':
268 elif accept == 'application/xml' or accept == 'text/xml':
273 def api_method(http_method=None, format_allowed=False):
274 """Decorator function for views that implement an API method"""
277 def wrapper(request, *args, **kwargs):
279 if http_method and request.method != http_method:
280 raise BadRequest('Method not allowed.')
282 # The args variable may contain up to (account, container, object).
283 if len(args) > 1 and len(args[1]) > 256:
284 raise BadRequest('Container name too large.')
285 if len(args) > 2 and len(args[2]) > 1024:
286 raise BadRequest('Object name too large.')
288 # Fill in custom request variables.
289 request.serialization = request_serialization(request, format_allowed)
290 # TODO: Authenticate.
291 request.user = "test"
293 response = func(request, *args, **kwargs)
294 update_response_headers(request, response)
297 return render_fault(request, fault)
298 except BaseException, e:
299 logger.exception('Unexpected error: %s' % e)
300 fault = ServiceUnavailable('Unexpected error')
301 return render_fault(request, fault)