ldp.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import logging
  2. from collections import defaultdict
  3. from uuid import uuid4
  4. from flask import Blueprint, request, send_file
  5. from werkzeug.datastructures import FileStorage
  6. from lakesuperior.exceptions import InvalidResourceError, \
  7. ResourceExistsError, ResourceNotExistsError, \
  8. InvalidResourceError, ServerManagedTermError
  9. from lakesuperior.model.ldp_rs import Ldpr, Ldpc, LdpRs
  10. from lakesuperior.model.ldp_nr import LdpNr
  11. from lakesuperior.store_layouts.rdf.base_rdf_layout import BaseRdfLayout
  12. from lakesuperior.util.translator import Translator
  13. logger = logging.getLogger(__name__)
  14. # Blueprint for LDP REST API. This is what is usually found under `/rest/` in
  15. # standard fcrepo4. Here, it is under `/ldp` but initially `/rest` can be kept
  16. # for backward compatibility.
  17. ldp = Blueprint('ldp', __name__)
  18. accept_patch = (
  19. 'application/sparql-update',
  20. )
  21. accept_rdf = (
  22. 'application/ld+json',
  23. 'application/n-triples',
  24. 'application/rdf+xml',
  25. #'application/x-turtle',
  26. #'application/xhtml+xml',
  27. #'application/xml',
  28. #'text/html',
  29. 'text/n3',
  30. #'text/plain',
  31. 'text/rdf+n3',
  32. 'text/turtle',
  33. )
  34. #allow = (
  35. # 'COPY',
  36. # 'DELETE',
  37. # 'GET',
  38. # 'HEAD',
  39. # 'MOVE',
  40. # 'OPTIONS',
  41. # 'PATCH',
  42. # 'POST',
  43. # 'PUT',
  44. #)
  45. std_headers = {
  46. 'Accept-Patch' : ','.join(accept_patch),
  47. 'Accept-Post' : ','.join(accept_rdf),
  48. #'Allow' : ','.join(allow),
  49. }
  50. ## REST SERVICES ##
  51. @ldp.route('/<path:uuid>', methods=['GET'])
  52. @ldp.route('/', defaults={'uuid': None}, methods=['GET'],
  53. strict_slashes=False)
  54. def get_resource(uuid):
  55. '''
  56. Retrieve RDF or binary content.
  57. '''
  58. rsrc = Ldpr.readonly_inst(uuid)
  59. if isinstance(rsrc, LdpRs) or request.headers['accept'] in accept_rdf:
  60. return _get_rdf(rsrc)
  61. else:
  62. return _get_bitstream(rsrc)
  63. @ldp.route('/<path:uuid>/fcr:metadata', methods=['GET'])
  64. def get_metadata(uuid):
  65. '''
  66. Retrieve RDF metadata of a LDP-NR.
  67. '''
  68. return _get_rdf(LdpRs(uuid))
  69. @ldp.route('/<path:parent>', methods=['POST'])
  70. @ldp.route('/', defaults={'parent': None}, methods=['POST'],
  71. strict_slashes=False)
  72. def post_resource(parent):
  73. '''
  74. Add a new resource in a new URI.
  75. '''
  76. out_headers = std_headers
  77. try:
  78. slug = request.headers['Slug']
  79. except KeyError:
  80. slug = None
  81. cls, data = class_from_req_body()
  82. try:
  83. rsrc = cls.inst_for_post(parent, slug)
  84. except ResourceNotExistsError as e:
  85. return str(e), 404
  86. except InvalidResourceError as e:
  87. return str(e), 409
  88. if cls == LdpNr:
  89. try:
  90. cont_disp = Translator.parse_rfc7240(
  91. request.headers['content-disposition'])
  92. except KeyError:
  93. cont_disp = None
  94. rsrc.post(data, mimetype=request.content_type, disposition=cont_disp)
  95. else:
  96. try:
  97. rsrc.post(data)
  98. except ServerManagedTermError as e:
  99. return str(e), 412
  100. out_headers.update({
  101. 'Location' : rsrc.uri,
  102. })
  103. return rsrc.uri, out_headers, 201
  104. @ldp.route('/<path:uuid>', methods=['PUT'])
  105. def put_resource(uuid):
  106. '''
  107. Add a new resource at a specified URI.
  108. '''
  109. logger.info('Request headers: {}'.format(request.headers))
  110. rsp_headers = std_headers
  111. cls, data = class_from_req_body()
  112. rsrc = cls(uuid)
  113. # Parse headers.
  114. pref_handling = None
  115. if cls == LdpNr:
  116. try:
  117. logger.debug('Headers: {}'.format(request.headers))
  118. cont_disp = Translator.parse_rfc7240(
  119. request.headers['content-disposition'])
  120. except KeyError:
  121. cont_disp = None
  122. try:
  123. ret = rsrc.put(data, disposition=cont_disp)
  124. except InvalidResourceError as e:
  125. return str(e), 409
  126. except ResourceExistsError as e:
  127. return str(e), 409
  128. else:
  129. if 'prefer' in request.headers:
  130. prefer = Translator.parse_rfc7240(request.headers['prefer'])
  131. logger.debug('Parsed Prefer header: {}'.format(prefer))
  132. if 'handling' in prefer:
  133. pref_handling = prefer['handling']['value']
  134. try:
  135. ret = rsrc.put(data, handling=pref_handling)
  136. except InvalidResourceError as e:
  137. return str(e), 409
  138. except ResourceExistsError as e:
  139. return str(e), 409
  140. except ServerManagedTermError as e:
  141. return str(e), 412
  142. res_code = 201 if ret == BaseRdfLayout.RES_CREATED else 204
  143. return '', res_code, rsp_headers
  144. @ldp.route('/<path:uuid>', methods=['PATCH'])
  145. def patch_resource(uuid):
  146. '''
  147. Update an existing resource with a SPARQL-UPDATE payload.
  148. '''
  149. headers = std_headers
  150. rsrc = Ldpc(uuid)
  151. try:
  152. rsrc.patch(request.get_data().decode('utf-8'))
  153. except ResourceNotExistsError:
  154. return 'Resource #{} not found.'.format(rsrc.uuid), 404
  155. except ServerManagedTermError as e:
  156. return str(e), 412
  157. return '', 204, headers
  158. @ldp.route('/<path:uuid>', methods=['DELETE'])
  159. def delete_resource(uuid):
  160. '''
  161. Delete a resource.
  162. '''
  163. headers = std_headers
  164. rsrc = Ldpc(uuid)
  165. try:
  166. rsrc.delete()
  167. except ResourceNotExistsError:
  168. return 'Resource #{} not found.'.format(rsrc.uuid), 404
  169. return '', 204, headers
  170. def class_from_req_body():
  171. logger.debug('Content type: {}'.format(request.mimetype))
  172. logger.debug('files: {}'.format(request.files))
  173. logger.debug('stream: {}'.format(request.stream))
  174. if request.mimetype in accept_rdf:
  175. cls = Ldpc
  176. # Parse out the RDF string.
  177. data = request.data.decode('utf-8')
  178. else:
  179. cls = LdpNr
  180. if request.mimetype == 'multipart/form-data':
  181. # This seems the "right" way to upload a binary file, with a
  182. # multipart/form-data MIME type and the file in the `file` field.
  183. # This however is not supported by FCREPO4.
  184. data = request.files.get('file').stream
  185. else:
  186. # This is a less clean way, with the file in the form body and the
  187. # request as application/x-www-form-urlencoded.
  188. # This is how FCREPO4 accepts binary uploads.
  189. data = request.stream
  190. logger.info('POSTing resource of type: {}'.format(cls.__name__))
  191. #logger.info('POST data: {}'.format(data))
  192. return cls, data
  193. def _get_rdf(rsrc):
  194. '''
  195. Get the RDF representation of a resource.
  196. @param rsrc An in-memory resource.
  197. '''
  198. out_headers = std_headers
  199. pref_return = defaultdict(dict)
  200. if 'prefer' in request.headers:
  201. prefer = Translator.parse_rfc7240(request.headers['prefer'])
  202. logger.debug('Parsed Prefer header: {}'.format(prefer))
  203. if 'return' in prefer:
  204. pref_return = prefer['return']
  205. try:
  206. imr = rsrc.get('rdf', pref_return=pref_return)
  207. logger.debug('GET RDF: {}'.format(imr))
  208. except ResourceNotExistsError as e:
  209. return str(e), 404
  210. else:
  211. out_headers.update(rsrc.head())
  212. return (imr.graph.serialize(format='turtle'), out_headers)
  213. def _get_bitstream(rsrc):
  214. out_headers = std_headers
  215. # @TODO This may change in favor of more low-level handling if the file
  216. # system is not local.
  217. return send_file(rsrc.local_path, as_attachment=True,
  218. attachment_filename=rsrc.filename)