Clean up, sort out logging.
[pithos] / pithos / api / functions.py
1 #\r
2 # Copyright (c) 2011 Greek Research and Technology Network\r
3 #\r
4 \r
5 from django.http import HttpResponse\r
6 from django.template.loader import render_to_string\r
7 from django.utils import simplejson as json\r
8 from django.utils.http import http_date, parse_etags\r
9 \r
10 try:\r
11     from django.utils.http import parse_http_date_safe\r
12 except:\r
13     from pithos.api.compat import parse_http_date_safe\r
14 \r
15 from pithos.api.faults import Fault, NotModified, BadRequest, Unauthorized, ItemNotFound, Conflict, LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity\r
16 from pithos.api.util import get_meta, get_range, api_method\r
17 from pithos.backends.dummy import BackEnd\r
18 \r
19 import os\r
20 import datetime\r
21 import logging\r
22 \r
23 from settings import PROJECT_PATH\r
24 STORAGE_PATH = os.path.join(PROJECT_PATH, 'data')\r
25 \r
26 logger = logging.getLogger(__name__)\r
27 \r
28 @api_method('GET')\r
29 def authenticate(request):\r
30     # Normal Response Codes: 204\r
31     # Error Response Codes: serviceUnavailable (503),\r
32     #                       unauthorized (401),\r
33     #                       badRequest (400)\r
34     \r
35     x_auth_user = request.META.get('HTTP_X_AUTH_USER')\r
36     x_auth_key = request.META.get('HTTP_X_AUTH_KEY')\r
37     \r
38     if not x_auth_user or not x_auth_key:\r
39         raise BadRequest('Missing auth user or key.')\r
40     \r
41     response = HttpResponse(status = 204)\r
42     response['X-Auth-Token'] = '0000'\r
43     response['X-Storage-Url'] = os.path.join(request.build_absolute_uri(), 'demo')\r
44     return response\r
45 \r
46 def account_demux(request, v_account):\r
47     if request.method == 'HEAD':\r
48         return account_meta(request, v_account)\r
49     elif request.method == 'GET':\r
50         return container_list(request, v_account)\r
51     elif request.method == 'POST':\r
52         return account_update(request, v_account)\r
53     else:\r
54         return method_not_allowed(request)\r
55 \r
56 def container_demux(request, v_account, v_container):\r
57     if request.method == 'HEAD':\r
58         return container_meta(request, v_account, v_container)\r
59     elif request.method == 'GET':\r
60         return object_list(request, v_account, v_container)\r
61     elif request.method == 'PUT':\r
62         return container_create(request, v_account, v_container)\r
63     elif request.method == 'POST':\r
64         return container_update(request, v_account, v_container)\r
65     elif request.method == 'DELETE':\r
66         return container_delete(request, v_account, v_container)\r
67     else:\r
68         return method_not_allowed(request)\r
69 \r
70 def object_demux(request, v_account, v_container, v_object):\r
71     if request.method == 'HEAD':\r
72         return object_meta(request, v_account, v_container, v_object)\r
73     elif request.method == 'GET':\r
74         return object_read(request, v_account, v_container, v_object)\r
75     elif request.method == 'PUT':\r
76         return object_write(request, v_account, v_container, v_object)\r
77     elif request.method == 'COPY':\r
78         return object_copy(request, v_account, v_container, v_object)\r
79     elif request.method == 'POST':\r
80         return object_update(request, v_account, v_container, v_object)\r
81     elif request.method == 'DELETE':\r
82         return object_delete(request, v_account, v_container, v_object)\r
83     else:\r
84         return method_not_allowed(request)\r
85 \r
86 @api_method('HEAD')\r
87 def account_meta(request, v_account):\r
88     # Normal Response Codes: 204\r
89     # Error Response Codes: serviceUnavailable (503),\r
90     #                       unauthorized (401),\r
91     #                       badRequest (400)\r
92     \r
93     be = BackEnd(STORAGE_PATH)\r
94     try:\r
95         info = be.get_account_meta(request.user)\r
96     except NameError:\r
97         info = {'count': 0, 'bytes': 0}\r
98     \r
99     response = HttpResponse(status = 204)\r
100     response['X-Account-Container-Count'] = info['count']\r
101     response['X-Account-Bytes-Used'] = info['bytes']\r
102     for k in [x for x in info.keys() if x.startswith('X-Account-Meta-')]:\r
103         response[k.encode('utf-8')] = info[k].encode('utf-8')\r
104     \r
105     return response\r
106 \r
107 @api_method('POST')\r
108 def account_update(request, v_account):\r
109     # Normal Response Codes: 202\r
110     # Error Response Codes: serviceUnavailable (503),\r
111     #                       itemNotFound (404),\r
112     #                       unauthorized (401),\r
113     #                       badRequest (400)\r
114     \r
115     meta = get_meta(request, 'X-Account-Meta-')\r
116     \r
117     be = BackEnd(STORAGE_PATH)\r
118     be.update_account_meta(request.user, meta)\r
119     \r
120     return HttpResponse(status = 202)\r
121 \r
122 @api_method('GET', format_allowed = True)\r
123 def container_list(request, v_account):\r
124     # Normal Response Codes: 200, 204\r
125     # Error Response Codes: serviceUnavailable (503),\r
126     #                       itemNotFound (404),\r
127     #                       unauthorized (401),\r
128     #                       badRequest (400)\r
129     \r
130     marker = request.GET.get('marker')\r
131     limit = request.GET.get('limit')\r
132     if limit:\r
133         try:\r
134             limit = int(limit)\r
135         except ValueError:\r
136             limit = 10000\r
137     \r
138     be = BackEnd(STORAGE_PATH)\r
139     try:\r
140         containers = be.list_containers(request.user, marker, limit)\r
141     except NameError:\r
142         containers = []\r
143     \r
144     if request.serialization == 'text':\r
145         if len(containers) == 0:\r
146             # The cloudfiles python bindings expect 200 if json/xml.\r
147             return HttpResponse(status = 204)\r
148         return HttpResponse('\n'.join(containers), status = 200)\r
149     \r
150     # TODO: Do this with a backend parameter?\r
151     try:\r
152         containers = [be.get_container_meta(request.user, x) for x in containers]\r
153     except NameError:\r
154         raise ItemNotFound()\r
155     if request.serialization == 'xml':\r
156         data = render_to_string('containers.xml', {'account': request.user, 'containers': containers})\r
157     elif request.serialization  == 'json':\r
158         data = json.dumps(containers)\r
159     return HttpResponse(data, status = 200)\r
160 \r
161 @api_method('HEAD')\r
162 def container_meta(request, v_account, v_container):\r
163     # Normal Response Codes: 204\r
164     # Error Response Codes: serviceUnavailable (503),\r
165     #                       itemNotFound (404),\r
166     #                       unauthorized (401),\r
167     #                       badRequest (400)\r
168     \r
169     be = BackEnd(STORAGE_PATH)\r
170     try:\r
171         info = be.get_container_meta(request.user, v_container)\r
172     except NameError:\r
173         raise ItemNotFound()\r
174     \r
175     response = HttpResponse(status = 204)\r
176     response['X-Container-Object-Count'] = info['count']\r
177     response['X-Container-Bytes-Used'] = info['bytes']\r
178     for k in [x for x in info.keys() if x.startswith('X-Container-Meta-')]:\r
179         response[k.encode('utf-8')] = info[k].encode('utf-8')\r
180     \r
181     return response\r
182 \r
183 @api_method('PUT')\r
184 def container_create(request, v_account, v_container):\r
185     # Normal Response Codes: 201, 202\r
186     # Error Response Codes: serviceUnavailable (503),\r
187     #                       itemNotFound (404),\r
188     #                       unauthorized (401),\r
189     #                       badRequest (400)\r
190     \r
191     meta = get_meta(request, 'X-Container-Meta-')\r
192     \r
193     be = BackEnd(STORAGE_PATH)\r
194     try:\r
195         be.create_container(request.user, v_container)\r
196         ret = 201\r
197     except NameError:\r
198         ret = 202\r
199     \r
200     if len(meta) > 0:\r
201         be.update_container_meta(request.user, v_container, meta)\r
202     \r
203     return HttpResponse(status = ret)\r
204 \r
205 @api_method('POST')\r
206 def container_update(request, v_account, v_container):\r
207     # Normal Response Codes: 202\r
208     # Error Response Codes: serviceUnavailable (503),\r
209     #                       itemNotFound (404),\r
210     #                       unauthorized (401),\r
211     #                       badRequest (400)\r
212     \r
213     meta = get_meta(request, 'X-Container-Meta-')\r
214     \r
215     be = BackEnd(STORAGE_PATH)\r
216     try:\r
217         be.update_container_meta(request.user, v_container, meta)\r
218     except NameError:\r
219         raise ItemNotFound()\r
220     \r
221     return HttpResponse(status = 202)\r
222 \r
223 @api_method('DELETE')\r
224 def container_delete(request, v_account, v_container):\r
225     # Normal Response Codes: 204\r
226     # Error Response Codes: serviceUnavailable (503),\r
227     #                       conflict (409),\r
228     #                       itemNotFound (404),\r
229     #                       unauthorized (401),\r
230     #                       badRequest (400)\r
231     \r
232     be = BackEnd(STORAGE_PATH)\r
233     try:\r
234         be.delete_container(request.user, v_container)\r
235     except NameError:\r
236         raise ItemNotFound()\r
237     except IndexError:\r
238         raise Conflict()\r
239     return HttpResponse(status = 204)\r
240 \r
241 @api_method('GET', format_allowed = True)\r
242 def object_list(request, v_account, v_container):\r
243     # Normal Response Codes: 200, 204\r
244     # Error Response Codes: serviceUnavailable (503),\r
245     #                       itemNotFound (404),\r
246     #                       unauthorized (401),\r
247     #                       badRequest (400)\r
248     \r
249     path = request.GET.get('path')\r
250     prefix = request.GET.get('prefix')\r
251     delimiter = request.GET.get('delimiter')\r
252     \r
253     # TODO: Check if the cloudfiles python bindings expect the results with the prefix.\r
254     # Path overrides prefix and delimiter.\r
255     if path:\r
256         prefix = path\r
257         delimiter = '/'\r
258     # Naming policy.\r
259     if prefix and delimiter:\r
260         prefix = prefix + delimiter\r
261     if not prefix:\r
262         prefix = ''\r
263     \r
264     marker = request.GET.get('marker')\r
265     limit = request.GET.get('limit')\r
266     if limit:\r
267         try:\r
268             limit = int(limit)\r
269         except ValueError:\r
270             limit = 10000\r
271     \r
272     be = BackEnd(STORAGE_PATH)\r
273     try:\r
274         objects = be.list_objects(request.user, v_container, prefix, delimiter, marker, limit)\r
275     except NameError:\r
276         raise ItemNotFound()\r
277     \r
278     if request.serialization == 'text':\r
279         if len(objects) == 0:\r
280             # The cloudfiles python bindings expect 200 if json/xml.\r
281             return HttpResponse(status = 204)\r
282         return HttpResponse('\n'.join(objects), status = 200)\r
283     \r
284     # TODO: Do this with a backend parameter?\r
285     try:\r
286         objects = [be.get_object_meta(request.user, v_container, x) for x in objects]\r
287     except NameError:\r
288         raise ItemNotFound()\r
289     # Format dates.\r
290     for x in objects:\r
291         if x.has_key('last_modified'):\r
292             x['last_modified'] = datetime.datetime.fromtimestamp(x['last_modified']).isoformat()\r
293     if request.serialization == 'xml':\r
294         data = render_to_string('objects.xml', {'container': v_container, 'objects': objects})\r
295     elif request.serialization  == 'json':\r
296         data = json.dumps(objects)\r
297     return HttpResponse(data, status = 200)\r
298 \r
299 @api_method('HEAD')\r
300 def object_meta(request, v_account, v_container, v_object):\r
301     # Normal Response Codes: 204\r
302     # Error Response Codes: serviceUnavailable (503),\r
303     #                       itemNotFound (404),\r
304     #                       unauthorized (401),\r
305     #                       badRequest (400)\r
306     \r
307     be = BackEnd(STORAGE_PATH)\r
308     try:\r
309         info = be.get_object_meta(request.user, v_container, v_object)\r
310     except NameError:\r
311         raise ItemNotFound()\r
312     \r
313     response = HttpResponse(status = 204)\r
314     response['ETag'] = info['hash']\r
315     response['Content-Length'] = info['bytes']\r
316     response['Content-Type'] = info['content_type']\r
317     response['Last-Modified'] = http_date(info['last_modified'])\r
318     for k in [x for x in info.keys() if x.startswith('X-Object-Meta-')]:\r
319         response[k.encode('utf-8')] = info[k].encode('utf-8')\r
320     \r
321     return response\r
322 \r
323 @api_method('GET')\r
324 def object_read(request, v_account, v_container, v_object):\r
325     # Normal Response Codes: 200, 206\r
326     # Error Response Codes: serviceUnavailable (503),\r
327     #                       rangeNotSatisfiable (416),\r
328     #                       preconditionFailed (412),\r
329     #                       itemNotFound (404),\r
330     #                       unauthorized (401),\r
331     #                       badRequest (400),\r
332     #                       notModified (304)\r
333     \r
334     be = BackEnd(STORAGE_PATH)\r
335     try:\r
336         info = be.get_object_meta(request.user, v_container, v_object)\r
337     except NameError:\r
338         raise ItemNotFound()\r
339     \r
340     # TODO: Check if the cloudfiles python bindings expect hash/content_type/last_modified on range requests.\r
341     response = HttpResponse()\r
342     response['ETag'] = info['hash']\r
343     response['Content-Type'] = info['content_type']\r
344     response['Last-Modified'] = http_date(info['last_modified'])\r
345     \r
346     # Range handling.\r
347     range = get_range(request)\r
348     if range is not None:\r
349         offset, length = range\r
350         if length:\r
351             if offset + length > info['bytes']:\r
352                 raise RangeNotSatisfiable()\r
353         else:\r
354             if offset > info['bytes']:\r
355                 raise RangeNotSatisfiable()\r
356         if not length:\r
357             length = -1\r
358         \r
359         response['Content-Length'] = length        \r
360         response.status_code = 206\r
361     else:\r
362         offset = 0\r
363         length = -1\r
364         \r
365         response['Content-Length'] = info['bytes']\r
366         response.status_code = 200\r
367     \r
368     # Conditions (according to RFC2616 must be evaluated at the end).\r
369     # TODO: Check etag/date conditions.\r
370     if_match = request.META.get('HTTP_IF_MATCH')\r
371     if if_match is not None and if_match != '*':\r
372         if info['hash'] not in parse_etags(if_match):\r
373             raise PreconditionFailed()\r
374     \r
375     if_none_match = request.META.get('HTTP_IF_NONE_MATCH')\r
376     if if_none_match is not None:\r
377         if if_none_match == '*' or info['hash'] in parse_etags(if_none_match):\r
378             raise NotModified()\r
379     \r
380     if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')\r
381     if if_modified_since is not None:\r
382         if_modified_since = parse_http_date_safe(if_modified_since)\r
383     if if_modified_since is not None and info['last_modified'] <= if_modified_since:\r
384         raise NotModified()\r
385 \r
386     if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')\r
387     if if_unmodified_since is not None:\r
388         if_unmodified_since = parse_http_date_safe(if_unmodified_since)\r
389     if if_unmodified_since is not None and info['last_modified'] > if_unmodified_since:\r
390         raise PreconditionFailed()\r
391     \r
392     try:\r
393         response.content = be.get_object(request.user, v_container, v_object, offset, length)\r
394     except NameError:\r
395         raise ItemNotFound()\r
396     \r
397     return response\r
398 \r
399 @api_method('PUT')\r
400 def object_write(request, v_account, v_container, v_object):\r
401     # Normal Response Codes: 201\r
402     # Error Response Codes: serviceUnavailable (503),\r
403     #                       unprocessableEntity (422),\r
404     #                       lengthRequired (411),\r
405     #                       itemNotFound (404),\r
406     #                       unauthorized (401),\r
407     #                       badRequest (400)\r
408     \r
409     be = BackEnd(STORAGE_PATH)\r
410     \r
411     copy_from = request.META.get('HTTP_X_COPY_FROM')\r
412     if copy_from:\r
413         parts = copy_from.split('/')\r
414         if len(parts) < 3 or parts[0] != '':\r
415             raise BadRequest('Bad X-Copy-From path.')\r
416         copy_container = parts[1]\r
417         copy_name = '/'.join(parts[2:])\r
418         \r
419         try:\r
420             info = be.get_object_meta(request.user, copy_container, copy_name)\r
421         except NameError:\r
422             raise ItemNotFound()\r
423         \r
424         content_length = request.META.get('CONTENT_LENGTH')\r
425         content_type = request.META.get('CONTENT_TYPE')\r
426         # TODO: Why is this required? Copy this ammount?\r
427         if not content_length:\r
428             raise LengthRequired()\r
429         if content_type:\r
430             info['content_type'] = content_type\r
431         \r
432         meta = get_meta(request, 'X-Object-Meta-')\r
433         info.update(meta)\r
434         \r
435         try:\r
436             be.copy_object(request.user, copy_container, copy_name, v_container, v_object)\r
437             be.update_object_meta(request.user, v_container, v_object, info)\r
438         except NameError:\r
439             raise ItemNotFound()\r
440         \r
441         response = HttpResponse(status = 201)\r
442     else:\r
443         content_length = request.META.get('CONTENT_LENGTH')\r
444         content_type = request.META.get('CONTENT_TYPE')\r
445         if not content_length or not content_type:\r
446             raise LengthRequired()\r
447         \r
448         info = {'content_type': content_type}\r
449         meta = get_meta(request, 'X-Object-Meta-')\r
450         info.update(meta)\r
451         \r
452         data = request.raw_post_data\r
453         try:\r
454             be.update_object(request.user, v_container, v_object, data)\r
455             be.update_object_meta(request.user, v_container, v_object, info)\r
456         except NameError:\r
457             raise ItemNotFound()\r
458         \r
459         # TODO: Check before update?\r
460         info = be.get_object_meta(request.user, v_container, v_object)\r
461         etag = request.META.get('HTTP_ETAG')\r
462         if etag:\r
463             etag = parse_etags(etag)[0] # TODO: Unescape properly.\r
464             if etag != info['hash']:\r
465                 be.delete_object(request.user, v_container, v_object)\r
466                 raise UnprocessableEntity()\r
467         \r
468         response = HttpResponse(status = 201)\r
469         response['ETag'] = info['hash']\r
470     \r
471     return response\r
472 \r
473 @api_method('COPY')\r
474 def object_copy(request, v_account, v_container, v_object):\r
475     # Normal Response Codes: 201\r
476     # Error Response Codes: serviceUnavailable (503),\r
477     #                       itemNotFound (404),\r
478     #                       unauthorized (401),\r
479     #                       badRequest (400)\r
480     \r
481     destination = request.META.get('HTTP_DESTINATION')\r
482     if not destination:\r
483         raise BadRequest('Missing Destination.');\r
484     \r
485     parts = destination.split('/')\r
486     if len(parts) < 3 or parts[0] != '':\r
487         raise BadRequest('Bad Destination path.')\r
488     dest_container = parts[1]\r
489     dest_name = '/'.join(parts[2:])\r
490     \r
491     be = BackEnd(STORAGE_PATH)\r
492     try:\r
493         info = be.get_object_meta(request.user, v_container, v_object)\r
494     except NameError:\r
495         raise ItemNotFound()\r
496     \r
497     content_type = request.META.get('CONTENT_TYPE')\r
498     if content_type:\r
499         info['content_type'] = content_type\r
500     meta = get_meta(request, 'X-Object-Meta-')\r
501     info.update(meta)\r
502     \r
503     try:\r
504         be.copy_object(request.user, v_container, v_object, dest_container, dest_name)\r
505         be.update_object_meta(request.user, dest_container, dest_name, info)\r
506     except NameError:\r
507         raise ItemNotFound()\r
508     \r
509     response = HttpResponse(status = 201)\r
510 \r
511 @api_method('POST')\r
512 def object_update(request, v_account, v_container, v_object):\r
513     # Normal Response Codes: 202\r
514     # Error Response Codes: serviceUnavailable (503),\r
515     #                       itemNotFound (404),\r
516     #                       unauthorized (401),\r
517     #                       badRequest (400)\r
518     \r
519     meta = get_meta(request, 'X-Object-Meta-')\r
520     \r
521     be = BackEnd(STORAGE_PATH)\r
522     try:\r
523         be.update_object_meta(request.user, v_container, v_object, meta)\r
524     except NameError:\r
525         raise ItemNotFound()\r
526     \r
527     return HttpResponse(status = 202)\r
528 \r
529 @api_method('DELETE')\r
530 def object_delete(request, v_account, v_container, v_object):\r
531     # Normal Response Codes: 204\r
532     # Error Response Codes: serviceUnavailable (503),\r
533     #                       itemNotFound (404),\r
534     #                       unauthorized (401),\r
535     #                       badRequest (400)\r
536     \r
537     be = BackEnd(STORAGE_PATH)\r
538     try:\r
539         be.delete_object(request.user, v_container, v_object)\r
540     except NameError:\r
541         raise ItemNotFound()\r
542     return HttpResponse(status = 204)\r
543 \r
544 @api_method()\r
545 def method_not_allowed(request):\r
546     raise BadRequest('Method not allowed.')\r