Browse Source

Major module overhaul to support Python API (tests not passing yet).

Stefano Cossu 6 years ago
parent
commit
6196f24b9f

+ 13 - 16
conftest.py

@@ -1,23 +1,21 @@
 import sys
-sys.path.append('.')
-import numpy
-import random
-import uuid
 
 import pytest
 
-from PIL import Image
-
+sys.path.append('.')
+from lakesuperior.config_parser import test_config
+from lakesuperior.globals import AppGlobals
+from lakesuperior.env import env
+env.app_globals = AppGlobals(test_config)
 from lakesuperior.app import create_app
-from lakesuperior.config_parser import config
-from lakesuperior.store.ldp_rs.lmdb_store import TxnManager
 from util.generators import random_image
-from util.bootstrap import bootstrap_binary_store
 
+env.config = test_config
 
 @pytest.fixture(scope='module')
 def app():
-    app = create_app(config['test'], config['logging'])
+    import pdb; pdb.set_trace()
+    app = create_app(env.config['application'])
 
     yield app
 
@@ -27,15 +25,14 @@ def db(app):
     '''
     Set up and tear down test triplestore.
     '''
-    db = app.rdfly
-    db.bootstrap()
-    bootstrap_binary_store(app)
+    rdfly = env.app_globals.rdfly
+    rdfly.bootstrap()
+    env.app_globals.nonrdfly.bootstrap()
 
-    yield db
+    yield rdfly
 
     print('Tearing down fixture graph store.')
-    if hasattr(db.store, 'destroy'):
-        db.store.destroy(db.store.path)
+    rdfly.store.destroy(rdfly.store.path)
 
 
 @pytest.fixture

+ 203 - 13
lakesuperior/api/resource.py

@@ -1,14 +1,19 @@
+import logging
+
 from functools import wraps
 from multiprocessing import Process
 from threading import Lock, Thread
 
-from flask import (
-        Blueprint, current_app, g, make_response, render_template,
-        request, send_file)
-
+from lakesuperior.config_parser import config
+from lakesuperior.exceptions import InvalidResourceError
+from lakesuperior.env import env
+from lakesuperior.model.ldp_factory import LdpFactory
 from lakesuperior.store.ldp_rs.lmdb_store import TxnManager
 
 
+logger = logging.getLogger(__name__)
+app_globals = env.app_globals
+
 def transaction(write=False):
     '''
     Handle atomic operations in a store.
@@ -20,16 +25,12 @@ def transaction(write=False):
     def _transaction_deco(fn):
         @wraps(fn)
         def _wrapper(*args, **kwargs):
-            if not hasattr(g, 'changelog'):
-                g.changelog = []
-            store = current_app.rdfly.store
-            with TxnManager(store, write=write) as txn:
+            with TxnManager(app_globals.rdf_store, write=write) as txn:
                 ret = fn(*args, **kwargs)
-            if len(g.changelog):
+            if len(app_globals.changelog):
                 job = Thread(target=process_queue)
                 job.start()
             return ret
-
         return _wrapper
     return _transaction_deco
 
@@ -40,8 +41,8 @@ def process_queue():
     '''
     lock = Lock()
     lock.acquire()
-    while len(g.changelog):
-        send_event_msg(g.changelog.pop())
+    while len(app_globals.changelog):
+        send_event_msg(app_globals.changelog.popleft())
     lock.release()
 
 
@@ -58,4 +59,193 @@ def send_event_msg(remove_trp, add_trp, metadata):
     subjects = set(remove_dict.keys()) | set(add_dict.keys())
     for rsrc_uri in subjects:
         self._logger.info('subject: {}'.format(rsrc_uri))
-        #current_app.messenger.send
+        app_globals.messenger.send
+
+
+### API METHODS ###
+
+@transaction()
+def get(uid, repr_options={}):
+    '''
+    Get an LDPR resource.
+
+    The resource comes preloaded with user data and metadata as indicated by
+    the `repr_options` argument. Any further handling of this resource is done
+    outside of a transaction.
+
+    @param uid (string) Resource UID.
+    @param repr_options (dict(bool)) Representation options. This is a dict
+    that is unpacked downstream in the process. The default empty dict results
+    in default values. The accepted dict keys are:
+    - incl_inbound: include inbound references. Default: False.
+    - incl_children: include children URIs. Default: True.
+    - embed_children: Embed full graph of all child resources. Default: False
+    '''
+    rsrc = LdpFactory.from_stored(uid, repr_options)
+    # Load graph before leaving the transaction.
+    rsrc.imr
+
+    return rsrc
+
+
+@transaction()
+def get_version_info(uid):
+    '''
+    Get version metadata (fcr:versions).
+    '''
+    return LdpFactory.from_stored(uid).version_info
+
+
+@transaction()
+def get_version(uid, ver_uid):
+    '''
+    Get version metadata (fcr:versions).
+    '''
+    return LdpFactory.from_stored(uid).get_version(ver_uid)
+
+
+@transaction(True)
+def create(parent, slug, **kwargs):
+    '''
+    Mint a new UID and create a resource.
+
+    The UID is computed from a given parent UID and a "slug", a proposed path
+    relative to the parent. The application will attempt to use the suggested
+    path but it may use a different one if a conflict with an existing resource
+    arises.
+
+    @param parent (string) UID of the parent resource.
+    @param slug (string) Tentative path relative to the parent UID.
+    @param **kwargs Other parameters are passed to the
+    LdpFactory.from_provided method. Please see the documentation for that
+    method for explanation of individual parameters.
+
+    @return string UID of the new resource.
+    '''
+    uid = LdpFactory.mint_uid(parent, slug)
+    logger.debug('Minted UID for new resource: {}'.format(uid))
+    rsrc = LdpFactory.from_provided(uid, **kwargs)
+
+    rsrc.create_or_replace_rsrc(create_only=True)
+
+    return uid
+
+
+@transaction(True)
+def create_or_replace(uid, stream=None, **kwargs):
+    '''
+    Create or replace a resource with a specified UID.
+
+    If the resource already exists, all user-provided properties of the
+    existing resource are deleted. If the resource exists and the provided
+    content is empty, an exception is raised (not sure why, but that's how
+    FCREPO4 handles it).
+
+    @param uid (string) UID of the resource to be created or updated.
+    @param stream (BytesIO) Content stream. If empty, an empty container is
+    created.
+    @param **kwargs Other parameters are passed to the
+    LdpFactory.from_provided method. Please see the documentation for that
+    method for explanation of individual parameters.
+
+    @return string Event type: whether the resource was created or updated.
+    '''
+    rsrc = LdpFactory.from_provided(uid, stream=stream, **kwargs)
+
+    if not stream and rsrc.is_stored:
+        raise InvalidResourceError(rsrc.uid,
+                'Resource {} already exists and no data set was provided.')
+
+    return rsrc.create_or_replace_rsrc()
+
+
+@transaction(True)
+def update(uid, update_str):
+    '''
+    Update a resource with a SPARQL-Update string.
+
+    @param uid (string) Resource UID.
+    @param update_str (string) SPARQL-Update statements.
+    '''
+    rsrc = LdpFactory.from_stored(uid)
+    rsrc.patch(update_str)
+
+    return rsrc
+
+
+@transaction(True)
+def create_version(uid, ver_uid):
+    '''
+    Create a resource version.
+
+    @param uid (string) Resource UID.
+    @param ver_uid (string) Version UID to be appended to the resource URI.
+    NOTE: this is a "slug", i.e. the version URI is not guaranteed to be the
+    one indicated.
+
+    @return string Version UID.
+    '''
+    return LdpFactory.from_stored(uid).create_version(ver_uid)
+
+
+@transaction(True)
+def delete(uid, leave_tstone=True):
+    '''
+    Delete a resource.
+
+    @param uid (string) Resource UID.
+    @param leave_tstone (bool) Whether to perform a soft-delete and leave a
+    tombstone resource, or wipe any memory of the resource.
+    '''
+    # If referential integrity is enforced, grab all inbound relationships
+    # to break them.
+    refint = rdfly.config['referential_integrity']
+    inbound = True if refint else inbound
+    repr_opts = {'incl_inbound' : True} if refint else {}
+
+    rsrc = LdpFactory.from_stored(uid, repr_opts)
+
+    children = rdfly.get_descendants(uid)
+
+    ret = (
+            rsrc.bury_rsrc(inbound)
+            if leave_tstone
+            else rsrc.forget_rsrc(inbound))
+
+    for child_uri in children:
+        try:
+            child_rsrc = LdpFactory.from_stored(
+                rdfly.uri_to_uid(child_uri),
+                repr_opts={'incl_children' : False})
+        except (TombstoneError, ResourceNotExistsError):
+            continue
+        if leave_tstone:
+            child_rsrc.bury_rsrc(inbound, tstone_pointer=rsrc.uri)
+        else:
+            child_rsrc.forget_rsrc(inbound)
+
+    return ret
+
+
+@transaction(True)
+def resurrect(uid):
+    '''
+    Reinstate a buried (soft-deleted) resource.
+
+    @param uid (string) Resource UID.
+    '''
+    return LdpFactory.from_stored(uid).resurrect_rsrc()
+
+
+@transaction(True)
+def forget(uid, inbound=True):
+    '''
+    Delete a resource completely, removing all its traces.
+
+    @param uid (string) Resource UID.
+    @param inbound (bool) Whether the inbound relationships should be deleted
+    as well. If referential integrity is checked system-wide inbound references
+    are always deleted and this option has no effect.
+    '''
+    return LdpFactory.from_stored(uid).forget_rsrc(inbound)
+

+ 3 - 43
lakesuperior/app.py

@@ -1,8 +1,4 @@
 import logging
-import os
-
-from importlib import import_module
-from logging.config import dictConfig
 
 from flask import Flask
 
@@ -10,31 +6,23 @@ from lakesuperior.endpoints.admin import admin
 from lakesuperior.endpoints.ldp import ldp
 from lakesuperior.endpoints.main import main
 from lakesuperior.endpoints.query import query
-from lakesuperior.messaging.messenger import Messenger
-from lakesuperior.toolbox import Toolbox
 
+logger = logging.getLogger(__name__)
 
-# App factory.
 
-def create_app(app_conf, logging_conf):
+def create_app(app_conf):
     '''
     App factory.
 
-    Create a Flask app with a given configuration and initialize persistent
-    connections.
+    Create a Flask app.
 
     @param app_conf (dict) Configuration parsed from `application.yml` file.
-    @param logging_conf (dict) Logging configuration from `logging.yml` file.
     '''
     app = Flask(__name__)
     app.config.update(app_conf)
 
-    dictConfig(logging_conf)
-    logger = logging.getLogger(__name__)
     logger.info('Starting LAKEsuperior HTTP server.')
 
