ldp.py 7.4 KB

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