diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/Cache.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/Cache.java
index 9b714f3b8895beee4bf4d9a3eae838f47122fa69..157e7b401d99576b876fb1d9f0d42aa5fd4f5ddb 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/Cache.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/Cache.java
@@ -50,6 +50,8 @@ public class Cache
         }
     }
 
+    private final Map<String, Node> nodesByPath = new HashMap<>();
+
     private final Map<String, TimeStampedObject<FtpFile>> filesByPath = new HashMap<>();
 
     private final Map<String, TimeStampedObject<DataSet>> dataSetsByCode = new HashMap<String, Cache.TimeStampedObject<DataSet>>();
@@ -74,6 +76,16 @@ public class Cache
         this.timeProvider = timeProvider;
     }
 
+    public void putNode(Node node, String path)
+    {
+        nodesByPath.put(path, node);
+    }
+
+    public Node getNode(String path)
+    {
+        return nodesByPath.get(path);
+    }
+
     public void putFile(FtpFile file, String path)
     {
         filesByPath.put(path, timestamp(file));
@@ -162,9 +174,16 @@ public class Cache
     private <T> T getObject(Map<String, TimeStampedObject<T>> map, String key)
     {
         TimeStampedObject<T> timeStampedObject = map.get(key);
-        return timeStampedObject == null
-                || timeProvider.getTimeInMilliseconds() - timeStampedObject.timestamp > LIVE_TIME ? null
-                        : timeStampedObject.object;
+        if (timeStampedObject == null)
+        {
+            return null;
+        }
+        if (timeProvider.getTimeInMilliseconds() - timeStampedObject.timestamp > LIVE_TIME)
+        {
+            map.remove(key);
+            return null;
+        }
+        return timeStampedObject.object;
     }
 
 }
