diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3ExperimentLevelResolver.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3ExperimentLevelResolver.java
index 005b2759cfe189dcbadaf2dd7695b104e056e704..c3011c0bd0a63c754411308ab18362fe0c0044d8 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3ExperimentLevelResolver.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3ExperimentLevelResolver.java
@@ -18,12 +18,8 @@ package ch.systemsx.cisd.openbis.dss.generic.server.ftp.v3;
 
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
 import java.util.Map;
 
-import org.apache.ftpserver.ftplet.FtpFile;
-
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSet;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.Experiment;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.fetchoptions.ExperimentFetchOptions;
@@ -42,7 +38,7 @@ class V3ExperimentLevelResolver extends V3Resolver
     }
 
     @Override
-    public FtpFile resolve(String fullPath, String[] subPath)
+    public V3FtpFile resolve(String fullPath, String[] subPath)
     {
         if (subPath.length == 0)
         {
@@ -52,12 +48,12 @@ class V3ExperimentLevelResolver extends V3Resolver
             Map<IExperimentId, Experiment> experiments = api.getExperiments(sessionToken, Collections.singletonList(experimentId), fetchOptions);
             Experiment exp = experiments.get(experimentId);
 
-            List<FtpFile> files = new LinkedList<>();
+            V3FtpDirectoryResponse response = new V3FtpDirectoryResponse(fullPath);
             for (DataSet dataSet : exp.getDataSets())
             {
-                files.add(createDirectoryScaffolding(fullPath, dataSet.getCode()));
+                response.AddDirectory(dataSet.getCode());
             }
-            return createDirectoryWithContent(fullPath, files);
+            return response;
         } else
         {
             String dataSetCode = subPath[0];
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3HierarchicalContentResolver.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3HierarchicalContentResolver.java
index 4a1518db664fd433d4eeaa03adc04f4bc607a06f..a5e265690142931cbb79ab2235db7685c12d8db2 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3HierarchicalContentResolver.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3HierarchicalContentResolver.java
@@ -16,11 +16,6 @@
 
 package ch.systemsx.cisd.openbis.dss.generic.server.ftp.v3;
 
-import java.util.LinkedList;
-import java.util.List;
-
-import org.apache.ftpserver.ftplet.FtpFile;
-
 import ch.systemsx.cisd.common.shared.basic.string.StringUtils;
 import ch.systemsx.cisd.openbis.common.io.hierarchical_content.api.IHierarchicalContent;
 import ch.systemsx.cisd.openbis.common.io.hierarchical_content.api.IHierarchicalContentNode;
@@ -38,7 +33,7 @@ class V3HierarchicalContentResolver extends V3Resolver
     }
 
     @Override
-    public FtpFile resolve(String fullPath, String[] subPath)
+    public V3FtpFile resolve(String fullPath, String[] subPath)
     {
         IHierarchicalContentNode rootNode;
         if (subPath.length == 0)
@@ -51,21 +46,21 @@ class V3HierarchicalContentResolver extends V3Resolver
 
         if (false == rootNode.isDirectory())
         {
-            return createFileWithContent(fullPath, rootNode);
+            return new V3FtpFileResponse(fullPath, rootNode);
         }
 
-        List<FtpFile> files = new LinkedList<>();
+        V3FtpDirectoryResponse response = new V3FtpDirectoryResponse(fullPath);
         for (IHierarchicalContentNode node : rootNode.getChildNodes())
         {
             if (node.isDirectory())
             {
-                files.add(createDirectoryScaffolding(fullPath, node.getName()));
+                response.AddDirectory(node.getName());
             } else
             {
-                files.add(createFileScaffolding(fullPath, node.getName(), node));
+                response.AddFile(node.getName(), node);
             }
         }
-        return createDirectoryWithContent(fullPath, files);
+        return response;
     }
 
 }
\ No newline at end of file
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3ProjectLevelResolver.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3ProjectLevelResolver.java
index 0b28a49c0c3ddcddceb5212a1c3f253ff3b79398..58d94bc9f13111d2a7addcad422e193b0201e16f 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3ProjectLevelResolver.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3ProjectLevelResolver.java
@@ -18,12 +18,8 @@ package ch.systemsx.cisd.openbis.dss.generic.server.ftp.v3;
 
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
 import java.util.Map;
 
-import org.apache.ftpserver.ftplet.FtpFile;
-
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.Experiment;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.ExperimentIdentifier;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.project.Project;
@@ -43,7 +39,7 @@ class V3ProjectLevelResolver extends V3Resolver
     }
 
     @Override