-    ## Configure endpoint blueprints here. ##
-
     app.register_blueprint(main)
     app.register_blueprint(ldp, url_prefix='/ldp', url_defaults={
         'url_prefix': 'ldp'
@@ -46,34 +34,6 @@ def create_app(app_conf, logging_conf):
     app.register_blueprint(query, url_prefix='/query')
     app.register_blueprint(admin, url_prefix='/admin')
 
-    # Initialize RDF layout.
-    rdfly_mod_name = app_conf['store']['ldp_rs']['layout']
-    rdfly_mod = import_module('lakesuperior.store.ldp_rs.{}'.format(
-            rdfly_mod_name))
-    rdfly_cls = getattr(rdfly_mod, camelcase(rdfly_mod_name))
-    app.rdfly = rdfly_cls(app_conf['store']['ldp_rs'])
-    logger.info('RDF layout: {}'.format(rdfly_mod_name))
-
-    # Initialize file layout.
-    nonrdfly_mod_name = app_conf['store']['ldp_nr']['layout']
-    nonrdfly_mod = import_module('lakesuperior.store.ldp_nr.{}'.format(
-            nonrdfly_mod_name))
-    nonrdfly_cls = getattr(nonrdfly_mod, camelcase(nonrdfly_mod_name))
-    app.nonrdfly = nonrdfly_cls(app_conf['store']['ldp_nr'])
-    logger.info('Non-RDF layout: {}'.format(nonrdfly_mod_name))
-
-    # Set up messaging.
-    app.messenger = Messenger(app_conf['messaging'])
-
     return app
 
 
-def camelcase(word):
-    '''
-    Convert a string with underscores with a camel-cased one.
-
-    Ripped from https://stackoverflow.com/a/6425628
-    '''
-    return ''.join(x.capitalize() or '_' for x in word.split('_'))
-
-

+ 5 - 5
lakesuperior/config_parser.py

@@ -37,15 +37,15 @@ This means that if you run a test suite, your live data may be wiped clean!
 Please review your configuration before starting.
 '''
 
-config['test'] = hiyapyco.load(CONFIG_DIR + '/application.yml',
-        CONFIG_DIR + '/test.yml', method=hiyapyco.METHOD_MERGE)
+test_config = {'application': hiyapyco.load(CONFIG_DIR + '/application.yml',
+        CONFIG_DIR + '/test.yml', method=hiyapyco.METHOD_MERGE)}
 
 if config['application']['store']['ldp_rs']['location'] \
-        == config['test']['store']['ldp_rs']['location']:
+        == test_config['application']['store']['ldp_rs']['location']:
             raise RuntimeError(error_msg.format('RDF'))
             sys.exit()
 
-if config['application']['store']['ldp_nr']['path'] == \
-        config['test']['store']['ldp_nr']['path']:
+if config['application']['store']['ldp_nr']['path'] \
+        == test_config['application']['store']['ldp_nr']['path']:
             raise RuntimeError(error_msg.format('binary'))
             sys.exit()

+ 6 - 5
lakesuperior/endpoints/admin.py

@@ -1,11 +1,13 @@
 import logging
 
-from flask import Blueprint, current_app, g, request, render_template
+from flask import Blueprint, render_template
 
+from lakesuperior.env import env
 from lakesuperior.store.ldp_rs.lmdb_store import TxnManager
 
 # Admin interface and API.
 
+app_globals = env.app_globals
 logger = logging.getLogger(__name__)
 
 admin = Blueprint('admin', __name__)
@@ -34,10 +36,9 @@ def stats():
             num /= 1024.0
         return "{:.1f} {}{}".format(num, 'Y', suffix)
 
-    store = current_app.rdfly.store
-    with TxnManager(store) as txn:
-        store_stats = store.stats()
-    rsrc_stats = current_app.rdfly.count_rsrc()
+    with TxnManager(app_globals.rdf_store) as txn:
+        store_stats = app_globals.rdf_store.stats()
+    rsrc_stats = app_globals.rdfly.count_rsrc()
     return render_template(
             'stats.html', rsrc_stats=rsrc_stats, store_stats=store_stats,
             fsize_fmt=fsize_fmt)

+ 179 - 143
lakesuperior/endpoints/ldp.py

@@ -1,6 +1,8 @@
 import logging
+import pdb
 
 from collections import defaultdict
+from io import BytesIO
 from pprint import pformat
 from uuid import uuid4
 
@@ -12,12 +14,13 @@ from flask import (
 from rdflib.namespace import XSD
 from rdflib.term import Literal
 
-from lakesuperior.api.resource import transaction
+from lakesuperior.api import resource as rsrc_api
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
 from lakesuperior.dictionaries.namespaces import ns_mgr as nsm
 from lakesuperior.exceptions import (ResourceNotExistsError, TombstoneError,
         ServerManagedTermError, InvalidResourceError, SingleSubjectError,
         ResourceExistsError, IncompatibleLdpTypeError)
+from lakesuperior.globals import RES_CREATED
 from lakesuperior.model.ldp_factory import LdpFactory
 from lakesuperior.model.ldp_nr import LdpNr
 from lakesuperior.model.ldp_rs import LdpRs
@@ -28,7 +31,6 @@ 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.
@@ -85,7 +87,6 @@ def log_request_start():
 
 @ldp.before_request
 def instantiate_req_vars():
-    g.store = current_app.rdfly.store
     g.tbox = Toolbox()
 
 
@@ -108,9 +109,10 @@ def log_request_end(rsp):
 @ldp.route('/', defaults={'uid': ''}, methods=['GET'], strict_slashes=False)
 @ldp.route('/<path:uid>/fcr:metadata', defaults={'force_rdf' : True},
         methods=['GET'])
-@transaction()
 def get_resource(uid, force_rdf=False):
     '''
+    https://www.w3.org/TR/ldp/#ldpr-HTTP_GET
+
     Retrieve RDF or binary content.
 
     @param uid (string) UID of resource to retrieve. The repository root has
@@ -128,85 +130,39 @@ def get_resource(uid, force_rdf=False):
             repr_options = parse_repr_options(prefer['return'])
 
     try:
-        rsrc = LdpFactory.from_stored(uid, repr_options)
+        rsrc = rsrc_api.get(uid, repr_options)
     except ResourceNotExistsError as e:
         return str(e), 404
     except TombstoneError as e:
         return _tombstone_response(e, uid)
     else:
-        out_headers.update(rsrc.head())
+        out_headers.update(_headers_from_metadata(rsrc))
         if (
                 isinstance(rsrc, LdpRs)
                 or is_accept_hdr_rdf_parsable()
                 or force_rdf):
-            rsp = rsrc.get()
-            return negotiate_content(rsp, out_headers)
+            gr = g.tbox.globalize_graph(rsrc.out_graph)
+            gr.namespace_manager = nsm
+            return _negotiate_content(gr, out_headers)
         else:
             logger.info('Streaming out binary content.')
-            rsp = make_response(send_file(rsrc.local_path, as_attachment=True,
-                    attachment_filename=rsrc.filename, mimetype=rsrc.mimetype))
-            rsp.headers['Link'] = '<{}/fcr:metadata>; rel="describedby"'\
-                    .format(rsrc.uri)
-
+            rsp = make_response(send_file(
+                    rsrc.local_path, as_attachment=True,
+                    attachment_filename=rsrc.filename,
+                    mimetype=rsrc.mimetype))
+            rsp.headers = out_headers
+            rsp.headers['Link'] = (
+                    '<{}/fcr:metadata>; rel="describedby"'.format(rsrc.uri))
             return rsp
 
 
-@ldp.route('/<path:parent>', methods=['POST'], strict_slashes=False)
-@ldp.route('/', defaults={'parent': ''}, 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:
-        with TxnManager(g.store, True):
-            uid = LdpFactory.mint_uid(parent, slug)
-            logger.debug('Generated UID for POST: {}'.format(uid))
-            rsrc = LdpFactory.from_provided(
-                    uid, content_length=request.content_length,
-                    stream=stream, mimetype=mimetype, handling=handling,
-                    disposition=disposition)
-            rsrc.post()
-    except ResourceNotExistsError as e:
-        return str(e), 404
-    except InvalidResourceError as e:
-        return str(e), 409
-    except TombstoneError as e:
-        return _tombstone_response(e, uid)
-    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('/<path:uid>/fcr:versions', methods=['GET'])
-@transaction()
 def get_version_info(uid):
     '''
     Get version info (`fcr:versions`).
     '''
     try:
-        rsp = Ldpr(uid).get_version_info()
+        gr = rsrc_api.get_version_info(uid)
     except ResourceNotExistsError as e:
         return str(e), 404
     except InvalidResourceError as e:
@@ -214,11 +170,10 @@ def get_version_info(uid):
     except TombstoneError as e:
         return _tombstone_response(e, uid)
     else:
-        return negotiate_content(rsp)
+        return _negotiate_content(g.tbox.globalize_graph(gr))
 
 
 @ldp.route('/<path:uid>/fcr:versions/<ver_uid>', methods=['GET'])
-@transaction()
 def get_version(uid, ver_uid):
     '''
     Get an individual resource version.
@@ -227,7 +182,7 @@ def get_version(uid, ver_uid):
     @param ver_uid (string) Version UID.
     '''
     try:
-        rsp = Ldpr(uid).get_version(ver_uid)
+        gr = rsrc_api.get_version(uid, ver_uid)
     except ResourceNotExistsError as e:
         return str(e), 404
     except InvalidResourceError as e:
@@ -235,98 +190,109 @@ def get_version(uid, ver_uid):
     except TombstoneError as e:
         return _tombstone_response(e, uid)
     else:
-        return negotiate_content(rsp)
+        return _negotiate_content(g.tbox.globalize_graph(gr))
 
 
-@ldp.route('/<path:uid>/fcr:versions', methods=['POST', 'PUT'])
-@transaction(True)
-def post_version(uid):
+@ldp.route('/<path:parent>', methods=['POST'], strict_slashes=False)
+@ldp.route('/', defaults={'parent': ''}, methods=['POST'],
+        strict_slashes=False)
+def post_resource(parent):
     '''
-    Create a new resource version.
+    https://www.w3.org/TR/ldp/#ldpr-HTTP_POST
+
+    Add a new resource in a new URI.
     '''
-    if request.method == 'PUT':
-        return 'Method not allowed.', 405
-    ver_uid = request.headers.get('slug', None)
+    out_headers = std_headers
     try:
-        ver_uri = LdpFactory.from_stored(uid).create_version(ver_uid)
+        slug = request.headers['Slug']
+        logger.debug('Slug: {}'.format(slug))
+    except KeyError:
+        slug = None
+
+    handling, disposition = set_post_put_params()
+    stream, mimetype = _bistream_from_req()
+
+    if LdpFactory.is_rdf_parsable(mimetype):
+        # If the content is RDF, localize in-repo URIs.
+        global_rdf = stream.read()
+        local_rdf = global_rdf.replace(
+                g.webroot.encode('utf-8'), nsc['fcres'].encode('utf-8'))
+        stream = BytesIO(local_rdf)
+        is_rdf = True
+    else:
+        is_rdf = False
+
+    try:
+        uid = rsrc_api.create(
+                parent, slug, 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, uid)
-    else:
-        return '', 201, {'Location': ver_uri}
+    except ServerManagedTermError as e:
+        return str(e), 412
 
+    uri = g.tbox.uid_to_uri(uid)
+    hdr = {'Location' : uri}
 
-@ldp.route('/<path:uid>/fcr:versions/<ver_uid>', methods=['PATCH'])
-@transaction(True)
-def patch_version(uid, ver_uid):
-    '''
-    Revert to a previous version.
+    if mimetype and not is_rdf:
+        hdr['Link'] = '<{0}/fcr:metadata>; rel="describedby"; anchor="<{0}>"'\
+                .format(uri)
 
-    NOTE: This creates a new version snapshot.
+    out_headers.update(hdr)
 
-    @param uid (string) Resource UID.
-    @param ver_uid (string) Version UID.
-    '''
-    try:
-        LdpFactory.from_stored(uid).revert_to_version(ver_uid)
-    except ResourceNotExistsError as e:
-        return str(e), 404
-    except InvalidResourceError as e:
-        return str(e), 409
-    except TombstoneError as e:
-        return _tombstone_response(e, uid)
-    else:
-        return '', 204
+    return uri, 201, out_headers
 
 
 @ldp.route('/<path:uid>', methods=['PUT'], strict_slashes=False)
 @ldp.route('/<path:uid>/fcr:metadata', defaults={'force_rdf' : True},
         methods=['PUT'])
-@transaction(True)
 def put_resource(uid):
     '''
-    Add a new resource at a specified URI.
+    https://www.w3.org/TR/ldp/#ldpr-HTTP_PUT
+
+    Add or replace a new resource at a specified URI.
     '''
     # Parse headers.
-    logger.info('Request headers: {}'.format(request.headers))
+    logger.debug('Request headers: {}'.format(request.headers))
 
     rsp_headers = {'Content-Type' : 'text/plain; charset=utf-8'}
 
     handling, disposition = set_post_put_params()
-    stream, mimetype = bitstream_from_req()
+    stream, mimetype = _bistream_from_req()
+
+    if LdpFactory.is_rdf_parsable(mimetype):
+        # If the content is RDF, localize in-repo URIs.
+        global_rdf = stream.read()
+        local_rdf = global_rdf.replace(
+                g.webroot.encode('utf-8'), nsc['fcres'].encode('utf-8'))
+        stream = BytesIO(local_rdf)
+        is_rdf = True
+    else:
+        is_rdf = False
 
     try:
-        rsrc = LdpFactory.from_provided(
-                uid, content_length=request.content_length,
-                stream=stream, mimetype=mimetype, handling=handling,
-                disposition=disposition)
-        if not request.content_length and rsrc.is_stored:
-            raise InvalidResourceError(rsrc.uid,
-                'Resource {} already exists and no data set was provided.')
-    except InvalidResourceError as e:
+        evt = rsrc_api.create_or_replace(uid, stream=stream, mimetype=mimetype,
+                handling=handling, disposition=disposition)
+    except (InvalidResourceError, ResourceExistsError) as e:
         return str(e), 409
     except (ServerManagedTermError, SingleSubjectError) as e:
         return str(e), 412
     except IncompatibleLdpTypeError as e:
         return str(e), 415
-
-    try:
-        ret = rsrc.put()
-        rsp_headers.update(rsrc.head())
-    except (InvalidResourceError, ResourceExistsError) as e:
-        return str(e), 409
     except TombstoneError as e:
         return _tombstone_response(e, uid)
 
-    if ret == Ldpr.RES_CREATED:
+    uri = g.tbox.uid_to_uri(uid)
+    if evt == RES_CREATED:
         rsp_code = 201
-        rsp_headers['Location'] = rsp_body = rsrc.uri
-        if isinstance(rsrc, LdpNr):
-            rsp_headers['Link'] = '<{0}/fcr:metadata>; rel="describedby"'\
-                    .format(rsrc.uri)
+        rsp_headers['Location'] = rsp_body = uri
+        if mimetype and not is_rdf:
+            rsp_headers['Link'] = (
+                    '<{0}/fcr:metadata>; rel="describedby"'.format(uri))
     else:
         rsp_code = 204
         rsp_body = ''
@@ -334,19 +300,21 @@ def put_resource(uid):
 
 
 @ldp.route('/<path:uid>', methods=['PATCH'], strict_slashes=False)
-@transaction(True)
 def patch_resource(uid):
     '''
+    https://www.w3.org/TR/ldp/#ldpr-HTTP_PATCH
+
     Update an existing resource with a SPARQL-UPDATE payload.
     '''
     rsp_headers = {'Content-Type' : 'text/plain; charset=utf-8'}
-    rsrc = LdpRs(uid)
     if request.mimetype != 'application/sparql-update':
         return 'Provided content type is not a valid parsable format: {}'\
                 .format(request.mimetype), 415
 
+    update_str = request.get_data().decode('utf-8')
+    local_update_str = g.tbox.localize_ext_str(update_str, nsc['fcres'][uri])
     try:
-        rsrc.patch(request.get_data().decode('utf-8'))
+        rsrc = rsrc_api.update(uid, local_update_str)
     except ResourceNotExistsError as e:
         return str(e), 404
     except TombstoneError as e:
@@ -354,18 +322,16 @@ def patch_resource(uid):
     except (ServerManagedTermError, SingleSubjectError) as e:
         return str(e), 412
     else:
-        rsp_headers.update(rsrc.head())
+        rsp_headers.update(_headers_from_metadata(rsrc))
         return '', 204, rsp_headers
 
 
 @ldp.route('/<path:uid>/fcr:metadata', methods=['PATCH'])
-@transaction(True)
 def patch_resource_metadata(uid):
     return patch_resource(uid)
 
 
 @ldp.route('/<path:uid>', methods=['DELETE'])
-@transaction(True)
 def delete_resource(uid):
     '''
     Delete a resource and optionally leave a tombstone.
@@ -377,15 +343,10 @@ def delete_resource(uid):
 
     In order to completely wipe out all traces of a resource, the tombstone
     must be deleted as well, or the `Prefer:no-tombstone` header can be used.
-    The latter will purge the resource immediately.
+    The latter will forget (completely delete) the resource immediately.
     '''
     headers = std_headers
 
-    # If referential integrity is enforced, grab all inbound relationships
-    # to break them.
-    repr_opts = {'incl_inbound' : True} \
-            if current_app.config['store']['ldp_rs']['referential_integrity'] \
-            else {}
     if 'prefer' in request.headers:
         prefer = g.tbox.parse_rfc7240(request.headers['prefer'])
         leave_tstone = 'no-tombstone' not in prefer
@@ -393,8 +354,7 @@ def delete_resource(uid):
         leave_tstone = True
 
     try:
-        LdpFactory.from_stored(uid, repr_opts).delete(
-                leave_tstone=leave_tstone)
+        rsrc_api.delete(uid, leave_tstone)
     except ResourceNotExistsError as e:
         return str(e), 404
     except TombstoneError as e:
@@ -405,7 +365,6 @@ def delete_resource(uid):
 
 @ldp.route('/<path:uid>/fcr:tombstone', methods=['GET', 'POST', 'PUT',
         'PATCH', 'DELETE'])
-@transaction(True)
 def tombstone(uid):
     '''
     Handle all tombstone operations.
@@ -413,20 +372,18 @@ def tombstone(uid):
     The only allowed methods are POST and DELETE; any other verb will return a
     405.
     '''
-    logger.debug('Deleting tombstone for {}.'.format(uid))
-    rsrc = Ldpr(uid)
     try:
-        rsrc.metadata
+        rsrc = rsrc_api.get(uid)
     except TombstoneError as e:
         if request.method == 'DELETE':
             if e.uid == uid:
-                rsrc.purge()
+                rsrc_api.forget(uid)
                 return '', 204
             else:
                 return _tombstone_response(e, uid)
         elif request.method == 'POST':
             if e.uid == uid:
-                rsrc_uri = rsrc.resurrect()
+                rsrc_uri = rsrc_api.resurrect(uid)
                 headers = {'Location' : rsrc_uri}
                 return rsrc_uri, 201, headers
             else:
@@ -439,7 +396,52 @@ def tombstone(uid):
         return '', 404
 
 
-def negotiate_content(rsp, headers=None):
+@ldp.route('/<path:uid>/fcr:versions', methods=['POST', 'PUT'])
+def post_version(uid):
+    '''
+    Create a new resource version.
+    '''
+    if request.method == 'PUT':
+        return 'Method not allowed.', 405
+    ver_uid = request.headers.get('slug', None)
+
+    try:
+        ver_uid = rsrc_api.create_version(uid, ver_uid)
+    except ResourceNotExistsError as e:
+        return str(e), 404
+    except InvalidResourceError as e:
+        return str(e), 409
+    except TombstoneError as e:
+        return _tombstone_response(e, uid)
+    else:
+        return '', 201, {'Location': g.tbox.uri_to_uri(ver_uid)}
+
+
+@ldp.route('/<path:uid>/fcr:versions/<ver_uid>', methods=['PATCH'])
+def patch_version(uid, ver_uid):
+    '''
+    Revert to a previous version.
+
+    NOTE: This creates a new version snapshot.
+
+    @param uid (string) Resource UID.
+    @param ver_uid (string) Version UID.
+    '''
+    try:
+        LdpFactory.from_stored(uid).revert_to_version(ver_uid)
+    except ResourceNotExistsError as e:
+        return str(e), 404
+    except InvalidResourceError as e:
+        return str(e), 409
+    except TombstoneError as e:
+        return _tombstone_response(e, uid)
+    else:
+        return '', 204
+
+
+## PRIVATE METHODS ##
+
+def _negotiate_content(rsp, headers=None):
     '''
     Return HTML or serialized RDF depending on accept headers.
     '''
@@ -454,7 +456,7 @@ def negotiate_content(rsp, headers=None):
         return (rsp.serialize(format='turtle'), headers)
 
 
-def bitstream_from_req():
+def _bistream_from_req():
     '''
     Find how a binary file and its MIMEtype were uploaded in the request.
     '''
@@ -462,7 +464,9 @@ def bitstream_from_req():
     logger.debug('files: {}'.format(request.files))
     logger.debug('stream: {}'.format(request.stream))
 
-    if request.mimetype == 'multipart/form-data':
+    if request.mimetype == '':
+        stream = mimetype = None
+    elif request.mimetype == 'multipart/form-data':
         # This seems the "right" way to upload a binary file, with a
         # multipart/form-data MIME type and the file in the `file`
         # field. This however is not supported by FCREPO4.
@@ -471,7 +475,6 @@ def bitstream_from_req():
         # @TODO This will turn out useful to provide metadata
         # with the binary.
         #metadata = request.files.get('metadata').stream
-        #provided_imr = [parse RDF here...]
     else:
         # This is a less clean way, with the file in the form body and
         # the request as application/x-www-form-urlencoded.
@@ -579,3 +582,36 @@ def parse_repr_options(retr_opts):
     return imr_options
 
 
+def _headers_from_metadata(rsrc):
+    '''
+    Create a dict of headers from a metadata graph.
+
+    @param rsrc (lakesuperior.model.ldpr.Ldpr) Resource to extract metadata
+    from.
+    '''
+    out_headers = defaultdict(list)
+
+    digest = rsrc.metadata.value(nsc['premis'].hasMessageDigest)
+    if digest:
+        etag = digest.identifier.split(':')[-1]
+        etag_str = (
+                'W/"{}"'.format(etag)
+                if nsc['ldp'].RDFSource in rsrc.ldp_types
+                else etag)
+        out_headers['ETag'] = etag_str,
+
+    last_updated_term = rsrc.metadata.value(nsc['fcrepo'].lastModified)
+    if last_updated_term:
+        out_headers['Last-Modified'] = arrow.get(last_updated_term)\
+            .format('ddd, D MMM YYYY HH:mm:ss Z')
+
+    for t in rsrc.ldp_types:
+        out_headers['Link'].append(
+                '{};rel="type"'.format(t.n3()))
+
+    mimetype = rsrc.metadata.value(nsc['ebucore'].hasMimeType)
+    if mimetype:
+        out_headers['Content-Type'] = mimetype
+
+    return out_headers
+

+ 4 - 2
lakesuperior/endpoints/query.py

@@ -3,6 +3,7 @@ import logging
 from flask import Blueprint, current_app, request, render_template
 from rdflib.plugin import PluginException
 
+from lakesuperior.env import env
 from lakesuperior.dictionaries.namespaces import ns_mgr as nsm
 from lakesuperior.query import QueryEngine
 from lakesuperior.store.ldp_rs.lmdb_store import LmdbStore, TxnManager
@@ -13,6 +14,8 @@ from lakesuperior.store.ldp_rs.lmdb_store import LmdbStore, TxnManager
 # N.B All data sources are read-only for this endpoint.
 
 logger = logging.getLogger(__name__)
+rdf_store = env.app_globals.rdf_store
+rdfly = env.app_globals.rdfly
 
 query = Blueprint('query', __name__)
 
@@ -52,8 +55,7 @@ def sparql():
         return render_template('sparql_query.html', nsm=nsm)
     else:
         logger.debug('Query: {}'.format(request.form['query']))
-        store = current_app.rdfly.store
-        with TxnManager(store) as txn:
+        with TxnManager(rdf_store) as txn:
             qres = QueryEngine().sparql_query(request.form['query'])
 
             match = request.accept_mimetypes.best_match(accept_mimetypes.keys())

+ 7 - 7
lakesuperior/messaging/formatters.py

@@ -4,7 +4,7 @@ import uuid
 
 from abc import ABCMeta, abstractmethod
 
-from lakesuperior.model.ldpr import Ldpr
+from lakesuperior.globals import RES_CREATED, RES_DELETED, RES_UPDATED
 
 
 class BaseASFormatter(metaclass=ABCMeta):
@@ -15,15 +15,15 @@ class BaseASFormatter(metaclass=ABCMeta):
     builder.
     '''
     ev_types = {
-        Ldpr.RES_CREATED : 'Create',
-        Ldpr.RES_DELETED : 'Delete',
-        Ldpr.RES_UPDATED : 'Update',
+        RES_CREATED : 'Create',
+        RES_DELETED : 'Delete',
+        RES_UPDATED : 'Update',
     }
 
     ev_names = {
-        Ldpr.RES_CREATED : 'Resource Modification',
-        Ldpr.RES_DELETED : 'Resource Creation',
-        Ldpr.RES_UPDATED : 'Resource Deletion',
+        RES_CREATED : 'Resource Modification',
+        RES_DELETED : 'Resource Creation',
+        RES_UPDATED : 'Resource Deletion',
     }
 
     def __init__(self, uri, ev_type, time, type, data=None,

+ 29 - 30
lakesuperior/model/ldp_factory.py

@@ -3,19 +3,22 @@ import logging
 from pprint import pformat
 from uuid import uuid4
 
-import rdflib
-
-from flask import current_app, g
-from rdflib import Graph
+from rdflib import Graph, parser, plugin, serializer
 from rdflib.resource import Resource
 from rdflib.namespace import RDF
 
 from lakesuperior import model
+from lakesuperior.config_parser import config
+from lakesuperior.env import env
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
 from lakesuperior.exceptions import (
         IncompatibleLdpTypeError, InvalidResourceError, ResourceExistsError,
         ResourceNotExistsError)
 
+
+rdfly = env.app_globals.rdfly
+
+
 class LdpFactory:
     '''
     Generate LDP instances.
@@ -31,7 +34,7 @@ class LdpFactory:
     def new_container(uid):
         if not uid:
             raise InvalidResourceError(uid)
-        if current_app.rdfly.ask_rsrc_exists(uid):
+        if rdfly.ask_rsrc_exists(uid):
             raise ResourceExistsError(uid)
         rsrc = model.ldp_rs.Ldpc(
                 uid, provided_imr=Resource(Graph(), nsc['fcres'][uid]))
@@ -55,7 +58,7 @@ class LdpFactory:
         #__class__._logger.info('Retrieving stored resource: {}'.format(uid))
         imr_urn = nsc['fcres'][uid]
 
-        rsrc_meta = current_app.rdfly.get_metadata(uid)
+        rsrc_meta = rdfly.get_metadata(uid)
         #__class__._logger.debug('Extracted metadata: {}'.format(
         #        pformat(set(rsrc_meta.graph))))
         rdf_types = set(rsrc_meta.graph[imr_urn : RDF.type])
@@ -76,44 +79,40 @@ class LdpFactory:
 
 
     @staticmethod
-    def from_provided(uid, content_length, mimetype, stream, **kwargs):
+    def from_provided(uid, mimetype, stream=None, **kwargs):
         '''
         Determine LDP type from request content.
 
         @param uid (string) UID of the resource to be created or updated.
-        @param content_length (int) The provided content length.
         @param mimetype (string) The provided content MIME type.
-        @param stream (IOStream) The provided data stream. This can be RDF or
-        non-RDF content.
+        @param stream (IOStream | None) The provided data stream. This can be
+        RDF or non-RDF content, or None. In the latter case, an empty container
+        is created.
         '''
-        urn = nsc['fcres'][uid]
+        uri = nsc['fcres'][uid]
 
         logger = __class__._logger
 
-        if not content_length:
+        if not stream:
             # Create empty LDPC.
             logger.info('No data received in request. '
                     'Creating empty container.')
             inst = model.ldp_rs.Ldpc(
-                    uid, provided_imr=Resource(Graph(), urn), **kwargs)
+                    uid, provided_imr=Resource(Graph(), uri), **kwargs)
 
         elif __class__.is_rdf_parsable(mimetype):
             # Create container and populate it with provided RDF data.
             input_rdf = stream.read()
-            provided_gr = Graph().parse(data=input_rdf,
-                    format=mimetype, publicID=urn)
+            gr = Graph().parse(data=input_rdf, format=mimetype, publicID=uri)
             #logger.debug('Provided graph: {}'.format(
             #        pformat(set(provided_gr))))
-            local_gr = g.tbox.localize_graph(provided_gr)
-            #logger.debug('Parsed local graph: {}'.format(
-            #        pformat(set(local_gr))))
-            provided_imr = Resource(local_gr, urn)
+            provided_imr = Resource(gr, uri)
 
             # Determine whether it is a basic, direct or indirect container.
             Ldpr = model.ldpr.Ldpr
-            if Ldpr.MBR_RSRC_URI in local_gr.predicates() and \
-                    Ldpr.MBR_REL_URI in local_gr.predicates():
-                if Ldpr.INS_CNT_REL_URI in local_gr.predicates():
+            if Ldpr.MBR_RSRC_URI in gr.predicates() and \
+                    Ldpr.MBR_REL_URI in gr.predicates():
+                if Ldpr.INS_CNT_REL_URI in gr.predicates():
                     cls = model.ldp_rs.LdpIc
                 else:
                     cls = model.ldp_rs.LdpDc
@@ -131,7 +130,7 @@ class LdpFactory:
 
         else:
             # Create a LDP-NR and equip it with the binary file provided.
-            provided_imr = Resource(Graph(), urn)
+            provided_imr = Resource(Graph(), uri)
             inst = model.ldp_nr.LdpNr(uid, stream=stream, mimetype=mimetype,
                     provided_imr=provided_imr, **kwargs)
 
@@ -158,8 +157,8 @@ class LdpFactory:
         @param mimetype (string) MIME type to check.
         '''
         try:
-            rdflib.plugin.get(mimetype, rdflib.parser.Parser)
-        except rdflib.plugin.PluginException:
+            plugin.get(mimetype, parser.Parser)
+        except plugin.PluginException:
             return False
         else:
             return True
@@ -173,8 +172,8 @@ class LdpFactory:
         @param mimetype (string) MIME type to check.
         '''
         try:
-            rdflib.plugin.get(mimetype, rdflib.serializer.Serializer)
-        except rdflib.plugin.PluginException:
+            plugin.get(mimetype, serializer.Serializer)
+        except plugin.PluginException:
             return False
         else:
             return True
@@ -199,8 +198,8 @@ class LdpFactory:
         what has been indicated.
         '''
         def split_if_legacy(uid):
-            if current_app.config['store']['ldp_rs']['legacy_ptree_split']:
-                uid = g.tbox.split_uuid(uid)
+            if config['application']['store']['ldp_rs']['legacy_ptree_split']:
+                uid = tbox.split_uuid(uid)
             return uid
 
         # Shortcut!
@@ -223,7 +222,7 @@ class LdpFactory:
         # Create candidate UID and validate.
         if path:
             cnd_uid = pfx + path
-            if current_app.rdfly.ask_rsrc_exists(cnd_uid):
+            if rdfly.ask_rsrc_exists(cnd_uid):
                 uid = pfx + split_if_legacy(str(uuid4()))
             else:
                 uid = cnd_uid

+ 16 - 11
lakesuperior/model/ldp_nr.py

@@ -1,12 +1,19 @@
+import pdb
+
 from rdflib import Graph
 from rdflib.namespace import RDF, XSD
 from rdflib.resource import Resource
 from rdflib.term import URIRef, Literal, Variable
 
+from lakesuperior.env import env
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
 from lakesuperior.model.ldpr import Ldpr
 from lakesuperior.model.ldp_rs import LdpRs
 
+
+nonrdfly = env.app_globals.nonrdfly
+
+
 class LdpNr(Ldpr):
     '''LDP-NR (Non-RDF Source).
 
@@ -54,26 +61,25 @@ class LdpNr(Ldpr):
     def local_path(self):
         cksum_term = self.imr.value(nsc['premis'].hasMessageDigest)
         cksum = str(cksum_term.identifier.replace('urn:sha1:',''))
-        return self.nonrdfly.local_path(cksum)
-
+        return nonrdfly.local_path(cksum)
 
-    ## LDP METHODS ##
 
-    def _create_or_replace_rsrc(self, create_only=False):
+    def create_or_replace_rsrc(self, create_only=False):
         '''
         Create a new binary resource with a corresponding RDF representation.
 
         @param file (Stream) A Stream resource representing the uploaded file.
         '''
         # Persist the stream.
-        file_uuid = self.digest = self.nonrdfly.persist(self.stream)
+        self.digest, self.size = nonrdfly.persist(self.stream)
 
         # Try to persist metadata. If it fails, delete the file.
-        self._logger.debug('Persisting LDP-NR triples in {}'.format(self.urn))
+        self._logger.debug('Persisting LDP-NR triples in {}'.format(self.uri))
         try:
-            ev_type = super()._create_or_replace_rsrc(create_only)
+            ev_type = super().create_or_replace_rsrc(create_only)
         except:
-            self.nonrdfly.delete(file_uuid)
+            # self.digest is also the file UID.
+            nonrdfly.delete(self.digest)
             raise
         else:
             return ev_type
@@ -93,9 +99,8 @@ class LdpNr(Ldpr):
         super()._add_srv_mgd_triples(create)
 
         # File size.
-        self._logger.debug('Data stream size: {}'.format(self.stream.limit))
-        self.provided_imr.set(nsc['premis'].hasSize,
-                Literal(self.stream.limit))
+        self._logger.debug('Data stream size: {}'.format(self.size))
+        self.provided_imr.set(nsc['premis'].hasSize, Literal(self.size))
 
         # Checksum.
         cksum_term = URIRef('urn:sha1:{}'.format(self.digest))

+ 0 - 3
lakesuperior/model/ldp_rs.py

@@ -40,14 +40,11 @@ class LdpRs(Ldpr):
 
     def patch(self, update_str):
         '''
-        https://www.w3.org/TR/ldp/#ldpr-HTTP_PATCH
-
         Update an existing resource by applying a SPARQL-UPDATE query.
 
         @param update_str (string) SPARQL-Update staements.
         '''
         self.handling = 'lenient' # FCREPO does that and Hyrax requires it.
-        local_update_str = g.tbox.localize_ext_str(update_str, self.urn)
         self._logger.debug('Local update string: {}'.format(local_update_str))
 
         return self._sparql_update(local_update_str)

+ 154 - 243
lakesuperior/model/ldpr.py

@@ -13,6 +13,8 @@ from rdflib.resource import Resource
 from rdflib.namespace import RDF
 from rdflib.term import URIRef, Literal
 
+from lakesuperior.env import env
+from lakesuperior.globals import RES_CREATED, RES_DELETED, RES_UPDATED
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
 from lakesuperior.dictionaries.namespaces import ns_mgr as nsm
 from lakesuperior.dictionaries.srv_mgd_terms import  srv_mgd_subjects, \
@@ -26,6 +28,8 @@ from lakesuperior.store.ldp_rs.rsrc_centric_layout import VERS_CONT_LABEL
 ROOT_UID = ''
 ROOT_RSRC_URI = nsc['fcres'][ROOT_UID]
 
+rdfly = env.app_globals.rdfly
+
 
 class Ldpr(metaclass=ABCMeta):
     '''LDPR (LDP Resource).
@@ -71,10 +75,6 @@ class Ldpr(metaclass=ABCMeta):
     # is not provided.
     DEFAULT_USER = Literal('BypassAdmin')
 
-    RES_CREATED = '_create_'
-    RES_DELETED = '_delete_'
-    RES_UPDATED = '_update_'
-
     # RDF Types that populate a new resource.
     base_types = {
         nsc['fcrepo'].Resource,
@@ -114,13 +114,9 @@ class Ldpr(metaclass=ABCMeta):
         operations such as `PUT` or `POST`, serialized as a string. This sets
         the `provided_imr` property.
         '''
-        self.uid = g.tbox.uri_to_uuid(uid) \
+        self.uid = rdfly.uri_to_uid(uid) \
                 if isinstance(uid, URIRef) else uid
-        self.urn = nsc['fcres'][uid]
-        self.uri = g.tbox.uuid_to_uri(self.uid)
-
-        self.rdfly = current_app.rdfly
-        self.nonrdfly = current_app.nonrdfly
+        self.uri = nsc['fcres'][uid]
 
         self.provided_imr = provided_imr
 
@@ -134,7 +130,7 @@ class Ldpr(metaclass=ABCMeta):
         @return rdflib.resource.Resource
         '''
         if not hasattr(self, '_rsrc'):
-            self._rsrc = self.rdfly.ds.resource(self.urn)
+            self._rsrc = rdfly.ds.resource(self.uri)
 
         return self._rsrc
 
@@ -151,14 +147,15 @@ class Ldpr(metaclass=ABCMeta):
         '''
         if not hasattr(self, '_imr'):
             if hasattr(self, '_imr_options'):
-                self._logger.info('Getting RDF representation for resource /{}'
+                self._logger.debug(
+                        'Getting RDF representation for resource /{}'
                         .format(self.uid))
                 #self._logger.debug('IMR options: {}'.format(self._imr_options))
                 imr_options = self._imr_options
             else:
                 imr_options = {}
             options = dict(imr_options, strict=True)
-            self._imr = self.rdfly.extract_imr(self.uid, **options)
+            self._imr = rdfly.extract_imr(self.uid, **options)
 
         return self._imr
 
@@ -173,7 +170,7 @@ class Ldpr(metaclass=ABCMeta):
         '''
         if isinstance(v, Resource):
             v = v.graph
-        self._imr = Resource(Graph(), self.urn)
+        self._imr = Resource(Graph(), self.uri)
         gr = self._imr.graph
         gr += v
 
@@ -198,7 +195,7 @@ class Ldpr(metaclass=ABCMeta):
             else:
                 self._logger.info('Getting metadata for resource /{}'
                         .format(self.uid))
-                self._metadata = self.rdfly.get_metadata(self.uid)
+                self._metadata = rdfly.get_metadata(self.uid)
 
         return self._metadata
 
@@ -231,9 +228,9 @@ class Ldpr(metaclass=ABCMeta):
                 imr_options = {}
             options = dict(imr_options, strict=True)
             try:
-                self._imr = self.rdfly.extract_imr(self.uid, **options)
+                self._imr = rdfly.extract_imr(self.uid, **options)
             except ResourceNotExistsError:
-                self._imr = Resource(Graph(), self.urn)
+                self._imr = Resource(Graph(), self.uri)
                 for t in self.base_types:
                     self.imr.add(RDF.type, t)
 
@@ -271,9 +268,10 @@ class Ldpr(metaclass=ABCMeta):
         '''
         if not hasattr(self, '_version_info'):
             try:
-                self._version_info = self.rdfly.get_version_info(self.uid)
+                #@ TODO get_version_info should return a graph.
+                self._version_info = rdfly.get_version_info(self.uid).graph
             except ResourceNotExistsError as e:
-                self._version_info = Resource(Graph(), self.urn)
+                self._version_info = Graph(identifer=self.uri)
 
         return self._version_info
 
@@ -295,7 +293,7 @@ class Ldpr(metaclass=ABCMeta):
             if hasattr(self, '_imr'):
                 self._is_stored = len(self.imr.graph) > 0
             else:
-                self._is_stored = self.rdfly.ask_rsrc_exists(self.uid)
+                self._is_stored = rdfly.ask_rsrc_exists(self.uid)
 
         return self._is_stored
 
@@ -315,7 +313,7 @@ class Ldpr(metaclass=ABCMeta):
             else:
                 return set()
 
-            self._types = set(metadata.graph[self.urn : RDF.type])
+            self._types = set(metadata.graph[self.uri : RDF.type])
 
         return self._types
 
@@ -357,108 +355,153 @@ class Ldpr(metaclass=ABCMeta):
         return out_headers
 
 
-
-    def get(self):
+    def get_version(self, ver_uid, **kwargs):
+        '''
+        Get a version by label.
         '''
-        Get an RDF representation of the resource.
+        return rdfly.extract_imr(self.uid, ver_uid, **kwargs).graph
 
-        The binary retrieval is handled directly by the router.
 
-        Internal URNs are replaced by global URIs using the endpoint webroot.
+    def create_or_replace_rsrc(self, create_only=False):
         '''
-        gr = g.tbox.globalize_graph(self.out_graph)
-        gr.namespace_manager = nsm
+        Create or update a resource. PUT and POST methods, which are almost
+        identical, are wrappers for this method.
 
-        return gr
+        @param create_only (boolean) Whether this is a create-only operation.
+        '''
+        create = create_only or not self.is_stored
 
+        self._add_srv_mgd_triples(create)
+        #self._ensure_single_subject_rdf(self.provided_imr.graph)
+        ref_int = rdfly.config['referential_integrity']
+        if ref_int:
+            self._check_ref_int(ref_int)
 
-    def get_version_info(self):
-        '''
-        Get the `fcr:versions` graph.
-        '''
-        gr = g.tbox.globalize_graph(self.version_info.graph)
-        gr.namespace_manager = nsm
+        rdfly.create_or_replace_rsrc(self.uid, self.provided_imr.graph)
+        self.imr = self.provided_imr
 
-        return gr
+        self._set_containment_rel()
+
+        return RES_CREATED if create else RES_UPDATED
+        #return self._head(self.provided_imr.graph)
 
 
-    def get_version(self, ver_uid, **kwargs):
+    def put(self):
         '''
-        Get a version by label.
+        https://www.w3.org/TR/ldp/#ldpr-HTTP_PUT
         '''
-        ver_gr = self.rdfly.extract_imr(self.uid, ver_uid, **kwargs).graph
+        return self.create_or_replace_rsrc()
 
-        gr = g.tbox.globalize_graph(ver_gr)
-        gr.namespace_manager = nsm
 
-        return gr
+    def patch(self, *args, **kwargs):
+        raise NotImplementedError()
 
 
-    def post(self):
+    def bury_rsrc(self, inbound, tstone_pointer=None):
         '''
-        https://www.w3.org/TR/ldp/#ldpr-HTTP_POST
+        Delete a single resource and create a tombstone.
 
-        Perform a POST action after a valid resource URI has been found.
+        @param inbound (boolean) Whether to delete the inbound relationships.
+        @param tstone_pointer (URIRef) If set to a URN, this creates a pointer
+        to the tombstone of the resource that used to contain the deleted
+        resource. Otherwise the deleted resource becomes a tombstone.
         '''
-        return self._create_or_replace_rsrc(create_only=True)
+        self._logger.info('Burying resource {}'.format(self.uid))
+        # Create a backup snapshot for resurrection purposes.
+        self.create_rsrc_snapshot(uuid4())
 
+        remove_trp = {
+                trp for trp in self.imr.graph
+                if trp[1] != nsc['fcrepo'].hasVersion}
 
-    def put(self):
-        '''
-        https://www.w3.org/TR/ldp/#ldpr-HTTP_PUT
-        '''
-        return self._create_or_replace_rsrc()
+        if tstone_pointer:
+            add_trp = {(self.uri, nsc['fcsystem'].tombstone,
+                    tstone_pointer)}
+        else:
+            add_trp = {
+                (self.uri, RDF.type, nsc['fcsystem'].Tombstone),
+                (self.uri, nsc['fcrepo'].created, g.timestamp_term),
+            }
 
+        self._modify_rsrc(RES_DELETED, remove_trp, add_trp)
 
-    def patch(self, *args, **kwargs):
-        raise NotImplementedError()
+        if inbound:
+            for ib_rsrc_uri in self.imr.graph.subjects(None, self.uri):
+                remove_trp = {(ib_rsrc_uri, None, self.uri)}
+                ib_rsrc = Ldpr(ib_rsrc_uri)
+                # To preserve inbound links in history, create a snapshot
+                ib_rsrc.create_rsrc_snapshot(uuid4())
+                ib_rsrc._modify_rsrc(RES_UPDATED, remove_trp)
 
+        return RES_DELETED
 
-    def delete(self, inbound=True, delete_children=True, leave_tstone=True):
-        '''
-        https://www.w3.org/TR/ldp/#ldpr-HTTP_DELETE
 
-        @param inbound (boolean) If specified, delete all inbound relationships
-        as well. This is the default and is always the case if referential
-        integrity is enforced by configuration.
-        @param delete_children (boolean) Whether to delete all child resources.
-        This is the default.
+    def forget_rsrc(self, inbound=True):
         '''
-        #import pdb; pdb.set_trace()
-        refint = self.rdfly.config['referential_integrity']
+        Remove all traces of a resource and versions.
+        '''
+        self._logger.info('Purging resource {}'.format(self.uid))
+        refint = current_app.config['store']['ldp_rs']['referential_integrity']
         inbound = True if refint else inbound
+        rdfly.forget_rsrc(self.uid, inbound)
 
-        children = (
-            self.rdfly.get_descendants(self.uid)
-            if delete_children else [])
+        # @TODO This could be a different event type.
+        return RES_DELETED
 
-        if leave_tstone:
-            ret = self._bury_rsrc(inbound)
-        else:
-            ret = self._purge_rsrc(inbound)
 
-        for child_uri in children:
-            try:
-                child_rsrc = LdpFactory.from_stored(
-                    g.tbox.uri_to_uuid(child_uri),
-                    repr_opts={'incl_children' : False})
-            except (TombstoneError, ResourceNotExistsError):
-                continue
-            if leave_tstone:
-                child_rsrc._bury_rsrc(inbound, tstone_pointer=self.urn)
+    def create_rsrc_snapshot(self, ver_uid):
+        '''
+        Perform version creation and return the version UID.
+        '''
+        # Create version resource from copying the current state.
+        self._logger.info(
+                'Creating version snapshot {} for resource {}.'.format(
+                    ver_uid, self.uid))
+        ver_add_gr = set()
+        vers_uid = '{}/{}'.format(self.uid, VERS_CONT_LABEL)
+        ver_uid = '{}/{}'.format(vers_uid, ver_uid)
+        ver_uri = nsc['fcres'][ver_uid]
+        ver_add_gr.add((ver_uri, RDF.type, nsc['fcrepo'].Version))
+        for t in self.imr.graph:
+            if (
+                t[1] == RDF.type and t[2] in {
+                    nsc['fcrepo'].Binary,
+                    nsc['fcrepo'].Container,
+                    nsc['fcrepo'].Resource,
+                }
+            ) or (
+                t[1] in {
+                    nsc['fcrepo'].hasParent,
+                    nsc['fcrepo'].hasVersions,
+                    nsc['fcrepo'].hasVersion,
+                    nsc['premis'].hasMessageDigest,
+                }
+            ):
+                pass
             else:
-                child_rsrc._purge_rsrc(inbound)
+                ver_add_gr.add((
+                        g.tbox.replace_term_domain(t[0], self.uri, ver_uri),
+                        t[1], t[2]))
 
-        return ret
+        rdfly.modify_rsrc(ver_uid, add_trp=ver_add_gr)
+
+        # Update resource admin data.
+        rsrc_add_gr = {
+            (self.uri, nsc['fcrepo'].hasVersion, ver_uri),
+            (self.uri, nsc['fcrepo'].hasVersions, nsc['fcres'][vers_uid]),
+        }
+        self._modify_rsrc(RES_UPDATED, add_trp=rsrc_add_gr, notify=False)
 
+        return ver_uid
 
-    def resurrect(self):
+
+    def resurrect_rsrc(self):
         '''
         Resurrect a resource from a tombstone.
 
         @EXPERIMENTAL
         '''
-        tstone_trp = set(self.rdfly.extract_imr(self.uid, strict=False).graph)
+        tstone_trp = set(rdfly.extract_imr(self.uid, strict=False).graph)
 
         ver_rsp = self.version_info.graph.query('''
         SELECT ?uid {
@@ -469,39 +512,27 @@ class Ldpr(metaclass=ABCMeta):
         LIMIT 1
         ''')
         ver_uid = str(ver_rsp.bindings[0]['uid'])
-        ver_trp = set(self.rdfly.get_metadata(self.uid, ver_uid).graph)
+        ver_trp = set(rdfly.get_metadata(self.uid, ver_uid).graph)
 
         laz_gr = Graph()
         for t in ver_trp:
             if t[1] != RDF.type or t[2] not in {
                 nsc['fcrepo'].Version,
             }:
-                laz_gr.add((self.urn, t[1], t[2]))
-        laz_gr.add((self.urn, RDF.type, nsc['fcrepo'].Resource))
+                laz_gr.add((self.uri, t[1], t[2]))
+        laz_gr.add((self.uri, RDF.type, nsc['fcrepo'].Resource))
         if nsc['ldp'].NonRdfSource in laz_gr[: RDF.type :]:
-            laz_gr.add((self.urn, RDF.type, nsc['fcrepo'].Binary))
+            laz_gr.add((self.uri, RDF.type, nsc['fcrepo'].Binary))
         elif nsc['ldp'].Container in laz_gr[: RDF.type :]:
-            laz_gr.add((self.urn, RDF.type, nsc['fcrepo'].Container))
+            laz_gr.add((self.uri, RDF.type, nsc['fcrepo'].Container))
 
-        self._modify_rsrc(self.RES_CREATED, tstone_trp, set(laz_gr))
+        self._modify_rsrc(RES_CREATED, tstone_trp, set(laz_gr))
         self._set_containment_rel()
 
         return self.uri
 
 
 
-    def purge(self, inbound=True):
-        '''
-        Delete a tombstone and all historic snapstots.
-
-        N.B. This does not trigger an event.
-        '''
-        refint = current_app.config['store']['ldp_rs']['referential_integrity']
-        inbound = True if refint else inbound
-
-        return self._purge_rsrc(inbound)
-
-
     def create_version(self, ver_uid=None):
         '''
         Create a new version of the resource.
@@ -516,7 +547,7 @@ class Ldpr(metaclass=ABCMeta):
         if not ver_uid or ver_uid in self.version_uids:
             ver_uid = str(uuid4())
 
-        return g.tbox.globalize_term(self.create_rsrc_snapshot(ver_uid))
+        return self.create_rsrc_snapshot(ver_uid)
 
 
     def revert_to_version(self, ver_uid, backup=True):
@@ -531,9 +562,9 @@ class Ldpr(metaclass=ABCMeta):
         if backup:
             self.create_version()
 
-        ver_gr = self.rdfly.extract_imr(self.uid, ver_uid=ver_uid,
+        ver_gr = rdfly.extract_imr(self.uid, ver_uid=ver_uid,
                 incl_children=False)
-        self.provided_imr = Resource(Graph(), self.urn)
+        self.provided_imr = Resource(Graph(), self.uri)
 
         for t in ver_gr.graph:
             if not self._is_trp_managed(t):
@@ -541,7 +572,7 @@ class Ldpr(metaclass=ABCMeta):
             # @TODO Check individual objects: if they are repo-managed URIs
             # and not existing or tombstones, they are not added.
 
-        return self._create_or_replace_rsrc(create_only=False)
+        return self.create_or_replace_rsrc(create_only=False)
 
 
     ## PROTECTED METHODS ##
@@ -556,126 +587,6 @@ class Ldpr(metaclass=ABCMeta):
                 t[1] == RDF.type and t[2] in srv_mgd_types)
 
 
-    def _create_or_replace_rsrc(self, create_only=False):
-        '''
-        Create or update a resource. PUT and POST methods, which are almost
-        identical, are wrappers for this method.
-
-        @param create_only (boolean) Whether this is a create-only operation.
-        '''
-        create = create_only or not self.is_stored
-
-        self._add_srv_mgd_triples(create)
-        #self._ensure_single_subject_rdf(self.provided_imr.graph)
-        ref_int = self.rdfly.config['referential_integrity']
-        if ref_int:
-            self._check_ref_int(ref_int)
-
-        self.rdfly.create_or_replace_rsrc(self.uid, self.provided_imr.graph)
-        self.imr = self.provided_imr
-
-        self._set_containment_rel()
-
-        return self.RES_CREATED if create else self.RES_UPDATED
-        #return self._head(self.provided_imr.graph)
-
-
-    def _bury_rsrc(self, inbound, tstone_pointer=None):
-        '''
-        Delete a single resource and create a tombstone.
-
-        @param inbound (boolean) Whether to delete the inbound relationships.
-        @param tstone_pointer (URIRef) If set to a URN, this creates a pointer
-        to the tombstone of the resource that used to contain the deleted
-        resource. Otherwise the deleted resource becomes a tombstone.
-        '''
-        self._logger.info('Burying resource {}'.format(self.uid))
-        # Create a backup snapshot for resurrection purposes.
-        self.create_rsrc_snapshot(uuid4())
-
-        remove_trp = {
-                trp for trp in self.imr.graph
-                if trp[1] != nsc['fcrepo'].hasVersion}
-
-        if tstone_pointer:
-            add_trp = {(self.urn, nsc['fcsystem'].tombstone,
-                    tstone_pointer)}
-        else:
-            add_trp = {
-                (self.urn, RDF.type, nsc['fcsystem'].Tombstone),
-                (self.urn, nsc['fcrepo'].created, g.timestamp_term),
-            }
-
-        self._modify_rsrc(self.RES_DELETED, remove_trp, add_trp)
-
-        if inbound:
-            for ib_rsrc_uri in self.imr.graph.subjects(None, self.urn):
-                remove_trp = {(ib_rsrc_uri, None, self.urn)}
-                ib_rsrc = Ldpr(ib_rsrc_uri)
-                # To preserve inbound links in history, create a snapshot
-                ib_rsrc.create_rsrc_snapshot(uuid4())
-                ib_rsrc._modify_rsrc(self.RES_UPDATED, remove_trp)
-
-        return self.RES_DELETED
-
-
-    def _purge_rsrc(self, inbound):
-        '''
-        Remove all traces of a resource and versions.
-        '''
-        self._logger.info('Purging resource {}'.format(self.uid))
-        self.rdfly.purge_rsrc(self.uid, inbound)
-
-        # @TODO This could be a different event type.
-        return self.RES_DELETED
-
-
-    def create_rsrc_snapshot(self, ver_uid):
-        '''
-        Perform version creation and return the internal URN.
-        '''
-        # Create version resource from copying the current state.
-        self._logger.info(
-                'Creating version snapshot {} for resource {}.'.format(
-                    ver_uid, self.uid))
-        ver_add_gr = set()
-        vers_uid = '{}/{}'.format(self.uid, VERS_CONT_LABEL)
-        ver_uid = '{}/{}'.format(vers_uid, ver_uid)
-        ver_uri = nsc['fcres'][ver_uid]
-        ver_add_gr.add((ver_uri, RDF.type, nsc['fcrepo'].Version))
-        for t in self.imr.graph:
-            if (
-                t[1] == RDF.type and t[2] in {
-                    nsc['fcrepo'].Binary,
-                    nsc['fcrepo'].Container,
-                    nsc['fcrepo'].Resource,
-                }
-            ) or (
-                t[1] in {
-                    nsc['fcrepo'].hasParent,
-                    nsc['fcrepo'].hasVersions,
-                    nsc['fcrepo'].hasVersion,
-                    nsc['premis'].hasMessageDigest,
-                }
-            ):
-                pass
-            else:
-                ver_add_gr.add((
-                        g.tbox.replace_term_domain(t[0], self.urn, ver_uri),
-                        t[1], t[2]))
-
-        self.rdfly.modify_rsrc(ver_uid, add_trp=ver_add_gr)
-
-        # Update resource admin data.
-        rsrc_add_gr = {
-            (self.urn, nsc['fcrepo'].hasVersion, ver_uri),
-            (self.urn, nsc['fcrepo'].hasVersions, nsc['fcres'][vers_uid]),
-        }
-        self._modify_rsrc(self.RES_UPDATED, add_trp=rsrc_add_gr, notify=False)
-
-        return nsc['fcres'][ver_uid]
-
-
     def _modify_rsrc(self, ev_type, remove_trp=set(), add_trp=set(),
              notify=True):
         '''
@@ -690,7 +601,7 @@ class Ldpr(metaclass=ABCMeta):
         @param add_trp (set) Triples to be added.
         @param notify (boolean) Whether to send a message about the change.
         '''
-        ret = self.rdfly.modify_rsrc(self.uid, remove_trp, add_trp)
+        ret = rdfly.modify_rsrc(self.uid, remove_trp, add_trp)
 
         if notify and current_app.config.get('messaging'):
             self._enqueue_msg(ev_type, remove_trp, add_trp)
@@ -714,9 +625,7 @@ class Ldpr(metaclass=ABCMeta):
                 elif actor is None and t[1] == nsc['fcrepo'].createdBy:
                     actor = t[2]
 
-        if not hasattr(g, 'changelog'):
-            g.changelog = []
-        g.changelog.append((set(remove_trp), set(add_trp), {
+        changelog.append((set(remove_trp), set(add_trp), {
             'ev_type' : ev_type,
             'time' : g.timestamp,
             'type' : type,
@@ -728,8 +637,8 @@ class Ldpr(metaclass=ABCMeta):
         gr = self.provided_imr.graph
 
         for o in gr.objects():
-            if isinstance(o, URIRef) and str(o).startswith(g.webroot)\
-                    and not self.rdfly.ask_rsrc_exists(o):
+            if isinstance(o, URIRef) and str(o).startswith(nsc['fcres'])\
+                    and not rdfly.ask_rsrc_exists(o):
                 if config == 'strict':
                     raise RefIntViolationError(o)
                 else:
@@ -819,16 +728,18 @@ class Ldpr(metaclass=ABCMeta):
         This function may recurse up the path tree until an existing container
         is found.
 
-        E.g. if only urn:fcres:a (short: a) exists:
-        - If a/b/c/d is being created, a becomes container of a/b/c/d. Also,
-          containers are created for a/b and a/b/c.
-        - If e is being created, the root node becomes container of e.
+        E.g. if only fcres:a exists:
+        - If fcres:a/b/c/d is being created, a becomes container of
+          fcres:a/b/c/d. Also, containers are created for fcres:a/b and
+          fcres:a/b/c.
+        - If fcres:e is being created, the root node becomes container of
+          fcres:e.
         '''
         if '/' in self.uid:
             # Traverse up the hierarchy to find the parent.
             path_components = self.uid.split('/')
             cnd_parent_uid = '/'.join(path_components[:-1])
-            if self.rdfly.ask_rsrc_exists(cnd_parent_uid):
+            if rdfly.ask_rsrc_exists(cnd_parent_uid):
                 parent_rsrc = LdpFactory.from_stored(cnd_parent_uid)
                 if nsc['ldp'].Container not in parent_rsrc.types:
                     raise InvalidResourceError(parent_uid,
@@ -845,11 +756,11 @@ class Ldpr(metaclass=ABCMeta):
             parent_uid = ROOT_UID
 
         add_gr = Graph()
-        add_gr.add((nsc['fcres'][parent_uid], nsc['ldp'].contains, self.urn))
+        add_gr.add((nsc['fcres'][parent_uid], nsc['ldp'].contains, self.uri))
         parent_rsrc = LdpFactory.from_stored(
                 parent_uid, repr_opts={'incl_children' : False},
                 handling='none')
-        parent_rsrc._modify_rsrc(self.RES_UPDATED, add_trp=add_gr)
+        parent_rsrc._modify_rsrc(RES_UPDATED, add_trp=add_gr)
 
         # Direct or indirect container relationship.
         self._add_ldp_dc_ic_rel(parent_rsrc)
@@ -877,7 +788,7 @@ class Ldpr(metaclass=ABCMeta):
         self._logger.info('Checking direct or indirect containment.')
         self._logger.debug('Parent predicates: {}'.format(cont_p))
 
-        add_trp = {(self.urn, nsc['fcrepo'].hasParent, cont_rsrc.urn)}
+        add_trp = {(self.uri, nsc['fcrepo'].hasParent, cont_rsrc.uri)}
 
         if self.MBR_RSRC_URI in cont_p and self.MBR_REL_URI in cont_p:
             s = cont_rsrc.metadata.value(self.MBR_RSRC_URI).identifier
@@ -887,7 +798,7 @@ class Ldpr(metaclass=ABCMeta):
                 self._logger.info('Parent is a direct container.')
 
                 self._logger.debug('Creating DC triples.')
-                o = self.urn
+                o = self.uri
 
             elif cont_rsrc.metadata[RDF.type : nsc['ldp'].IndirectContainer] \
                    and self.INS_CNT_REL_URI in cont_p:
@@ -898,7 +809,7 @@ class Ldpr(metaclass=ABCMeta):
                 self._logger.debug('Target URI: {}'.format(o))
                 self._logger.debug('Creating IC triples.')
 
-            target_rsrc = LdpFactory.from_stored(g.tbox.uri_to_uuid(s))
-            target_rsrc._modify_rsrc(self.RES_UPDATED, add_trp={(s, p, o)})
+            target_rsrc = LdpFactory.from_stored(rdfly.uri_to_uid(s))
+            target_rsrc._modify_rsrc(RES_UPDATED, add_trp={(s, p, o)})
 
-        self._modify_rsrc(self.RES_UPDATED, add_trp=add_trp)
+        self._modify_rsrc(RES_UPDATED, add_trp=add_trp)

+ 15 - 1
lakesuperior/store/ldp_nr/default_layout.py

@@ -1,4 +1,5 @@
 import os
+import shutil
 
 from hashlib import sha1
 from uuid import uuid4
@@ -13,6 +14,17 @@ class DefaultLayout(BaseNonRdfLayout):
 
     ## INTERFACE METHODS ##
 
+    def bootstrap(self):
+        '''
+        Initialize binary file store.
+        '''
+        try:
+            shutil.rmtree(self.root)
+        except FileNotFoundError:
+            pass
+        os.makedirs(self.root + '/tmp')
+
+
     def persist(self, stream, bufsize=8192):
         '''
         Store the stream in the file system.
@@ -31,12 +43,14 @@ class DefaultLayout(BaseNonRdfLayout):
                 self._logger.debug('Writing temp file to {}.'.format(tmp_file))
 
                 hash = sha1()
+                size = 0
                 while True:
                     buf = stream.read(bufsize)
                     if not buf:
                         break
                     hash.update(buf)
                     f.write(buf)
+                    size += len(buf)
         except:
             self._logger.exception('File write failed on {}.'.format(tmp_file))
             os.unlink(tmp_file)
@@ -57,7 +71,7 @@ class DefaultLayout(BaseNonRdfLayout):
         else:
             os.rename(tmp_file, dst)
 
-        return uuid
+        return uuid, size
 
 
     def delete(self, uuid):

+ 18 - 11
lakesuperior/store/ldp_rs/rsrc_centric_layout.py

@@ -3,7 +3,6 @@ import logging
 from collections import defaultdict
 from itertools import chain
 
-from flask import g
 from rdflib import Dataset, Graph, Literal, URIRef, plugin
 from rdflib.namespace import RDF
 from rdflib.query import ResultException
@@ -273,11 +272,6 @@ class RsrcCentricLayout:
             uid = self.snapshot_uid(uid, ver_uid)
         gr = self.ds.graph(nsc['fcadmin'][uid]) | Graph()
         uri = nsc['fcres'][uid]
-        if not len(gr):
-            # If no resource is found, search in pairtree graph.
-            gr = Graph()
-            for p, o in self.ds.graph(PTREE_GR_URI)[uri : : ]:
-                gr.add(uri, p, o)
 
         rsrc = Resource(gr, uri)
         if strict:
@@ -293,6 +287,11 @@ class RsrcCentricLayout:
         # @NOTE This pretty much bends the ontology—it replaces the graph URI
         # with the subject URI. But the concepts of data and metadata in Fedora
         # are quite fluid anyways...
+        # WIP—Is it worth to replace SPARQL here?
+        #versions = self.ds.graph(nsc['fcadmin'][uid]).triples(
+        #        (nsc['fcres'][uid], nsc['fcrepo'].hasVersion, None))
+        #for version in versions:
+        #    version_meta = self.ds.graph(HIST_GRAPH_URI).triples(
         qry = '''
         CONSTRUCT {
           ?s fcrepo:hasVersion ?v .
@@ -312,6 +311,7 @@ class RsrcCentricLayout:
             'hg': HIST_GR_URI,
             's': nsc['fcres'][uid]})
         rsrc = Resource(gr, nsc['fcres'][uid])
+        # @TODO Should return a graph.
         if strict:
             self._check_rsrc_status(rsrc)
 
@@ -392,7 +392,7 @@ class RsrcCentricLayout:
         return gr.update(qry)
 
 
-    def purge_rsrc(self, uid, inbound=True, children=True):
+    def forget_rsrc(self, uid, inbound=True, children=True):
         '''
         Completely delete a resource and (optionally) its children and inbound
         references.
@@ -402,13 +402,13 @@ class RsrcCentricLayout:
         # Localize variables to be used in loops.
         uri = nsc['fcres'][uid]
         topic_uri = nsc['foaf'].primaryTopic
-        uid_fn = g.tbox.uri_to_uuid
+        uid_fn = self.uri_to_uid
 
         # remove children.
         if children:
             self._logger.debug('Purging children for /{}'.format(uid))
             for rsrc_uri in self.get_descendants(uid, False):
-                self.purge_rsrc(uid_fn(rsrc_uri), inbound, False)
+                self.forget_rsrc(uid_fn(rsrc_uri), inbound, False)
             # Remove structure graph.
             self.ds.remove_graph(nsc['fcstruct'][uid])
 
@@ -528,13 +528,20 @@ class RsrcCentricLayout:
             gr.remove((None, RDF.type, t))
 
 
+    def uri_to_uid(self, uri):
+        '''
+        Convert an internal URI to a UID.
+        '''
+        return str(uri).replace(nsc['fcres'], '')
+
+
     ## PROTECTED MEMBERS ##
 
     def _check_rsrc_status(self, rsrc):
         '''
         Check if a resource is not existing or if it is a tombstone.
         '''
-        uid = g.tbox.uri_to_uuid(rsrc.identifier)
+        uid = self.uri_to_uid(rsrc.identifier)
         if not len(rsrc.graph):
             raise ResourceNotExistsError(uid)
 
@@ -544,7 +551,7 @@ class RsrcCentricLayout:
                     uid, rsrc.value(nsc['fcrepo'].created))
         elif rsrc.value(nsc['fcsystem'].tombstone):
             raise TombstoneError(
-                    g.tbox.uri_to_uuid(
+                    self.uri_to_uid(
                         rsrc.value(nsc['fcsystem'].tombstone).identifier),
                         rsrc.value(nsc['fcrepo'].created))
 

+ 2 - 5
lakesuperior/toolbox.py

@@ -37,7 +37,7 @@ class Toolbox:
         return URIRef(s)
 
 
-    def uuid_to_uri(self, uid):
+    def uid_to_uri(self, uid):
         '''Convert a UID to a URI.
 
         @return URIRef
@@ -47,7 +47,7 @@ class Toolbox:
         return URIRef(uri)
 
 
-    def uri_to_uuid(self, uri):
+    def uri_to_uid(self, uri):
         '''Convert an absolute URI (internal or external) to a UID.
 
         @return string
@@ -244,9 +244,6 @@ class Toolbox:
         '''
         Generate a checksum for a graph.
 
-        This is not straightforward because a graph is derived from an
-        unordered data structure (RDF).
-
         What this method does is ordering the graph by subject, predicate,
         object, then creating a pickle string and a checksum of it.
 

+ 6 - 6
profiler.py

@@ -5,14 +5,14 @@ from werkzeug.contrib.profiler import ProfilerMiddleware
 from lakesuperior.app import create_app
 from lakesuperior.config_parser import config
 
-
-fcrepo = create_app(config['application'], config['logging'])
-
 options = {
     'restrictions': [30],
     #'profile_dir': '/tmp/lsup_profiling'
 }
-fcrepo.wsgi_app = ProfilerMiddleware(fcrepo.wsgi_app, **options)
-fcrepo.config['PROFILE'] = True
-fcrepo.run(debug = True)
+
+if __name__ == '__main__':
+    fcrepo = create_app(config['application'])
+    fcrepo.wsgi_app = ProfilerMiddleware(fcrepo.wsgi_app, **options)
+    fcrepo.config['PROFILE'] = True
+    fcrepo.run(debug = True)
 

+ 6 - 4
server.py

@@ -1,11 +1,13 @@
-from flask import render_template
-
 from lakesuperior.app import create_app
 from lakesuperior.config_parser import config
+from lakesuperior.globals import AppGlobals
+from lakesuperior.env import env
 
+env.config = config
+env.app_globals = AppGlobals(config)
+dictConfig(env.config['logging'])
 
-fcrepo = create_app(config['application'], config['logging'])
-
+fcrepo = create_app(env.config['application'])
 
 if __name__ == "__main__":
     fcrepo.run(host='0.0.0.0')

+ 0 - 96
tests/10K_children.py

@@ -1,96 +0,0 @@
-#!/usr/bin/env python
-import sys
-sys.path.append('.')
-
-from uuid import uuid4
-
-import arrow
-import requests
-
-from rdflib import Graph, URIRef, Literal
-
-from util.generators import random_utf8_string
-
-
-default_n = 10000
-webroot = 'http://localhost:8000/ldp'
-#webroot = 'http://localhost:8080/rest'
-container_uri = webroot + '/pomegranate'
-
-sys.stdout.write('How many children? [{}] >'.format(default_n))
-choice = input().lower()
-n = int(choice) if choice else default_n
-
-sys.stdout.write('Delete container? [n] >')
-choice = input().lower()
-del_cont = choice or 'n'
-
-sys.stdout.write('POST or PUT? [PUT] >')
-choice = input().lower()
-if choice and choice.lower() not in ('post', 'put'):
-    raise ValueError('Not a valid verb.')
-method = choice.lower() or 'put'
-
-# Generate 10,000 children of root node.
-
-if del_cont  == 'y':
-    requests.delete(container_uri, headers={'prefer': 'no-tombstone'})
-requests.put(container_uri)
-
-
-start = arrow.utcnow()
-ckpt = start
-
-print('Inserting {} children.'.format(n))
-
-# URI used to establish an in-repo relationship.
-prev_uri = container_uri
-size = 50 # Size of graph to be multiplied by 4.
-
-try:
-    for i in range(1, n):
-        url = '{}/{}'.format(container_uri, uuid4()) if method == 'put' \
-                else container_uri
-
-        # Generate synthetic graph.
-        #print('generating graph: {}'.format(i))
-        g = Graph()
-        for ii in range(size):
-            g.add((
-                URIRef(''),
-                URIRef('urn:inturi_p:{}'.format(ii % size)),
-                URIRef(prev_uri)
-            ))
-            g.add((
-                URIRef(''),
-                URIRef('urn:lit_p:{}'.format(ii % size)),
-                Literal(random_utf8_string(64))
-            ))
-            g.add((
-                URIRef(''),
-                URIRef('urn:lit_p:{}'.format(ii % size)),
-                Literal(random_utf8_string(64))
-            ))
-            g.add((
-                URIRef(''),
-                URIRef('urn:exturi_p:{}'.format(ii % size)),
-                URIRef('http://exmple.edu/res/{}'.format(ii // 10))
-            ))
-
-        # Send request.
-        rsp = requests.request(
-                method, url, data=g.serialize(format='ttl'),
-                headers={ 'content-type': 'text/turtle'})
-        rsp.raise_for_status()
-        prev_uri = rsp.headers['location']
-        if i % 10 == 0:
-            now = arrow.utcnow()
-            tdelta = now - ckpt
-            ckpt = now
-            print('Record: {}\tTime elapsed: {}'.format(i, tdelta))
-except KeyboardInterrupt:
-    print('Interruped after {} iterations.'.format(i))
-
-tdelta = arrow.utcnow() - start
-print('Total elapsed time: {}'.format(tdelta))
-print('Average time per resource: {}'.format(tdelta.total_seconds()/i))

+ 0 - 75
tests/bdb.py

@@ -1,75 +0,0 @@
-#!/usr/bin/env python
-import sys
-
-from random import randrange
-from uuid import uuid4
-
-import arrow
-
-from rdflib import Dataset
-from rdflib import plugin
-from rdflib.store import Store
-from rdflib.term import URIRef
-
-default_n = 10000
-sys.stdout.write('How many resources? [{}] >'.format(default_n))
-choice = input().lower()
-n = int(choice) if choice else default_n
-store_uid = randrange(8192)
-store_name = '/tmp/lsup_{}.db'.format(store_uid)
-
-store = plugin.get('Sleepycat', Store)()
-ds = Dataset(store)
-store.open(store_name)
-
-start = arrow.utcnow()
-ckpt = start
-
-for i in range(1, n):
-    try:
-        subj = URIRef('http://ex.org/rdf/{}'.format(uuid4()))
-        pomegranate = URIRef('http://ex.org/pomegranate')
-        #gr = ds.graph('http://ex.org/graph#g{}'.format(i))
-        gr = ds.graph('http://ex.org/graph#g1')
-        for ii in range(1, 100):
-            gr.add((subj, URIRef('http://ex.org/p1'),
-                URIRef('http://ex.org/random#'.format(randrange(2048)))))
-        gr.add((pomegranate, URIRef('http://ex.org/p2'), subj))
-
-        q = '''
-        CONSTRUCT {
-            ?meta_s ?meta_p ?meta_o .
-            ?s ?p ?o .
-            ?s <info:fcrepo#writable> true .
-        }
-        WHERE {
-          GRAPH ?mg {
-            ?meta_s ?meta_p ?meta_o .
-          }
-          OPTIONAL {
-            GRAPH ?sg {
-              ?s ?p ?o .
-              FILTER ( ?p != <http://ex.org/p2> )
-            }
-          }
-        }
-        '''
-        qres = ds.query(q, initBindings={'s': pomegranate, 'mg': gr, 'sg': gr})
-
-        if i % 100 == 0:
-            now = arrow.utcnow()
-            tdelta = now - ckpt
-            ckpt = now
-            print('Record: {}\tTime this round: {}'.format(i, tdelta))
-            #print('Qres size: {}'.format(len(qres)))
-    except KeyboardInterrupt:
-        print('Interrupted after {} iterations.'.format(i))
-        break
-
-tdelta = arrow.utcnow() - start
-print('Store name: {}'.format(store_name))
-print('Total elapsed time: {}'.format(tdelta))
-print('Average time per resource: {}'.format(tdelta.total_seconds()/i))
-print('Graph size: {}'.format(len(gr)))
-
-store.close()

+ 26 - 1
tests/endpoints/test_ldp.py

@@ -1,3 +1,4 @@
+import pdb
 import pytest
 import uuid
 
@@ -141,11 +142,35 @@ class TestLdp:
         rnd_img['content'].seek(0)
         resp = self.client.put('/ldp/ldpnr01', data=rnd_img['content'],
                 headers={
+                    'Content-Type': 'image/png',
                     'Content-Disposition' : 'attachment; filename={}'.format(
                     rnd_img['filename'])})
         assert resp.status_code == 201
 
-        resp = self.client.get('/ldp/ldpnr01', headers={'accept' : 'image/png'})
+        resp = self.client.get(
+                '/ldp/ldpnr01', headers={'accept' : 'image/png'})
+        assert resp.status_code == 200
+        assert sha1(resp.data).hexdigest() == rnd_img['hash']
+
+
+    def test_put_ldp_nr_multipart(self, rnd_img):
+        '''
+        PUT a resource with a multipart/form-data payload.
+        '''
+        rnd_img['content'].seek(0)
+        resp = self.client.put(
+            '/ldp/ldpnr02',
+            data={
+                'file': (
+                    rnd_img['content'], rnd_img['filename'],
+                    'image/png',
+                )
+            }
+        )
+        assert resp.status_code == 201
+
+        resp = self.client.get(
+                '/ldp/ldpnr02', headers={'accept' : 'image/png'})
         assert resp.status_code == 200
         assert sha1(resp.data).hexdigest() == rnd_img['hash']
 

+ 0 - 169
tests/initial_tests.py

@@ -1,169 +0,0 @@
-#!/usr/bin/env python3
-
-## Small set of tests to begin with.
-## For testing, import this file:
-##
-## `from tests.initial_tests import *`
-##
-## Then clear the data store with clear() and run
-## individual functions inspecting the dataset at each step.
-
-import pdb
-
-import rdflib
-
-from rdflib.graph import Dataset
-from rdflib.namespace import RDF
-from rdflib.plugins.stores.sparqlstore import SPARQLUpdateStore
-from rdflib.term import URIRef
-
-query_ep = 'http://localhost:3030/lakesuperior-dev/query'
-update_ep = 'http://localhost:3030/lakesuperior-dev/update'
-
-store = SPARQLUpdateStore(queryEndpoint=query_ep, update_endpoint=update_ep,
-        autocommit=False)
-ds = Dataset(store, default_union=True)
-
-
-def query(q):
-    res = ds.query(q)
-    print(res.serialize().decode('utf-8'))
-
-
-def clear():
-    '''Clear triplestore.'''
-    for g in ds.graphs():
-        ds.remove_graph(g)
-    store.commit()
-    print('All graphs removed from store.')
-
-
-def insert(report=False):
-    '''Add a resource.'''
-
-    res1 = ds.graph(URIRef('urn:res:12873624'))
-    meta1 = ds.graph(URIRef('urn:meta:12873624'))
-    res1.add((URIRef('urn:state:001'), RDF.type, URIRef('http://example.edu#Blah')))
-
-    meta1.add((URIRef('urn:state:001'), RDF.type, URIRef('http://example.edu#ActiveState')))
-    store.commit()
-
-    if report:
-        print('Inserted resource:')
-        query('''
-            SELECT ?s ?p ?o
-            FROM <urn:res:12873624>
-            FROM <urn:meta:12873624> {
-                ?s a <http://example.edu#ActiveState> .
-                ?s ?p ?o .
-            }'''
-        )
-
-
-def update(report=False):
-    '''Update resource and create a historic snapshot.'''
-
-    res1 = ds.graph(URIRef('urn:res:12873624'))
-    meta1 = ds.graph(URIRef('urn:meta:12873624'))
-    res1.add((URIRef('urn:state:002'), RDF.type, URIRef('http://example.edu#Boo')))
-
-    meta1.remove((URIRef('urn:state:001'), RDF.type, URIRef('http://example.edu#ActiveState')))
-    meta1.add((URIRef('urn:state:001'), RDF.type, URIRef('http://example.edu#Snapshot')))
-    meta1.add((URIRef('urn:state:002'), RDF.type, URIRef('http://example.edu#ActiveState')))
-    meta1.add((URIRef('urn:state:002'), URIRef('http://example.edu#prevState'), URIRef('urn:state:001')))
-    store.commit()
-
-    if report:
-        print('Updated resource:')
-        query('''
-            SELECT ?s ?p ?o
-            FROM <urn:res:12873624>
-            FROM <urn:meta:12873624> {
-                ?s a <http://example.edu#ActiveState> .
-                ?s ?p ?o .
-            }'''
-        )
-        print('Version snapshot:')
-        query('''
-            SELECT ?s ?p ?o
-            FROM <urn:res:12873624>
-            FROM <urn:meta:12873624> {
-                ?s a <http://example.edu#Snapshot> .
-                ?s ?p ?o .
-            }'''
-        )
-
-
-def delete(report=False):
-    '''Delete resource and leave a tombstone.'''
-
-    meta1 = ds.graph(URIRef('urn:meta:12873624'))
-    meta1.remove((URIRef('urn:state:002'), RDF.type, URIRef('http://example.edu#ActiveState')))
-    meta1.add((URIRef('urn:state:002'), RDF.type, URIRef('http://example.edu#Tombstone')))
-    store.commit()
-
-    if report:
-        print('Deleted resource (tombstone):')
-        query('''
-            SELECT ?s ?p ?o
-            FROM <urn:res:12873624>
-            FROM <urn:meta:12873624> {
-                ?s a <http://example.edu#Tombstone> .
-                ?s ?p ?o .
-            }'''
-        )
-
-
-def undelete(report=False):
-    '''Resurrect resource from a tombstone.'''
-
-    meta1 = ds.graph(URIRef('urn:meta:12873624'))
-    meta1.remove((URIRef('urn:state:002'), RDF.type, URIRef('http://example.edu#Tombstone')))
-    meta1.add((URIRef('urn:state:002'), RDF.type, URIRef('http://example.edu#ActiveState')))
-    store.commit()
-
-    if report:
-        print('Undeleted resource:')
-        query('''
-            SELECT ?s ?p ?o
-            FROM <urn:res:12873624>
-            FROM <urn:meta:12873624> {
-                ?s a <http://example.edu#ActiveState> .
-                ?s ?p ?o .
-            }'''
-        )
-
-
-def abort_tx(report=False):
-    '''Abort an operation in the middle of a transaction and roll back.'''
-
-    try:
-        res2 = ds.graph(URIRef('urn:state:002'))
-        res2.add((URIRef('urn:lake:12873624'), RDF.type, URIRef('http://example.edu#Werp')))
-        raise RuntimeError('Something awful happened!')
-        store.commit()
-    except RuntimeError as e:
-        print('Exception caught: {}'.format(e))
-        store.rollback()
-
-    if report:
-        print('Failed operation (no updates):')
-        query('''
-            SELECT ?s ?p ?o
-            FROM <urn:res:12873624>
-            FROM <urn:meta:12873624> {
-                ?s a <http://example.edu#ActiveState> .
-                ?s ?p ?o .
-            }'''
-        )
-
-
-def partial_query(report=False):
-    '''Execute a query containing a token that throws an error in the middle.
-
-    The purpose of this is to verify whether the store is truly transactional,
-    i.e. the whole operation in a transaction is rolled back even if some
-    updates have already been processed.'''
-
-    # @TODO
-    pass

+ 0 - 3
tests/siege.txt

@@ -1,3 +0,0 @@
-# Use with Siege, e.g.: siege -f tests/siege.txt -t25 -c8
-
-http://localhost:8000/ldp/pomegranate

+ 10 - 10
tests/test_toolbox.py

@@ -26,19 +26,19 @@ class TestToolbox:
     #    assert g.tbox.camelcase('_test_input_string') == '_TestInputString'
     #    assert g.tbox.camelcase('test__input__string') == 'Test_Input_String'
 
-    def test_uuid_to_uri(self):
-        assert g.tbox.uuid_to_uri('1234') == URIRef(g.webroot + '/1234')
-        assert g.tbox.uuid_to_uri('') == URIRef(g.webroot)
+    def test_uid_to_uri(self):
+        assert g.tbox.uid_to_uri('1234') == URIRef(g.webroot + '/1234')
+        assert g.tbox.uid_to_uri('') == URIRef(g.webroot)
 
 
-    def test_uri_to_uuid(self):
-        assert g.tbox.uri_to_uuid(URIRef(g.webroot) + '/test01') == 'test01'
-        assert g.tbox.uri_to_uuid(URIRef(g.webroot) + '/test01/test02') == \
+    def test_uri_to_uid(self):
+        assert g.tbox.uri_to_uid(URIRef(g.webroot) + '/test01') == 'test01'
+        assert g.tbox.uri_to_uid(URIRef(g.webroot) + '/test01/test02') == \
                 'test01/test02'
-        assert g.tbox.uri_to_uuid(URIRef(g.webroot)) == ''
-        assert g.tbox.uri_to_uuid(nsc['fcres']['']) == ''
-        assert g.tbox.uri_to_uuid(nsc['fcres']['1234']) == '1234'
-        assert g.tbox.uri_to_uuid(nsc['fcres']['1234/5678']) == '1234/5678'
+        assert g.tbox.uri_to_uid(URIRef(g.webroot)) == ''
+        assert g.tbox.uri_to_uid(nsc['fcres']['']) == ''
+        assert g.tbox.uri_to_uid(nsc['fcres']['1234']) == '1234'
+        assert g.tbox.uri_to_uid(nsc['fcres']['1234/5678']) == '1234/5678'
 
 
     def test_localize_string(self):

+ 16 - 37
util/bootstrap.py

@@ -1,12 +1,12 @@
 #!/usr/bin/env python
 
 import os
-import shutil
 import sys
 sys.path.append('.')
 
-from lakesuperior.app import create_app
-from lakesuperior.config_parser import config
+import lakesuperior.env_setup
+
+from lakesuperior.env import env
 from lakesuperior.store.ldp_rs.lmdb_store import TxnManager
 from lakesuperior.model.ldpr import Ldpr
 
@@ -15,40 +15,19 @@ This script will parse configuration files and initialize a filesystem and
 triplestore with an empty FCREPO repository.
 It is used in test suites and on a first run.
 
-Additional, scaffolding files may be parsed to create initial contents.
+Additional scaffolding files may be parsed to create initial contents.
 '''
 
+sys.stdout.write(
+        'This operation will WIPE ALL YOUR DATA. Are you sure? '
+        '(Please type `yes` to continue) > ')
+choice = input().lower()
+if choice != 'yes':
+    print('Aborting.')
+    sys.exit()
+
+with TxnManager(env.app_globals.rdf_store, write=True) as txn:
+    env.app_globals.rdfly.bootstrap()
+    env.app_globals.rdfly.store.close()
 
-def bootstrap_binary_store(app):
-    '''
-    Initialize binary file store.
-    '''
-    root_path = app.config['store']['ldp_nr']['path']
-    print('Removing binary store path: {}'.format(root_path))
-    try:
-        shutil.rmtree(root_path)
-    except FileNotFoundError:
-        pass
-    print('Recreating binary store path: {}'.format(root_path))
-    os.makedirs(root_path + '/tmp')
-    print('Binary store initialized.')
-
-
-if __name__=='__main__':
-    sys.stdout.write(
-            'This operation will WIPE ALL YOUR DATA. Are you sure? '
-            '(Please type `yes` to continue) > ')
-    choice = input().lower()
-    if choice != 'yes':
-        print('Aborting.')
-        sys.exit()
-
-    app = create_app(config['application'], config['logging'])
-    if hasattr(app.rdfly.store, 'begin'):
-        with TxnManager(app.rdfly.store, write=True) as txn:
-            app.rdfly.bootstrap()
-            app.rdfly.store.close()
-    else:
-        app.rdfly.bootstrap()
-
-    bootstrap_binary_store(app)
+env.app_globals.nonrdfly.bootstrap()