\ No newline at end of file
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/Node.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/Node.java
new file mode 100644
index 0000000000000000000000000000000000000000..92fa3744325dd67c54ad99952be2848d2d4b5489
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/Node.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2021 ETH Zuerich, SIS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.dss.generic.server.ftp;
+
+/**
+ * @author Franz-Josef Elmer
+ *
+ */
+public class Node
+{
+    private final String type;
+    private final String permId;
+
+    public Node(String type, String permId)
+    {
+        this.type = type;
+        this.permId = permId;
+    }
+
+    public String getType()
+    {
+        return type;
+    }
+
+    public String getPermId()
+    {
+        return permId;
+    }
+}
diff --git a/openbis_standard_technologies/dist/core-plugins/eln-lims/1/dss/file-system-plugins/eln-tree/plugin.properties b/openbis_standard_technologies/dist/core-plugins/eln-lims/1/dss/file-system-plugins/eln-tree/plugin.properties
new file mode 100644
index 0000000000000000000000000000000000000000..108bd92e19d2d0517881b3a280daeece8970a539
--- /dev/null
+++ b/openbis_standard_technologies/dist/core-plugins/eln-lims/1/dss/file-system-plugins/eln-tree/plugin.properties
@@ -0,0 +1,3 @@
+resolver-class = ch.systemsx.cisd.openbis.dss.generic.server.fs.plugins.JythonResolver
+code = Lab Notebook
+script-file = script.py
\ No newline at end of file
diff --git a/openbis_standard_technologies/dist/core-plugins/eln-lims/1/dss/file-system-plugins/eln-tree/script.py b/openbis_standard_technologies/dist/core-plugins/eln-lims/1/dss/file-system-plugins/eln-tree/script.py
new file mode 100644
index 0000000000000000000000000000000000000000..5a3a28e26c9fc7901f2c77f56f274b0688772d5f
--- /dev/null
+++ b/openbis_standard_technologies/dist/core-plugins/eln-lims/1/dss/file-system-plugins/eln-tree/script.py
@@ -0,0 +1,77 @@
+from ch.ethz.sis.openbis.generic.asapi.v3.dto.space.search import SpaceSearchCriteria
+from ch.ethz.sis.openbis.generic.asapi.v3.dto.space.fetchoptions import SpaceFetchOptions
+from ch.ethz.sis.openbis.generic.asapi.v3.dto.project.search import ProjectSearchCriteria
+from ch.ethz.sis.openbis.generic.asapi.v3.dto.project.fetchoptions import ProjectFetchOptions
+from ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.search import ExperimentSearchCriteria
+from ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.fetchoptions import ExperimentFetchOptions
+from ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.search import SampleSearchCriteria
+from ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.fetchoptions import SampleFetchOptions
+from ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.search import DataSetSearchCriteria
+from ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.fetchoptions import DataSetFetchOptions
+from ch.systemsx.cisd.openbis.dss.generic.server.ftp import Node
+
+def resolve(subPath, context):
+    path = "/".join(subPath)
+    if len(subPath) == 0:
+        return listSpaces(context)
+    space = subPath[0]
+    if len(subPath) == 1:
+        return listProjects(space, context)
+    project = subPath[1]
+    if len(subPath) == 2:
+        return listExperiments(space, project, context)
+    experiment = subPath[2]
+    if len(subPath) == 3:
+        return listExperimentContent(path, context)
+
+def listSpaces(context):
+    spaces = context.getApi().searchSpaces(context.getSessionToken(), SpaceSearchCriteria(), SpaceFetchOptions()).getObjects()
+    response = context.createDirectoryResponse()
+    for space in spaces:
+        response.addDirectory(space.getCode(), space.getModificationDate())
+    return response
+
+def listProjects(space, context):
+    projectSearchCriteria = ProjectSearchCriteria()
+    projectSearchCriteria.withSpace().withCode().thatEquals(space)
+    projects = context.getApi().searchProjects(context.getSessionToken(), projectSearchCriteria, ProjectFetchOptions()).getObjects()
+    response = context.createDirectoryResponse()
+    for project in projects:
+        response.addDirectory(project.getCode(), project.getModificationDate())
+    return response
+
+def listExperiments(space, project, context):
+    searchCriteria = ExperimentSearchCriteria()
+    searchCriteria.withProject().withCode().thatEquals(project)
+    searchCriteria.withProject().withSpace().withCode().thatEquals(space)
+    fetchOptions = ExperimentFetchOptions()
+    fetchOptions.withProperties()
+    experiments = context.getApi().searchExperiments(context.getSessionToken(), searchCriteria, fetchOptions).getObjects()
+    response = context.createDirectoryResponse()
+    for experiment in experiments:
+        nodeName = experiment.getProperties().get("$NAME")
+        if nodeName is None:
+            nodeName = experiment.getCode()
+        path = "%s/%s/%s" % (space, project, nodeName)
+        context.getCache().putNode(Node("EXP", experiment.getPermId().getPermId()), path)
+        response.addDirectory(nodeName, experiment.getModificationDate())
+    return response
+
+def listExperimentContent(path, context):
+    node = context.getCache().getNode(path)
+    experimentPermId = node.getPermId()
+    print("EXP:%s" % experimentPermId)
+    dataSetSearchCriteria = DataSetSearchCriteria()
+    dataSetSearchCriteria.withExperiment().withPermId().thatEquals(experimentPermId)
+    dataSets = context.getApi().searchDataSets(context.getSessionToken(), dataSetSearchCriteria, DataSetFetchOptions()).getObjects()
+    response = context.createDirectoryResponse()
+    for dataSet in dataSets:
+        response.addDirectory(dataSet.getCode(), dataSet.getModificationDate())
+
+    sampleSearchCriteria = SampleSearchCriteria()
+    sampleSearchCriteria.withExperiment().withPermId().thatEquals(experimentPermId)
+    samples = context.getApi().searchSamples(context.getSessionToken(), sampleSearchCriteria, SampleFetchOptions()).getObjects()
+    for sample in samples:
+        response.addDirectory(sample.getCode(), sample.getModificationDate())
+    return response
+    
\ No newline at end of file