-    public FtpFile resolve(String fullPath, String[] subPath)
+    public V3FtpFile resolve(String fullPath, String[] subPath)
     {
         if (subPath.length == 0)
         {
@@ -53,12 +49,12 @@ class V3ProjectLevelResolver extends V3Resolver
             Map<IProjectId, Project> projects = api.getProjects(sessionToken, Collections.singletonList(projectIdentifier), fetchOptions);
             Project project = projects.get(projectIdentifier);
 
-            List<FtpFile> files = new LinkedList<>();
+            V3FtpDirectoryResponse response = new V3FtpDirectoryResponse(fullPath);
             for (Experiment exp : project.getExperiments())
             {
-                files.add(createDirectoryScaffolding(fullPath, exp.getCode()));
+                response.AddDirectory(exp.getCode());
             }
-            return createDirectoryWithContent(fullPath, files);
+            return response;
         } else
         {
             String item = subPath[0];
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3Resolver.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3Resolver.java
index 9cdb66842ad4c5c990c776537771bb7b26e90ab8..f824a78cbc10d66fb9c75e55952e0a7a8c8d9a50 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3Resolver.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3Resolver.java
@@ -18,6 +18,7 @@ package ch.systemsx.cisd.openbis.dss.generic.server.ftp.v3;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.ArrayList;
 import java.util.List;
 
 import org.apache.ftpserver.ftplet.FtpFile;
@@ -44,112 +45,128 @@ public abstract class V3Resolver
         this.api = resolverContext.getV3Api();
     }
 
-    public abstract FtpFile resolve(String fullPath, String[] subPath);
-
-    public static FtpFile createDirectoryScaffolding(String parentPath, String name)
-    {
-        return new AbstractFtpFolder(parentPath + "/" + name)
-            {
-                @Override
-                public List<FtpFile> unsafeListFiles() throws RuntimeException
-                {
-                    throw new IllegalStateException("Don't expect to sak for file listing of scaffolding directory");
-                }
-            };
-    }
+    /**
+     * Create a ftp file which has specified full path, resolving the local path specified as an array of path items.
+     */
+    public abstract V3FtpFile resolve(String fullPath, String[] pathItems);
 
-    public static FtpFile createDirectoryWithContent(String name, final List<FtpFile> files)
+    public interface V3FtpFile extends FtpFile
     {
-        return new AbstractFtpFolder(name)
-            {
-
-                @Override
-                public List<FtpFile> unsafeListFiles() throws RuntimeException
-                {
-                    return files;
-                }
 
-            };
     }
 
-    protected FtpFile createFileWithContent(String name, final IHierarchicalContentNode node)
+    public class V3FtpDirectoryResponse extends AbstractFtpFolder implements V3FtpFile
     {
-        return new AbstractFtpFile(name)
-            {
-
-                @Override
-                public boolean isFile()
-                {
-                    return true;
-                }
-
-                @Override
-                public boolean isDirectory()
+        private final List<FtpFile> files;
+
+        public V3FtpDirectoryResponse(String fullPath)
+        {
+            super(fullPath);
+            this.files = new ArrayList<>();
+        }
+
+        @Override
+        public List<FtpFile> unsafeListFiles() throws RuntimeException
+        {
+            return files;
+        }
+
+        public void AddDirectory(String directoryName)
+        {
+            files.add(new AbstractFtpFolder(getAbsolutePath() + "/" + directoryName)
                 {
-                    return false;
-                }
-
-                @Override
-                public long getSize()
-                {
-                    throw new IllegalStateException("Size is not required for content node");
-                }
-
-                @Override
-                public InputStream createInputStream(long offset) throws IOException
+                    @Override
+                    public List<FtpFile> unsafeListFiles() throws RuntimeException
+                    {
+                        throw new IllegalStateException("Don't expect to sak for file listing of scaffolding directory");
+                    }
+                });
+        }
+
+        public void AddFile(String fileName, final IHierarchicalContentNode node)
+        {
+            AbstractFtpFile file = new AbstractFtpFile(this.absolutePath + "/" + fileName)
                 {
-                    return node.getInputStream();
-                }
 
-                @Override
-                public List<FtpFile> unsafeListFiles() throws RuntimeException
-                {
-                    throw new IllegalStateException("Don't expect to sak for file listing of file");
-                }
-            };
+                    @Override
+                    public boolean isFile()
+                    {
+                        return true;
+                    }
+
+                    @Override
+                    public boolean isDirectory()
+                    {
+                        return false;
+                    }
+
+                    @Override
+                    public long getSize()
+                    {
+                        return node.getFileLength();
+                    }
+
+                    @Override
+                    public long getLastModified()
+                    {
+                        return node.getLastModified();
+                    }
+
+                    @Override
+                    public InputStream createInputStream(long offset) throws IOException
+                    {
+                        throw new IllegalStateException("Don't expect to ask for input stream of scaffolding file");
+                    }
+
+                    @Override
+                    public List<FtpFile> unsafeListFiles() throws RuntimeException
+                    {
+                        throw new IllegalStateException("Don't expect to ask for file listing of scaffolding file");
+                    }
+                };
+            files.add(file);
+        }
     }
 
