Merge API and backend container functions
[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_object_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 from pithos.backends.dummy_debug import *\r
24 \r
25 import logging\r
26 \r
27 logging.basicConfig(level=logging.DEBUG)\r
28 \r
29 @api_method('GET')\r
30 def authenticate(request):\r
31     # Normal Response Codes: 204\r
32     # Error Response Codes: serviceUnavailable (503),\r
33     #                       unauthorized (401),\r
34     #                       badRequest (400)\r
35     \r
36     x_auth_user = request.META.get('HTTP_X_AUTH_USER')\r
37     x_auth_key = request.META.get('HTTP_X_AUTH_KEY')\r
38     \r
39     if not x_auth_user or not x_auth_key:\r
40         raise BadRequest('Missing auth user or key.')\r
41     \r
42     response = HttpResponse(status = 204)\r
43     response['X-Auth-Token'] = 'eaaafd18-0fed-4b3a-81b4-663c99ec1cbb'\r
44     # TODO: Must support X-Storage-Url to be compatible.\r
45     response['X-Storage-Url'] = 'http://127.0.0.1:8000/v1/asdf'\r
46     return response\r
47 \r
48 def account_demux(request, v_account):\r
49     if request.method == 'HEAD':\r
50         return account_meta(request, v_account)\r
51     elif request.method == 'GET':\r
52         return container_list(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 == 'DELETE':\r
64         return container_delete(request, v_account, v_container)\r
65     else:\r
66         return method_not_allowed(request)\r
67 \r
68 def object_demux(request, v_account, v_container, v_object):\r
69     if request.method == 'HEAD':\r
70         return object_meta(request, v_account, v_container, v_object)\r
71     elif request.method == 'GET':\r
72         return object_read(request, v_account, v_container, v_object)\r
73     elif request.method == 'PUT':\r
74         return object_write(request, v_account, v_container, v_object)\r
75     elif request.method == 'COPY':\r
76         return object_copy(request, v_account, v_container, v_object)\r
77     elif request.method == 'POST':\r
78         return object_update(request, v_account, v_container, v_object)\r
79     elif request.method == 'DELETE':\r
80         return object_delete(request, v_account, v_container, v_object)\r
81     else:\r
82         return method_not_allowed(request)\r
83 \r
84 @api_method('HEAD')\r
85 def account_meta(request, v_account):\r
86     # Normal Response Codes: 204\r
87     # Error Response Codes: serviceUnavailable (503),\r
88     #                       unauthorized (401),\r
89     #                       badRequest (400)\r
90     \r
91     be = BackEnd(STORAGE_PATH)\r
92     try:\r
93         info = be.get_account_meta(request.user)\r
94     except NameError:\r
95         info = {'count': 0, 'bytes': 0}\r
96         \r
97     response = HttpResponse(status = 204)\r
98     response['X-Account-Container-Count'] = info['count']\r
99     response['X-Account-Bytes-Used'] = info['bytes']\r
100     return response\r
101 \r
102 @api_method('GET', format_allowed = True)\r
103 def container_list(request, v_account):\r
104     # Normal Response Codes: 200, 204\r
105     # Error Response Codes: serviceUnavailable (503),\r
106     #                       itemNotFound (404),\r
107     #                       unauthorized (401),\r
108     #                       badRequest (400)\r
109     \r
110     marker = request.GET.get('marker')\r
111     limit = request.GET.get('limit')\r
112     if limit:\r
113         try:\r
114             limit = int(limit)\r
115         except ValueError:\r
116             limit = 10000\r
117     \r
118     be = BackEnd(STORAGE_PATH)\r
119     try:\r
120         containers = be.list_containers(request.user, marker, limit)\r
121     except NameError:\r
122         containers = []\r
123     if len(containers) == 0:\r
124         return HttpResponse(status = 204)\r
125     \r
126     if request.serialization == 'text':\r
127         return HttpResponse('\n'.join(containers), status = 200)\r
128     \r
129     # TODO: Do this with a backend parameter?\r
130     try:\r
131         containers = [be.get_container_meta(request.user, x) for x in containers]\r
132     except NameError:\r
133         raise ItemNotFound()\r
134     if request.serialization == 'xml':\r
135         data = render_to_string('containers.xml', {'account': request.user, 'containers': containers})\r
136     elif request.serialization  == 'json':\r
137         data = json.dumps(containers)    \r
138     return HttpResponse(data, status = 200)\r
139 \r
140 @api_method('HEAD')\r
141 def container_meta(request, v_account, v_container):\r
142     # Normal Response Codes: 204\r
143     # Error Response Codes: serviceUnavailable (503),\r
144     #                       itemNotFound (404),\r
145     #                       unauthorized (401),\r
146     #                       badRequest (400)\r
147     \r
148     be = BackEnd(STORAGE_PATH)\r
149     try:\r
150         info = be.get_container_meta(request.user, v_container)\r
151     except NameError:\r
152         raise ItemNotFound()\r
153     \r
154     response = HttpResponse(status = 204)\r
155     response['X-Container-Object-Count'] = info['count']\r
156     response['X-Container-Bytes-Used'] = info['bytes']\r
157     return response\r
158 \r
159 @api_method('PUT')\r
160 def container_create(request, v_account, v_container):\r
161     # Normal Response Codes: 201, 202\r
162     # Error Response Codes: serviceUnavailable (503),\r
163     #                       itemNotFound (404),\r
164     #                       unauthorized (401),\r
165     #                       badRequest (400)\r
166 \r
167     be = BackEnd(STORAGE_PATH)\r
168     try:\r
169         be.create_container(request.user, v_container)\r
170         return HttpResponse(status = 201)\r
171     except NameError:\r
172         return HttpResponse(status = 202)\r
173 \r
174 @api_method('DELETE')\r
175 def container_delete(request, v_account, v_container):\r
176     # Normal Response Codes: 204\r
177     # Error Response Codes: serviceUnavailable (503),\r
178     #                       itemNotFound (404),\r
179     #                       unauthorized (401),\r
180     #                       badRequest (400)\r
181     \r
182     be = BackEnd(STORAGE_PATH)\r
183     try:\r
184         info = be.get_container_meta(request.user, v_container)\r
185     except NameError:\r
186         raise ItemNotFound()\r
187     \r
188     if info['count'] > 0:\r
189         return HttpResponse(status = 409)\r
190     \r
191     # TODO: Handle both exceptions.\r
192     try:\r
193         be.delete_container(request.user, v_container)\r
194     except:\r
195         raise ItemNotFound()\r
196     return HttpResponse(status = 204)\r
197 \r
198 # --- UP TO HERE ---\r
199 \r
200 @api_method('GET', format_allowed = True)\r
201 def object_list(request, v_account, v_container):\r
202     # Normal Response Codes: 200, 204\r
203     # Error Response Codes: serviceUnavailable (503),\r
204     #                       itemNotFound (404),\r
205     #                       unauthorized (401),\r
206     #                       badRequest (400)\r
207     \r
208     path = request.GET.get('path')\r
209     prefix = request.GET.get('prefix')\r
210     delimiter = request.GET.get('delimiter')\r
211     logging.debug("path: %s", path)\r
212     \r
213     # Path overrides prefix and delimiter.\r
214     if path:\r
215         prefix = path\r
216         delimiter = '/'\r
217     # Naming policy.\r
218     if prefix and delimiter:\r
219         prefix = prefix + delimiter\r
220     \r
221     marker = request.GET.get('marker')\r
222     limit = request.GET.get('limit')\r
223     if limit:\r
224         try:\r
225             limit = int(limit)\r
226         except ValueError:\r
227             limit = None\r
228     \r
229     objects = list_objects(request.user, v_container, prefix, delimiter, marker, limit)\r
230     if len(objects) == 0:\r
231         return HttpResponse(status = 204)\r
232     \r
233     if request.serialization == 'xml':\r
234         data = render_to_string('objects.xml', {'container': v_container, 'objects': objects})\r
235     elif request.serialization  == 'json':\r
236         data = json.dumps(objects)\r
237     else:\r
238         data = '\n'.join(x['name'] for x in objects)\r
239     \r
240     return HttpResponse(data, status = 200)\r
241 \r
242 @api_method('HEAD')\r
243 def object_meta(request, v_account, v_container, v_object):\r
244     # Normal Response Codes: 204\r
245     # Error Response Codes: serviceUnavailable (503),\r
246     #                       itemNotFound (404),\r
247     #                       unauthorized (401),\r
248     #                       badRequest (400)\r
249 \r
250     info = get_object_meta(request.user, v_container, v_object)\r
251     \r
252     response = HttpResponse(status = 204)\r
253     response['ETag'] = info['hash']\r
254     response['Content-Length'] = info['bytes']\r
255     response['Content-Type'] = info['content_type']\r
256     response['Last-Modified'] = http_date(info['last_modified'])\r
257     for k, v in info['meta'].iteritems():\r
258         response['X-Object-Meta-%s' % k.capitalize()] = v\r
259     \r
260     return response\r
261 \r
262 @api_method('GET')\r
263 def object_read(request, v_account, v_container, v_object):\r
264     # Normal Response Codes: 200, 206\r
265     # Error Response Codes: serviceUnavailable (503),\r
266     #                       rangeNotSatisfiable (416),\r
267     #                       preconditionFailed (412),\r
268     #                       itemNotFound (404),\r
269     #                       unauthorized (401),\r
270     #                       badRequest (400),\r
271     #                       notModified (304)\r
272     \r
273     info = get_object_meta(request.user, v_container, v_object)\r
274     \r
275     response = HttpResponse()\r
276     response['ETag'] = info['hash']\r
277     response['Content-Type'] = info['content_type']\r
278     response['Last-Modified'] = http_date(info['last_modified'])\r
279     \r
280     # Range handling.\r
281     range = get_range(request)\r
282     if range is not None:\r
283         offset, length = range\r
284         if not length:\r
285             length = 0\r
286         if offset + length > info['bytes']:\r
287             raise RangeNotSatisfiable()\r
288         \r
289         response['Content-Length'] = length        \r
290         response.status_code = 206\r
291     else:\r
292         offset = 0\r
293         length = 0\r
294         \r
295         response['Content-Length'] = info['bytes']\r
296         response.status_code = 200\r
297     \r
298     # Conditions (according to RFC2616 must be evaluated at the end).\r
299     if_match = request.META.get('HTTP_IF_MATCH')\r
300     if if_match is not None and if_match != '*':\r
301         if info['hash'] not in parse_etags(if_match):\r
302             raise PreconditionFailed()\r
303     \r
304     if_none_match = request.META.get('HTTP_IF_NONE_MATCH')\r
305 #     if if_none_match is not None:\r
306 #         if if_none_match = '*' or info['hash'] in parse_etags(if_none_match):\r
307 #             raise NotModified()\r
308     \r
309     if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')\r
310     if if_modified_since is not None:\r
311         if_modified_since = parse_http_date_safe(if_modified_since)\r
312     if if_modified_since is not None and info['last_modified'] <= if_modified_since:\r
313         raise NotModified()\r
314 \r
315     if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')\r
316     if if_unmodified_since is not None:\r
317         if_unmodified_since = parse_http_date_safe(if_unmodified_since)\r
318     if if_unmodified_since is not None and info['last_modified'] > if_unmodified_since:\r
319         raise PreconditionFailed()\r
320     \r
321     response.content = get_object_data(request.user, v_container, v_object, offset, length)\r
322     return response\r
323 \r
324 @api_method('PUT')\r
325 def object_write(request, v_account, v_container, v_object):\r
326     # Normal Response Codes: 201\r
327     # Error Response Codes: serviceUnavailable (503),\r
328     #                       unprocessableEntity (422),\r
329     #                       lengthRequired (411),\r
330     #                       itemNotFound (404),\r
331     #                       unauthorized (401),\r
332     #                       badRequest (400)\r
333     \r
334     copy_from = request.META.get('HTTP_X_COPY_FROM')\r
335     if copy_from:\r
336         parts = copy_from.split('/')\r
337         if len(parts) < 3 or parts[0] != '':\r
338             raise BadRequest('Bad X-Copy-From path.')\r
339         copy_container = parts[1]\r
340         copy_name = '/'.join(parts[2:])\r
341         \r
342         info = get_object_meta(request.user, copy_container, copy_name)\r
343         \r
344         content_length = request.META.get('CONTENT_LENGTH')\r
345         content_type = request.META.get('CONTENT_TYPE')\r
346         if not content_length:\r
347             raise LengthRequired()\r
348         if content_type:\r
349             info['content_type'] = content_type\r
350         \r
351         meta = get_object_meta(request)\r
352         for k, v in meta.iteritems():\r
353             info['meta'][k] = v\r
354         \r
355         copy_object(request.user, copy_container, copy_name, v_container, v_object)\r
356         update_object_meta(request.user, v_container, v_object, info)\r
357         \r
358         response = HttpResponse(status = 201)\r
359     else:\r
360         content_length = request.META.get('CONTENT_LENGTH')\r
361         content_type = request.META.get('CONTENT_TYPE')\r
362         if not content_length or not content_type:\r
363             raise LengthRequired()\r
364     \r
365         meta = get_object_meta(request)\r
366         info = {'bytes': content_length, 'content_type': content_type, 'meta': meta}\r
367     \r
368         etag = request.META.get('HTTP_ETAG')\r
369         if etag:\r
370             etag = parse_etags(etag)[0] # TODO: Unescape properly.\r
371             info['hash'] = etag\r
372     \r
373         data = request.read()\r
374         # TODO: Hash function.\r
375         # etag = hash(data)\r
376         # if info.get('hash') and info['hash'] != etag:\r
377         #     raise UnprocessableEntity()\r
378     \r
379         update_object_data(request.user, v_container, v_name, info, data)\r
380     \r
381         response = HttpResponse(status = 201)\r
382         # response['ETag'] = etag\r
383     \r
384     return response\r
385 \r
386 @api_method('COPY')\r
387 def object_copy(request, v_account, v_container, v_object):\r
388     # Normal Response Codes: 201\r
389     # Error Response Codes: serviceUnavailable (503),\r
390     #                       itemNotFound (404),\r
391     #                       unauthorized (401),\r
392     #                       badRequest (400)\r
393     \r
394     destination = request.META.get('HTTP_DESTINATION')\r
395     if not destination:\r
396         raise BadRequest('Missing Destination.');\r
397     \r
398     parts = destination.split('/')\r
399     if len(parts) < 3 or parts[0] != '':\r
400         raise BadRequest('Bad Destination path.')\r
401     dest_container = parts[1]\r
402     dest_name = '/'.join(parts[2:])\r
403         \r
404     info = get_object_meta(request.user, v_container, v_object)\r
405         \r
406     content_type = request.META.get('CONTENT_TYPE')\r
407     if content_type:\r
408         info['content_type'] = content_type\r
409         \r
410     meta = get_object_meta(request)\r
411     for k, v in meta.iteritems():\r
412         info['meta'][k] = v\r
413     \r
414     copy_object(request.user, v_container, v_object, dest_container, dest_name)\r
415     update_object_meta(request.user, dest_container, dest_name, info)\r
416     \r
417     response = HttpResponse(status = 201)\r
418 \r
419 @api_method('POST')\r
420 def object_update(request, v_account, v_container, v_object):\r
421     # Normal Response Codes: 202\r
422     # Error Response Codes: serviceUnavailable (503),\r
423     #                       itemNotFound (404),\r
424     #                       unauthorized (401),\r
425     #                       badRequest (400)\r
426     \r
427     meta = get_object_meta(request)\r
428     \r
429     update_object_meta(request.user, v_container, v_object, meta)\r
430     return HttpResponse(status = 202)\r
431 \r
432 @api_method('DELETE')\r
433 def object_delete(request, v_account, v_container, v_object):\r
434     # Normal Response Codes: 204\r
435     # Error Response Codes: serviceUnavailable (503),\r
436     #                       itemNotFound (404),\r
437     #                       unauthorized (401),\r
438     #                       badRequest (400)\r
439     \r
440     delete_object(request.user, v_container, v_object)\r
441     return HttpResponse(status = 204)\r
442 \r
443 @api_method()\r
444 def method_not_allowed(request):\r
445     raise BadRequest('Method not allowed.')\r