ldp.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679
  1. import logging
  2. import pdb
  3. from base64 import b64encode
  4. from collections import defaultdict
  5. from io import BytesIO
  6. from pprint import pformat
  7. from uuid import uuid4
  8. import arrow
  9. from flask import (
  10. Blueprint, Response, g, make_response, render_template,
  11. request, send_file)
  12. from rdflib import Graph, plugin, parser#, serializer
  13. from lakesuperior.api import resource as rsrc_api
  14. from lakesuperior.dictionaries.namespaces import ns_collection as nsc
  15. from lakesuperior.dictionaries.namespaces import ns_mgr as nsm
  16. from lakesuperior.exceptions import (ResourceNotExistsError, TombstoneError,
  17. ServerManagedTermError, InvalidResourceError, SingleSubjectError,
  18. ResourceExistsError, IncompatibleLdpTypeError)
  19. from lakesuperior.globals import RES_CREATED
  20. from lakesuperior.model.ldp_factory import LdpFactory
  21. from lakesuperior.model.ldp_nr import LdpNr
  22. from lakesuperior.model.ldp_rs import LdpRs
  23. from lakesuperior.model.ldpr import Ldpr
  24. from lakesuperior.store.ldp_rs.lmdb_store import TxnManager
  25. from lakesuperior.store.ldp_rs.metadata_store import MetadataStore
  26. from lakesuperior.toolbox import Toolbox
  27. DEFAULT_RDF_MIMETYPE = 'text/turtle'
  28. """
  29. Fallback serialization format used when no acceptable formats are specified.
  30. """
  31. logger = logging.getLogger(__name__)
  32. rdf_parsable_mimetypes = {
  33. mt.name for mt in plugin.plugins()
  34. if mt.kind is parser.Parser and '/' in mt.name
  35. }
  36. """MIMEtypes that can be parsed into RDF."""
  37. rdf_serializable_mimetypes = {
  38. #mt.name for mt in plugin.plugins()
  39. #if mt.kind is serializer.Serializer and '/' in mt.name
  40. 'application/ld+json',
  41. 'application/n-triples',
  42. 'application/rdf+xml',
  43. 'text/turtle',
  44. 'text/n3',
  45. }
  46. """
  47. MIMEtypes that RDF can be serialized into.
  48. These are not automatically derived from RDFLib because only triple
  49. (not quad) serializations are applicable.
  50. """
  51. accept_patch = (
  52. 'application/sparql-update',
  53. )
  54. std_headers = {
  55. 'Accept-Patch' : ','.join(accept_patch),
  56. 'Accept-Post' : ','.join(rdf_parsable_mimetypes),
  57. }
  58. """Predicates excluded by view."""
  59. vw_blacklist = {
  60. }
  61. ldp = Blueprint(
  62. 'ldp', __name__, template_folder='templates',
  63. static_url_path='/static', static_folder='templates/static')
  64. """
  65. Blueprint for LDP REST API. This is what is usually found under ``/rest/`` in
  66. standard fcrepo4. Here, it is under ``/ldp`` but initially ``/rest`` will be
  67. kept for backward compatibility.
  68. """
  69. ## ROUTE PRE- & POST-PROCESSING ##
  70. @ldp.url_defaults
  71. def bp_url_defaults(endpoint, values):
  72. url_prefix = getattr(g, 'url_prefix', None)
  73. if url_prefix is not None:
  74. values.setdefault('url_prefix', url_prefix)
  75. @ldp.url_value_preprocessor
  76. def bp_url_value_preprocessor(endpoint, values):
  77. g.url_prefix = values.pop('url_prefix')
  78. g.webroot = request.host_url + g.url_prefix
  79. # Normalize leading slashes for UID.
  80. if 'uid' in values:
  81. values['uid'] = '/' + values['uid'].lstrip('/')
  82. if 'parent_uid' in values:
  83. values['parent_uid'] = '/' + values['parent_uid'].lstrip('/')
  84. @ldp.before_request
  85. def log_request_start():
  86. logger.info('** Start {} {} **'.format(request.method, request.url))
  87. @ldp.before_request
  88. def instantiate_req_vars():
  89. g.tbox = Toolbox()
  90. @ldp.after_request
  91. def log_request_end(rsp):
  92. logger.info('** End {} {} **'.format(request.method, request.url))
  93. return rsp
  94. ## REST SERVICES ##
  95. @ldp.route('/<path:uid>', methods=['GET'], strict_slashes=False)
  96. @ldp.route('/', defaults={'uid': '/'}, methods=['GET'], strict_slashes=False)
  97. @ldp.route('/<path:uid>/fcr:metadata', defaults={'out_fmt' : 'rdf'},
  98. methods=['GET'])
  99. @ldp.route('/<path:uid>/fcr:content', defaults={'out_fmt' : 'non_rdf'},
  100. methods=['GET'])
  101. def get_resource(uid, out_fmt=None):
  102. r"""
  103. https://www.w3.org/TR/ldp/#ldpr-HTTP_GET
  104. Retrieve RDF or binary content.
  105. :param str uid: UID of resource to retrieve. The repository root has
  106. an empty string for UID.
  107. :param str out_fmt: Force output to RDF or non-RDF if the resource is
  108. a LDP-NR. This is not available in the API but is used e.g. by the
  109. ``\*/fcr:metadata`` and ``\*/fcr:content`` endpoints. The default is
  110. False.
  111. """
  112. logger.info('UID: {}'.format(uid))
  113. out_headers = std_headers
  114. repr_options = defaultdict(dict)
  115. if 'prefer' in request.headers:
  116. prefer = g.tbox.parse_rfc7240(request.headers['prefer'])
  117. logger.debug('Parsed Prefer header: {}'.format(pformat(prefer)))
  118. if 'return' in prefer:
  119. repr_options = parse_repr_options(prefer['return'])
  120. try:
  121. rsrc = rsrc_api.get(uid, repr_options)
  122. except ResourceNotExistsError as e:
  123. return str(e), 404
  124. except TombstoneError as e:
  125. return _tombstone_response(e, uid)
  126. else:
  127. if out_fmt is None:
  128. rdf_mimetype = _best_rdf_mimetype()
  129. out_fmt = (
  130. 'rdf'
  131. if isinstance(rsrc, LdpRs) or rdf_mimetype is not None
  132. else 'non_rdf')
  133. out_headers.update(_headers_from_metadata(rsrc, out_fmt))
  134. uri = g.tbox.uid_to_uri(uid)
  135. if out_fmt == 'rdf':
  136. if locals().get('rdf_mimetype', None) is None:
  137. rdf_mimetype = DEFAULT_RDF_MIMETYPE
  138. ggr = g.tbox.globalize_graph(rsrc.out_graph)
  139. ggr.namespace_manager = nsm
  140. return _negotiate_content(
  141. ggr, rdf_mimetype, out_headers, uid=uid, uri=uri)
  142. else:
  143. if not getattr(rsrc, 'local_path', False):
  144. return ('{} has no binary content.'.format(rsrc.uid), 404)
  145. logger.debug('Streaming out binary content.')
  146. rsp = make_response(send_file(
  147. rsrc.local_path, as_attachment=True,
  148. attachment_filename=rsrc.filename,
  149. mimetype=rsrc.mimetype), 200, out_headers)
  150. rsp.headers.add('Link',
  151. '<{}/fcr:metadata>; rel="describedby"'.format(uri))
  152. return rsp
  153. @ldp.route('/<path:uid>/fcr:versions', methods=['GET'])
  154. def get_version_info(uid):
  155. """
  156. Get version info (`fcr:versions`).
  157. :param str uid: UID of resource to retrieve versions for.
  158. """
  159. rdf_mimetype = _best_rdf_mimetype() or DEFAULT_RDF_MIMETYPE
  160. try:
  161. gr = rsrc_api.get_version_info(uid)
  162. except ResourceNotExistsError as e:
  163. return str(e), 404
  164. except InvalidResourceError as e:
  165. return str(e), 409
  166. except TombstoneError as e:
  167. return _tombstone_response(e, uid)
  168. else:
  169. return _negotiate_content(g.tbox.globalize_graph(gr), rdf_mimetype)
  170. @ldp.route('/<path:uid>/fcr:versions/<ver_uid>', methods=['GET'])
  171. def get_version(uid, ver_uid):
  172. """
  173. Get an individual resource version.
  174. :param str uid: Resource UID.
  175. :param str ver_uid: Version UID.
  176. """
  177. rdf_mimetype = _best_rdf_mimetype() or DEFAULT_RDF_MIMETYPE
  178. try:
  179. gr = rsrc_api.get_version(uid, ver_uid)
  180. except ResourceNotExistsError as e:
  181. return str(e), 404
  182. except InvalidResourceError as e:
  183. return str(e), 409
  184. except TombstoneError as e:
  185. return _tombstone_response(e, uid)
  186. else:
  187. return _negotiate_content(g.tbox.globalize_graph(gr), rdf_mimetype)
  188. @ldp.route('/<path:parent_uid>', methods=['POST'], strict_slashes=False)
  189. @ldp.route('/', defaults={'parent_uid': '/'}, methods=['POST'],
  190. strict_slashes=False)
  191. def post_resource(parent_uid):
  192. """
  193. https://www.w3.org/TR/ldp/#ldpr-HTTP_POST
  194. Add a new resource in a new URI.
  195. """
  196. rsp_headers = std_headers
  197. try:
  198. slug = request.headers['Slug']
  199. logger.debug('Slug: {}'.format(slug))
  200. except KeyError:
  201. slug = None
  202. handling, disposition = set_post_put_params()
  203. stream, mimetype = _bistream_from_req()
  204. if mimetype in rdf_parsable_mimetypes:
  205. # If the content is RDF, localize in-repo URIs.
  206. global_rdf = stream.read()
  207. rdf_data = g.tbox.localize_payload(global_rdf)
  208. rdf_fmt = mimetype
  209. stream = mimetype = None
  210. else:
  211. rdf_data = rdf_fmt = None
  212. try:
  213. uid = rsrc_api.create(
  214. parent_uid, slug, stream=stream, mimetype=mimetype,
  215. rdf_data=rdf_data, rdf_fmt=rdf_fmt, handling=handling,
  216. disposition=disposition)
  217. except ResourceNotExistsError as e:
  218. return str(e), 404
  219. except InvalidResourceError as e:
  220. return str(e), 409
  221. except TombstoneError as e:
  222. return _tombstone_response(e, uid)
  223. except ServerManagedTermError as e:
  224. return str(e), 412
  225. uri = g.tbox.uid_to_uri(uid)
  226. hdr = {'Location' : uri}
  227. if mimetype and rdf_fmt is None:
  228. hdr['Link'] = '<{0}/fcr:metadata>; rel="describedby"; anchor="{0}"'\
  229. .format(uri)
  230. rsp_headers.update(hdr)
  231. rsp_headers.update(_digest_headers(nsc['fcres'][uid]))
  232. return uri, 201, rsp_headers
  233. @ldp.route('/<path:uid>', methods=['PUT'], strict_slashes=False)
  234. @ldp.route('/<path:uid>/fcr:metadata', defaults={'force_rdf' : True},
  235. methods=['PUT'])
  236. def put_resource(uid):
  237. """
  238. https://www.w3.org/TR/ldp/#ldpr-HTTP_PUT
  239. Add or replace a new resource at a specified URI.
  240. """
  241. # Parse headers.
  242. logger.debug('Request headers: {}'.format(request.headers))
  243. rsp_headers = {'Content-Type' : 'text/plain; charset=utf-8'}
  244. handling, disposition = set_post_put_params()
  245. stream, mimetype = _bistream_from_req()
  246. if mimetype in rdf_parsable_mimetypes:
  247. # If the content is RDF, localize in-repo URIs.
  248. global_rdf = stream.read()
  249. rdf_data = g.tbox.localize_payload(global_rdf)
  250. rdf_fmt = mimetype
  251. stream = mimetype = None
  252. else:
  253. rdf_data = rdf_fmt = None
  254. try:
  255. evt = rsrc_api.create_or_replace(
  256. uid, stream=stream, mimetype=mimetype,
  257. rdf_data=rdf_data, rdf_fmt=rdf_fmt, handling=handling,
  258. disposition=disposition)
  259. except (InvalidResourceError, ResourceExistsError) as e:
  260. return str(e), 409
  261. except (ServerManagedTermError, SingleSubjectError) as e:
  262. return str(e), 412
  263. except IncompatibleLdpTypeError as e:
  264. return str(e), 415
  265. except TombstoneError as e:
  266. return _tombstone_response(e, uid)
  267. uri = g.tbox.uid_to_uri(uid)
  268. if evt == RES_CREATED:
  269. rsp_code = 201
  270. rsp_headers['Location'] = rsp_body = uri
  271. if mimetype and not rdf_data:
  272. rsp_headers['Link'] = (
  273. '<{0}/fcr:metadata>; rel="describedby"'.format(uri))
  274. else:
  275. rsp_code = 204
  276. rsp_body = ''
  277. rsp_headers.update(_digest_headers(nsc['fcres'][uid]))
  278. return rsp_body, rsp_code, rsp_headers
  279. @ldp.route('/<path:uid>', methods=['PATCH'], strict_slashes=False)
  280. @ldp.route('/', defaults={'uid': '/'}, methods=['PATCH'],
  281. strict_slashes=False)
  282. def patch_resource(uid, is_metadata=False):
  283. """
  284. https://www.w3.org/TR/ldp/#ldpr-HTTP_PATCH
  285. Update an existing resource with a SPARQL-UPDATE payload.
  286. """
  287. rsp_headers = {'Content-Type' : 'text/plain; charset=utf-8'}
  288. if request.mimetype != 'application/sparql-update':
  289. return 'Provided content type is not a valid parsable format: {}'\
  290. .format(request.mimetype), 415
  291. update_str = request.get_data().decode('utf-8')
  292. local_update_str = g.tbox.localize_ext_str(update_str, nsc['fcres'][uid])
  293. try:
  294. rsrc = rsrc_api.update(uid, local_update_str, is_metadata)
  295. except ResourceNotExistsError as e:
  296. return str(e), 404
  297. except TombstoneError as e:
  298. return _tombstone_response(e, uid)
  299. except (ServerManagedTermError, SingleSubjectError) as e:
  300. return str(e), 412
  301. except InvalidResourceError as e:
  302. return str(e), 415
  303. else:
  304. rsp_headers.update(_headers_from_metadata(rsrc))
  305. return '', 204, rsp_headers
  306. @ldp.route('/<path:uid>/fcr:metadata', methods=['PATCH'])
  307. def patch_resource_metadata(uid):
  308. return patch_resource(uid, True)
  309. @ldp.route('/<path:uid>', methods=['DELETE'])
  310. def delete_resource(uid):
  311. """
  312. Delete a resource and optionally leave a tombstone.
  313. This behaves differently from FCREPO. A tombstone indicated that the
  314. resource is no longer available at its current location, but its historic
  315. snapshots still are. Also, deleting a resource with a tombstone creates
  316. one more version snapshot of the resource prior to being deleted.
  317. In order to completely wipe out all traces of a resource, the tombstone
  318. must be deleted as well, or the ``Prefer:no-tombstone`` header can be used.
  319. The latter will forget (completely delete) the resource immediately.
  320. """
  321. headers = std_headers
  322. if 'prefer' in request.headers:
  323. prefer = g.tbox.parse_rfc7240(request.headers['prefer'])
  324. leave_tstone = 'no-tombstone' not in prefer
  325. else:
  326. leave_tstone = True
  327. try:
  328. rsrc_api.delete(uid, leave_tstone)
  329. except ResourceNotExistsError as e:
  330. return str(e), 404
  331. except TombstoneError as e:
  332. return _tombstone_response(e, uid)
  333. return '', 204, headers
  334. @ldp.route('/<path:uid>/fcr:tombstone', methods=['GET', 'POST', 'PUT',
  335. 'PATCH', 'DELETE'])
  336. def tombstone(uid):
  337. """
  338. Handle all tombstone operations.
  339. The only allowed methods are POST and DELETE; any other verb will return a
  340. 405.
  341. """
  342. try:
  343. rsrc = rsrc_api.get(uid)
  344. except TombstoneError as e:
  345. if request.method == 'DELETE':
  346. if e.uid == uid:
  347. rsrc_api.delete(uid, False)
  348. return '', 204
  349. else:
  350. return _tombstone_response(e, uid)
  351. elif request.method == 'POST':
  352. if e.uid == uid:
  353. rsrc_uri = rsrc_api.resurrect(uid)
  354. headers = {'Location' : rsrc_uri}
  355. return rsrc_uri, 201, headers
  356. else:
  357. return _tombstone_response(e, uid)
  358. else:
  359. return 'Method Not Allowed.', 405
  360. except ResourceNotExistsError as e:
  361. return str(e), 404
  362. else:
  363. return '', 404
  364. @ldp.route('/<path:uid>/fcr:versions', methods=['POST', 'PUT'])
  365. def post_version(uid):
  366. """
  367. Create a new resource version.
  368. """
  369. if request.method == 'PUT':
  370. return 'Method not allowed.', 405
  371. ver_uid = request.headers.get('slug', None)
  372. try:
  373. ver_uid = rsrc_api.create_version(uid, ver_uid)
  374. except ResourceNotExistsError as e:
  375. return str(e), 404
  376. except InvalidResourceError as e:
  377. return str(e), 409
  378. except TombstoneError as e:
  379. return _tombstone_response(e, uid)
  380. else:
  381. return '', 201, {'Location': g.tbox.uid_to_uri(ver_uid)}
  382. @ldp.route('/<path:uid>/fcr:versions/<ver_uid>', methods=['PATCH'])
  383. def patch_version(uid, ver_uid):
  384. """
  385. Revert to a previous version.
  386. NOTE: This creates a new version snapshot.
  387. :param str uid: Resource UID.
  388. :param str ver_uid: Version UID.
  389. """
  390. try:
  391. rsrc_api.revert_to_version(uid, rsrc_uid)
  392. except ResourceNotExistsError as e:
  393. return str(e), 404
  394. except InvalidResourceError as e:
  395. return str(e), 409
  396. except TombstoneError as e:
  397. return _tombstone_response(e, uid)
  398. else:
  399. return '', 204
  400. ## PRIVATE METHODS ##
  401. def _best_rdf_mimetype():
  402. """
  403. Check if any of the 'Accept' header values provided is a RDF parsable
  404. format.
  405. """
  406. for accept in request.accept_mimetypes:
  407. mimetype = accept[0]
  408. if mimetype in rdf_parsable_mimetypes:
  409. return mimetype
  410. return None
  411. def _negotiate_content(gr, rdf_mimetype, headers=None, **vw_kwargs):
  412. """
  413. Return HTML or serialized RDF depending on accept headers.
  414. """
  415. if request.accept_mimetypes.best == 'text/html':
  416. return render_template(
  417. 'resource.html', gr=gr, nsc=nsc, nsm=nsm,
  418. blacklist=vw_blacklist, arrow=arrow, **vw_kwargs)
  419. else:
  420. for p in vw_blacklist:
  421. gr.remove((None, p, None))
  422. return Response(
  423. gr.serialize(format=rdf_mimetype), 200, headers,
  424. mimetype=rdf_mimetype)
  425. def _bistream_from_req():
  426. """
  427. Find how a binary file and its MIMEtype were uploaded in the request.
  428. """
  429. #logger.debug('Content type: {}'.format(request.mimetype))
  430. #logger.debug('files: {}'.format(request.files))
  431. #logger.debug('stream: {}'.format(request.stream))
  432. if request.mimetype == 'multipart/form-data':
  433. # This seems the "right" way to upload a binary file, with a
  434. # multipart/form-data MIME type and the file in the `file`
  435. # field. This however is not supported by FCREPO4.
  436. stream = request.files.get('file').stream
  437. mimetype = request.files.get('file').content_type
  438. # @TODO This will turn out useful to provide metadata
  439. # with the binary.
  440. #metadata = request.files.get('metadata').stream
  441. else:
  442. # This is a less clean way, with the file in the form body and
  443. # the request as application/x-www-form-urlencoded.
  444. # This is how FCREPO4 accepts binary uploads.
  445. stream = request.stream
  446. # @FIXME Must decide what to do with this.
  447. mimetype = request.mimetype
  448. if mimetype == '' or mimetype == 'application/x-www-form-urlencoded':
  449. if getattr(stream, 'limit', 0) == 0:
  450. stream = mimetype = None
  451. else:
  452. mimetype = 'application/octet-stream'
  453. return stream, mimetype
  454. def _tombstone_response(e, uid):
  455. headers = {
  456. 'Link': '<{}/fcr:tombstone>; rel="hasTombstone"'.format(request.url),
  457. } if e.uid == uid else {}
  458. return str(e), 410, headers
  459. def set_post_put_params():
  460. """
  461. Sets handling and content disposition for POST and PUT by parsing headers.
  462. """
  463. handling = 'strict'
  464. if 'prefer' in request.headers:
  465. prefer = g.tbox.parse_rfc7240(request.headers['prefer'])
  466. logger.debug('Parsed Prefer header: {}'.format(prefer))
  467. if 'handling' in prefer:
  468. handling = prefer['handling']['value']
  469. try:
  470. disposition = g.tbox.parse_rfc7240(
  471. request.headers['content-disposition'])
  472. except KeyError:
  473. disposition = None
  474. return handling, disposition
  475. def parse_repr_options(retr_opts):
  476. """
  477. Set options to retrieve IMR.
  478. Ideally, IMR retrieval is done once per request, so all the options
  479. are set once in the `imr()` property.
  480. :param dict retr_opts:: Options parsed from `Prefer` header.
  481. """
  482. logger.debug('Parsing retrieval options: {}'.format(retr_opts))
  483. imr_options = {}
  484. if retr_opts.get('value') == 'minimal':
  485. imr_options = {
  486. 'embed_children' : False,
  487. 'incl_children' : False,
  488. 'incl_inbound' : False,
  489. 'incl_srv_mgd' : False,
  490. }
  491. else:
  492. # Default.
  493. imr_options = {
  494. 'embed_children' : False,
  495. 'incl_children' : True,
  496. 'incl_inbound' : False,
  497. 'incl_srv_mgd' : True,
  498. }
  499. # Override defaults.
  500. if 'parameters' in retr_opts:
  501. include = retr_opts['parameters']['include'].split(' ') \
  502. if 'include' in retr_opts['parameters'] else []
  503. omit = retr_opts['parameters']['omit'].split(' ') \
  504. if 'omit' in retr_opts['parameters'] else []
  505. logger.debug('Include: {}'.format(include))
  506. logger.debug('Omit: {}'.format(omit))
  507. if str(Ldpr.EMBED_CHILD_RES_URI) in include:
  508. imr_options['embed_children'] = True
  509. if str(Ldpr.RETURN_CHILD_RES_URI) in omit:
  510. imr_options['incl_children'] = False
  511. if str(Ldpr.RETURN_INBOUND_REF_URI) in include:
  512. imr_options['incl_inbound'] = True
  513. if str(Ldpr.RETURN_SRV_MGD_RES_URI) in omit:
  514. imr_options['incl_srv_mgd'] = False
  515. logger.debug('Retrieval options: {}'.format(pformat(imr_options)))
  516. return imr_options
  517. def _headers_from_metadata(rsrc, out_fmt='text/turtle'):
  518. """
  519. Create a dict of headers from a metadata graph.
  520. :param lakesuperior.model.ldpr.Ldpr rsrc: Resource to extract metadata
  521. from.
  522. """
  523. rsp_headers = defaultdict(list)
  524. digest = rsrc.metadata.value(rsrc.uri, nsc['premis'].hasMessageDigest)
  525. # Only add ETag and digest if output is not RDF.
  526. if digest and out_fmt == 'non_rdf':
  527. digest_components = digest.split(':')
  528. cksum_hex = digest_components[-1]
  529. cksum = bytearray.fromhex(cksum_hex)
  530. digest_algo = digest_components[-2]
  531. etag_str = (
  532. 'W/"{}"'.format(cksum_hex)
  533. if nsc['ldp'].RDFSource in rsrc.ldp_types
  534. else cksum_hex)
  535. rsp_headers['ETag'] = etag_str,
  536. rsp_headers['Digest'] = '{}={}'.format(
  537. digest_algo.upper(), b64encode(cksum).decode('ascii'))
  538. else:
  539. rsp_headers.update(_digest_headers(rsrc.uri))
  540. last_updated_term = rsrc.metadata.value(nsc['fcrepo'].lastModified)
  541. if last_updated_term:
  542. rsp_headers['Last-Modified'] = arrow.get(last_updated_term)\
  543. .format('ddd, D MMM YYYY HH:mm:ss Z')
  544. for t in rsrc.ldp_types:
  545. rsp_headers['Link'].append('{};rel="type"'.format(t.n3()))
  546. mimetype = rsrc.metadata.value(nsc['ebucore'].hasMimeType)
  547. if mimetype:
  548. rsp_headers['Content-Type'] = mimetype
  549. return rsp_headers
  550. def _digest_headers(uri):
  551. """
  552. Get an LDP-RS resource digest and create header tags.
  553. The ``Digest`` and ``ETag`` headers are created.
  554. """
  555. headers = {}
  556. digest = MetadataStore().get_checksum(uri)
  557. headers['Digest'] = 'SHA256={}'.format(b64encode(digest).decode('ascii'))
  558. headers['ETag'] = 'W/{}'.format(digest.hex())
  559. return headers