-    protected FtpFile createFileScaffolding(String parentPath, String name, final IHierarchicalContentNode node)
+    public class V3FtpFileResponse extends AbstractFtpFile implements V3FtpFile
     {
-        return new AbstractFtpFile(parentPath + "/" + name)
-            {
-
-                @Override
-                public boolean isFile()
-                {
-                    return true;
-                }
-
-                @Override
-                public boolean isDirectory()
-                {
-                    return false;
-                }
-
-                @Override
-                public long getSize()
-                {
-                    return node.getFileLength();
-                }
-
-                @Override
-                public long getLastModified()
-                {
-                    return node.getLastModified();
-                }
-
-                @Override
-                public InputStream createInputStream(long offset) throws IOException
-                {
-                    throw new IllegalStateException("Don't expect to ask for input stream of scaffolding file");
-                }
-
-                @Override
-                public List<FtpFile> unsafeListFiles() throws RuntimeException
-                {
-                    throw new IllegalStateException("Don't expect to ask for file listing of scaffolding file");
-                }
-            };
+        private IHierarchicalContentNode node;
+
+        public V3FtpFileResponse(String fullPath, final IHierarchicalContentNode node)
+        {
+            super(fullPath);
+            this.node = node;
+        }
+
+        @Override
+        public boolean isFile()
+        {
+            return true;
+        }
+
+        @Override
+        public boolean isDirectory()
+        {
+            return false;
+        }
+
+        @Override
+        public long getSize()
+        {
+            return node.getFileLength();
+        }
+
+        @Override
+        public InputStream createInputStream(long offset) throws IOException
+        {
+            return node.getInputStream();
+        }
+
+        @Override
+        public List<FtpFile> unsafeListFiles() throws RuntimeException
+        {
+            throw new IllegalStateException("Don't expect to sak for file listing of file");
+        }
     }
 
     /**
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3RootLevelResolver.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3RootLevelResolver.java
index 8e609432608456a438850f88f8735ea75eb39587..8540304568c7da4870edc8e955b214bae437073c 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3RootLevelResolver.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3RootLevelResolver.java
@@ -17,11 +17,8 @@
 package ch.systemsx.cisd.openbis.dss.generic.server.ftp.v3;
 
 import java.util.Arrays;
-import java.util.LinkedList;
 import java.util.List;
 
-import org.apache.ftpserver.ftplet.FtpFile;
-
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.Space;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.fetchoptions.SpaceFetchOptions;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.search.SpaceSearchCriteria;
@@ -35,18 +32,19 @@ class V3RootLevelResolver extends V3Resolver
     }
 
     @Override
-    public FtpFile resolve(String fullPath, String[] subPath)
+    public V3FtpFile resolve(String fullPath, String[] subPath)
     {
         if (subPath.length == 0)
         {
             List<Space> spaces =
                     api.searchSpaces(sessionToken, new SpaceSearchCriteria(), new SpaceFetchOptions()).getObjects();
-            List<FtpFile> files = new LinkedList<>();
+
+            V3FtpDirectoryResponse response = new V3FtpDirectoryResponse(fullPath);
             for (Space space : spaces)
             {
-                files.add(createDirectoryScaffolding(fullPath, space.getCode()));
+                response.AddDirectory(space.getCode());
             }
-            return createDirectoryWithContent(fullPath, files);
+            return response;
         } else
         {
             String item = subPath[0];
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3SpaceLevelResolver.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3SpaceLevelResolver.java
index cda8e3bc9d4eb306c32f633a03618064d539f0df..0637ffefb5422abbf07fdc0d05654c417989d067 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3SpaceLevelResolver.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/v3/V3SpaceLevelResolver.java
@@ -17,11 +17,8 @@
 package ch.systemsx.cisd.openbis.dss.generic.server.ftp.v3;
 
 import java.util.Arrays;
-import java.util.LinkedList;
 import java.util.List;
 
-import org.apache.ftpserver.ftplet.FtpFile;
-
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.project.Project;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.project.fetchoptions.ProjectFetchOptions;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.project.search.ProjectSearchCriteria;
@@ -38,7 +35,7 @@ class V3SpaceLevelResolver extends V3Resolver
     }
 
     @Override
-    public FtpFile resolve(String fullPath, String[] subPath)
+    public V3FtpFile resolve(String fullPath, String[] subPath)
     {
         if (subPath.length == 0)
         {
@@ -47,12 +44,13 @@ class V3SpaceLevelResolver extends V3Resolver
             ProjectFetchOptions fetchOptions = new ProjectFetchOptions();
             List<Project> projects =
                     api.searchProjects(sessionToken, searchCriteria, fetchOptions).getObjects();
-            List<FtpFile> files = new LinkedList<>();
+
+            V3FtpDirectoryResponse response = new V3FtpDirectoryResponse(fullPath);
             for (Project project : projects)
             {
-                files.add(V3Resolver.createDirectoryScaffolding(fullPath, project.getCode()));
+                response.AddDirectory(project.getCode());
             }
-            return V3Resolver.createDirectoryWithContent(fullPath, files);
+            return response;
         } else
         {
             String item = subPath[0];