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