import logging from collections import defaultdict from pprint import pformat from uuid import uuid4 import arrow from flask import ( Blueprint, current_app, g, make_response, render_template, request, send_file) from rdflib.namespace import RDF, XSD from rdflib.term import Literal from lakesuperior.dictionaries.namespaces import ns_collection as nsc from lakesuperior.dictionaries.namespaces import ns_mgr as nsm from lakesuperior.exceptions import * from lakesuperior.model.ldpr import Ldpr from lakesuperior.model.ldp_nr import LdpNr from lakesuperior.model.ldp_rs import LdpRs from lakesuperior.toolbox import Toolbox logger = logging.getLogger(__name__) # Blueprint for LDP REST API. This is what is usually found under `/rest/` in # standard fcrepo4. Here, it is under `/ldp` but initially `/rest` can be kept # for backward compatibility. ldp = Blueprint( 'ldp', __name__, template_folder='templates', static_url_path='/static', static_folder='../../static') accept_patch = ( 'application/sparql-update', ) accept_rdf = ( 'application/ld+json', 'application/n-triples', 'application/rdf+xml', #'application/x-turtle', #'application/xhtml+xml', #'application/xml', #'text/html', 'text/n3', #'text/plain', 'text/rdf+n3', 'text/turtle', ) std_headers = { 'Accept-Patch' : ','.join(accept_patch), 'Accept-Post' : ','.join(accept_rdf), #'Allow' : ','.join(allow), } @ldp.url_defaults def bp_url_defaults(endpoint, values): url_prefix = getattr(g, 'url_prefix', None) if url_prefix is not None: values.setdefault('url_prefix', url_prefix) @ldp.url_value_preprocessor def bp_url_value_preprocessor(endpoint, values): g.url_prefix = values.pop('url_prefix') g.webroot = request.host_url + g.url_prefix @ldp.before_request def log_request_start(): logger.info('\n\n** Start {} {} **'.format(request.method, request.url)) @ldp.before_request def instantiate_toolbox(): g.tbox = Toolbox() @ldp.before_request def request_timestamp(): g.timestamp = arrow.utcnow() g.timestamp_term = Literal(g.timestamp, datatype=XSD.dateTime) @ldp.after_request def log_request_end(rsp): logger.info('** End {} {} **\n\n'.format(request.method, request.url)) return rsp ## REST SERVICES ## @ldp.route('/', methods=['GET'], strict_slashes=False) @ldp.route('/', defaults={'uuid': None}, methods=['GET'], strict_slashes=False) @ldp.route('//fcr:metadata', defaults={'force_rdf' : True}, methods=['GET']) def get_resource(uuid, force_rdf=False): ''' Retrieve RDF or binary content. @param uuid (string) UUID of resource to retrieve. @param force_rdf (boolean) Whether to retrieve RDF even if the resource is a LDP-NR. This is not available in the API but is used e.g. by the `*/fcr:metadata` endpoint. The default is False. ''' out_headers = std_headers repr_options = defaultdict(dict) if 'prefer' in request.headers: prefer = g.tbox.parse_rfc7240(request.headers['prefer']) logger.debug('Parsed Prefer header: {}'.format(pformat(prefer))) if 'return' in prefer: repr_options = parse_repr_options(prefer['return']) try: rsrc = Ldpr.outbound_inst(uuid, repr_options) except ResourceNotExistsError as e: return str(e), 404 except TombstoneError as e: return _tombstone_response(e, uuid) else: out_headers.update(rsrc.head()) if isinstance(rsrc, LdpRs) \ or is_accept_hdr_rdf_parsable() \ or force_rdf: resp = rsrc.get() if request.accept_mimetypes.best == 'text/html': rsrc = resp.resource(request.path) return render_template('resource.html', rsrc=rsrc, nsm=nsm) else: return (resp.serialize(format='turtle'), out_headers) else: logger.info('Streaming out binary content.') rsp = make_response(send_file(rsrc.local_path, as_attachment=True, attachment_filename=rsrc.filename)) rsp.headers['Link'] = '<{}/fcr:metadata>; rel="describedby"'\ .format(rsrc.uri) return rsp @ldp.route('/', methods=['POST'], strict_slashes=False) @ldp.route('/', defaults={'parent': None}, methods=['POST'], strict_slashes=False) def post_resource(parent): ''' Add a new resource in a new URI. ''' out_headers = std_headers try: slug = request.headers['Slug'] logger.info('Slug: {}'.format(slug)) except KeyError: slug = None handling, disposition = set_post_put_params() stream, mimetype = bitstream_from_req() try: uuid = uuid_for_post(parent, slug) logger.debug('Generated UUID for POST: {}'.format(uuid)) rsrc = Ldpr.inbound_inst(uuid, content_length=request.content_length, stream=stream, mimetype=mimetype, handling=handling, disposition=disposition) except ResourceNotExistsError as e: return str(e), 404 except InvalidResourceError as e: return str(e), 409 except TombstoneError as e: return _tombstone_response(e, uuid) try: rsrc.post() except ServerManagedTermError as e: return str(e), 412 hdr = { 'Location' : rsrc.uri, } if isinstance(rsrc, LdpNr): hdr['Link'] = '<{0}/fcr:metadata>; rel="describedby"; anchor="<{0}>"'\ .format(rsrc.uri) out_headers.update(hdr) return rsrc.uri, 201, out_headers @ldp.route('//fcr:versions', methods=['GET']) def get_version_info(uuid): try: rsp = Ldpr(uuid).version_info except ResourceNotExistsError as e: return str(e), 404 except InvalidResourceError as e: return str(e), 409 except TombstoneError as e: return _tombstone_response(e, uuid) else: return rsp.serialize(format='turtle'), 200 @ldp.route('//fcr:versions/