diff --git a/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dssapi/v3/DataStoreServerApi.java b/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dssapi/v3/DataStoreServerApi.java
index 87d81735ed0e5a5312be38c4426e2d3c84fb77b1..b7fe35041edbcab9f98a982d470fb1d6c22fbc9d 100644
--- a/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dssapi/v3/DataStoreServerApi.java
+++ b/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dssapi/v3/DataStoreServerApi.java
@@ -29,6 +29,7 @@ import java.util.Map.Entry;
 import java.util.Properties;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.TreeSet;
 import java.util.stream.Collectors;
 
 import org.apache.commons.collections4.iterators.IteratorChain;
@@ -350,7 +351,7 @@ public class DataStoreServerApi extends AbstractDssServiceRpc<IDataStoreServerAp
             Set<String> ids = filesByDataSet.get(dataSetCode);
             if (ids == null)
             {
-                ids = new HashSet<>();
+                ids = new TreeSet<>();
                 filesByDataSet.put(dataSetCode, ids);
             }
             String filePath = filePermId.getFilePath();
diff --git a/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dssapi/v3/FileTransferServerServlet.java b/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dssapi/v3/FileTransferServerServlet.java
index 21bf52d3c696206e82413f7768e68d575a538994..e3b62b6ff710942cee817f5b6db766d664bb6ae0 100644
--- a/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dssapi/v3/FileTransferServerServlet.java
+++ b/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dssapi/v3/FileTransferServerServlet.java
@@ -85,14 +85,15 @@ import ch.systemsx.cisd.openbis.dss.generic.shared.api.internal.authorization.Ds
 
 /**
  * Servlet which provides download service of data set files using the file-transfer protocol.
+ * 
  * @author Franz-Josef Elmer
  */
 public class FileTransferServerServlet extends HttpServlet
 {
     private static final long serialVersionUID = 1L;
-    
+
     public static final String SERVLET_NAME = "file-transfer";
-    
+
     private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION,
             FileTransferServerServlet.class);
 
@@ -268,7 +269,7 @@ public class FileTransferServerServlet extends HttpServlet
     private DownloadSessionId getDownloadSessionId(Map<String, String[]> parameterMap) throws ServletException
     {
         DownloadSessionId downloadSessionId = new DownloadSessionId();
-        ClassUtils.setFieldValue(downloadSessionId, "id", getParameters(parameterMap, 
+        ClassUtils.setFieldValue(downloadSessionId, "id", getParameters(parameterMap,
                 FastDownloadParameter.DOWNLOAD_SESSION_ID_PARAMETER).get(0));
         return downloadSessionId;
     }
@@ -317,20 +318,24 @@ public class FileTransferServerServlet extends HttpServlet
         private ConcurrencyProvider(Properties properties)
         {
             maximumNumberOfAllowedStreams = PropertyUtils.getInt(properties, "api.v3.fast-download.maximum-number-of-allowed-streams", 10);
-            operationLog.info("max number of allowed streams: "+ maximumNumberOfAllowedStreams);
+            operationLog.info("max number of allowed streams: " + maximumNumberOfAllowedStreams);
         }
 
         @Override
         public int getAllowedNumberOfStreams(IUserSessionId userSessionId, Integer wishedNumberOfStreams, List<DownloadState> downloadStates)
                 throws DownloadException
         {
-            int allowedNumberOfStreams = maximumNumberOfAllowedStreams;
-            for (DownloadState downloadState : downloadStates)
+            int currentNumberOfStreams = downloadStates.stream().collect(Collectors.summingInt(DownloadState::getCurrentNumberOfStreams));
+            int freeNumberOfStreams = maximumNumberOfAllowedStreams - currentNumberOfStreams;
+            int allowedNumberOfStreams = freeNumberOfStreams / 2;
+            if (wishedNumberOfStreams != null && wishedNumberOfStreams < allowedNumberOfStreams)
             {
-                allowedNumberOfStreams -= downloadState.getCurrentNumberOfStreams();
+                allowedNumberOfStreams = wishedNumberOfStreams;
             }
-            System.err.println("allowed number of streams:"+allowedNumberOfStreams+" "+downloadStates.size());
-            return Math.min(wishedNumberOfStreams == null ? 1 : wishedNumberOfStreams, allowedNumberOfStreams);
+            operationLog.info("current number of streams: " + currentNumberOfStreams + ", wished number of streams: "
+                    + (wishedNumberOfStreams == null ? "unspecified" : wishedNumberOfStreams)
+                    + ", allowed number of streams: " + allowedNumberOfStreams);
+            return allowedNumberOfStreams;
         }
     }
 
diff --git a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/AbstractFileTest.java b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/AbstractFileTest.java
index 71466ad2ee7e78a15d3cb73edbeb3384a9a91e0a..0b2edd497ff56147115ae1b414eb35f7aaf44b27 100644
--- a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/AbstractFileTest.java
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/AbstractFileTest.java
@@ -115,7 +115,7 @@ public class AbstractFileTest extends SystemTestCase
     protected String createRandomContent(String path)
     {
         StringBuilder builder = new StringBuilder().append("file content of ").append(path).append("\n");
-        for (int i = 0; i < 10000; i++)
+        for (int i = 0; i < 30000; i++)
         {
             builder.append(UUID.nameUUIDFromBytes((path+ " "+i).getBytes()).toString()).append("\n");
         }
diff --git a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/DownloadFileTest.java b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/DownloadFileTest.java
index 63a8505ed1471a8c8cebcee881e8df1bbd966dde..544d3daaa7e4af0a870c0df6bab18d3d689dfec3 100644
--- a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/DownloadFileTest.java
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/DownloadFileTest.java
@@ -1,25 +1,51 @@
 package ch.ethz.sis.openbis.generic.dss.systemtest.api.v3;
 
+import java.io.File;
 import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
 
 import org.apache.commons.io.IOUtils;
 import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import ch.ethz.sis.filetransfer.DownloadException;
+import ch.ethz.sis.filetransfer.DownloadStatus;
+import ch.ethz.sis.filetransfer.IDownloadItemId;
+import ch.ethz.sis.filetransfer.IDownloadListener;
+import ch.ethz.sis.filetransfer.ILogger;
+import ch.ethz.sis.filetransfer.LogLevel;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSet;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.fetchoptions.DataSetFetchOptions;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.DataSetPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.IDataSetId;
 import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.download.DataSetFileDownload;
 import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.download.DataSetFileDownloadOptions;
 import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.download.DataSetFileDownloadReader;
+import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.fastdownload.FastDownloadSession;
+import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.fastdownload.FastDownloadSessionOptions;
 import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.id.DataSetFilePermId;
 import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.id.IDataSetFileId;
+import ch.ethz.sis.openbis.generic.dssapi.v3.fastdownload.FastDownloadResult;
+import ch.ethz.sis.openbis.generic.dssapi.v3.fastdownload.FastDownloader;
+import ch.systemsx.cisd.common.collection.SimpleComparator;
+import ch.systemsx.cisd.common.exceptions.ExceptionUtils;
+import ch.systemsx.cisd.common.filesystem.FileUtilities;
 
 public class DownloadFileTest extends AbstractFileTest
 {
 
+    private File target;
+
     @Override
     @BeforeClass
     protected void beforeClass() throws Exception
@@ -28,6 +54,152 @@ public class DownloadFileTest extends AbstractFileTest
         registerDataSet();
     }
 
+    @BeforeMethod
+    public void setUp()
+    {
+        target = new File(workingDirectory, "file-downloads");
+        FileUtilities.deleteRecursively(target);
+    }
+
+    @Test
+    public void testFastDownloadOfACompleteDataSet()
+    {
+        // Given
+        String sessionToken = as.login(TEST_USER, PASSWORD);
+        DataSetFilePermId root = new DataSetFilePermId(new DataSetPermId(dataSetCode), "");
+        List<DataSetFilePermId> fileIds = Arrays.asList(root);
+        FastDownloadSessionOptions options = new FastDownloadSessionOptions().withWishedNumberOfStreams(1);
+
+        // When
+        FastDownloadSession downloadSession = dss.createFastDownloadSession(sessionToken, fileIds, options);
+        FastDownloadResult downloadResult = new FastDownloader(downloadSession).downloadTo(target);
+
+        // Then
+        assertEquals(DownloadStatus.FINISHED, downloadResult.getStatus());
+        assertDownloads(sessionToken, downloadResult.getPathsById(), fileIds);
+    }
+
+    @Test
+    public void testFastDownloadOfAFileWithLogger()
+    {
+        // Given
+        String sessionToken = as.login(TEST_USER, PASSWORD);
+        DataSetFilePermId fileId = new DataSetFilePermId(new DataSetPermId(dataSetCode), getPath("subdir3/file6.txt"));
+        List<DataSetFilePermId> fileIds = Arrays.asList(fileId);
+        FastDownloadSessionOptions options = new FastDownloadSessionOptions().withWishedNumberOfStreams(1);
+        ILogger logger = new RecordingLogger();
+
+        // When
+        FastDownloadSession downloadSession = dss.createFastDownloadSession(sessionToken, fileIds, options);
+        FastDownloadResult downloadResult = new FastDownloader(downloadSession).withLogger(logger)
+                .downloadTo(target);
+
+        // Then
+        assertEquals(DownloadStatus.FINISHED, downloadResult.getStatus());
+        assertDownloads(sessionToken, downloadResult.getPathsById(), fileIds);
+        assertEquals("log: class ch.ethz.sis.filetransfer.DownloadClientDownload INFO Download state changed to: STARTED\n"
+                + "log: class ch.ethz.sis.filetransfer.DownloadClientDownload INFO Download state changed to: FINISHED\n",
+                logger.toString());
+    }
+
+    @Test
+    public void testFastDownloadAFileWithListener()
+    {
+        // Given
+        String sessionToken = as.login(TEST_USER, PASSWORD);
+        DataSetFilePermId file = new DataSetFilePermId(new DataSetPermId(dataSetCode), getPath("file1.txt"));
+        List<DataSetFilePermId> fileIds = Arrays.asList(file);
+        FastDownloadSessionOptions options = new FastDownloadSessionOptions().withWishedNumberOfStreams(1);
+        IDownloadListener listener = new RecordingListener();
+
+        // When
+        FastDownloadSession downloadSession = dss.createFastDownloadSession(sessionToken, fileIds, options);
+        FastDownloadResult downloadResult = new FastDownloader(downloadSession).withListener(listener).downloadTo(target);
+
+        // Then
+        assertEquals(DownloadStatus.FINISHED, downloadResult.getStatus());
+        assertDownloads(sessionToken, downloadResult.getPathsById(), fileIds);
+        String id1 = "DownloadItemId[id=" + dataSetCode + "/" + file.getFilePath() + "]";
+        String path1 = "targets/unit-test-wd/SystemTests/file-downloads/" + dataSetCode + "/" + file.getFilePath();
+        assertEquals("onDownloadStarted:\n"
+                + "onItemStarted: " + id1 + "\n"
+                + "onChunkDownloaded: 0\n"
+                + "onChunkDownloaded: 1\n"
+                + "onItemFinished: " + id1 + " " + path1 + "\n"
+                + "onDownloadFinished: [" + id1 + "=" + path1 + "]\n", listener.toString());
+    }
+
+    @Test
+    public void testFastDownloadAFolderWithListener()
+    {
+        // Given
+        String sessionToken = as.login(TEST_USER, PASSWORD);
+        DataSetFilePermId folder = new DataSetFilePermId(new DataSetPermId(dataSetCode), getPath("subdir1"));
+        List<DataSetFilePermId> fileIds = Arrays.asList(folder);
+        FastDownloadSessionOptions options = new FastDownloadSessionOptions().withWishedNumberOfStreams(1);
+        IDownloadListener listener = new RecordingListener();
+
+        // When
+        FastDownloadSession downloadSession = dss.createFastDownloadSession(sessionToken, fileIds, options);
+        FastDownloadResult downloadResult = new FastDownloader(downloadSession).withListener(listener).downloadTo(target);
+
+        // Then
+        assertEquals(DownloadStatus.FINISHED, downloadResult.getStatus());
+        assertDownloads(sessionToken, downloadResult.getPathsById(), fileIds);
+        String id2 = "DownloadItemId[id=" + dataSetCode + "/" + folder.getFilePath() + "]";
+        String path2 = "targets/unit-test-wd/SystemTests/file-downloads/" + dataSetCode + "/" + folder.getFilePath();
+        assertEquals("onDownloadStarted:\n"
+                + "onItemStarted: " + id2 + "\n"
+                + "onChunkDownloaded: 0\n"
+                + "onChunkDownloaded: 1\n"
+                + "onChunkDownloaded: 2\n"
+                + "onChunkDownloaded: 3\n"
+                + "onChunkDownloaded: 4\n"
+                + "onChunkDownloaded: 5\n"
+                + "onItemFinished: " + id2 + " " + path2 + "\n"
+                + "onDownloadFinished: [" + id2 + "=" + path2 + "]\n", listener.toString());
+    }
+
+    @Test
+    public void testFastDownloadUnauthorized()
+    {
+        // Given
+        String sessionToken = as.login(TEST_SPACE_USER, PASSWORD);
+        DataSetFilePermId folder = new DataSetFilePermId(new DataSetPermId(dataSetCode), getPath("subdir1"));
+        List<DataSetFilePermId> fileIds = Arrays.asList(folder);
+        FastDownloadSessionOptions options = new FastDownloadSessionOptions().withWishedNumberOfStreams(1);
+
+        // When
+        FastDownloadSession downloadSession = dss.createFastDownloadSession(sessionToken, fileIds, options);
+
+        // Then
+        assertEquals("[]", downloadSession.getFiles().toString());
+    }
+
+    @Test
+    public void testFastDownloadUnauthorizedByCheating()
+    {
+        // Given
+        String sessionToken = as.login(TEST_SPACE_USER, PASSWORD);
+        DataSetFilePermId folder = new DataSetFilePermId(new DataSetPermId(dataSetCode), getPath("subdir1"));
+        List<DataSetFilePermId> fileIds = Arrays.asList(folder);
+        FastDownloadSessionOptions options = new FastDownloadSessionOptions().withWishedNumberOfStreams(1);
+        FastDownloadSession downloadSession = dss.createFastDownloadSession(sessionToken, fileIds, options);
+        assertEquals(0, downloadSession.getFiles().size());
+        downloadSession.getFiles().add(folder);
+
+        try
+        {
+            // When
+            new FastDownloader(downloadSession).downloadTo(target);
+            fail("DownloadException expected");
+        } catch (DownloadException e)
+        {
+            // Then
+            assertEquals("java.lang.IllegalArgumentException: Item ids cannot be null or empty", ExceptionUtils.getEndOfChain(e).getMessage());
+        }
+    }
+
     @Test
     public void testDownloadUnauthorized()
     {
@@ -222,4 +394,160 @@ public class DownloadFileTest extends AbstractFileTest
     {
         return "original/" + dataSetCode + "/";
     }
+
+    private void assertDownloads(String sessionToken, Map<IDataSetFileId, Path> pathsById, List<DataSetFilePermId> fileIds)
+    {
+        System.out.println("PATHS BY ID:" + pathsById);
+        List<IDataSetId> dataSetIds = new ArrayList<>(fileIds.stream().map(DataSetFilePermId::getDataSetId).collect(Collectors.toSet()));
+        DataSetFetchOptions fetchOptions = new DataSetFetchOptions();
+        fetchOptions.withPhysicalData();
+        Map<IDataSetId, DataSet> dataSets = as.getDataSets(sessionToken, dataSetIds, fetchOptions);
+        for (DataSetFilePermId fileId : fileIds)
+        {
+            String location = dataSets.get(fileId.getDataSetId()).getPhysicalData().getLocation();
+            File expectedFile = new File(store, "1/" + location + "/" + fileId.getFilePath());
+            Path path = pathsById.get(fileId);
+            assertNotNull("Path for file " + fileId, path);
+            File actualFile = path.toFile();
+            assertSameContent(expectedFile, actualFile);
+        }
+        assertEquals(fileIds.size(), pathsById.size());
+    }
+
+    private void assertSameContent(File expectedFile, File actualFile)
+    {
+        assertEquals(expectedFile.getName(), actualFile.getName());
+        assertEquals(actualFile + " exists", expectedFile.exists(), actualFile.exists());
+        assertEquals(actualFile + " is directory", expectedFile.isDirectory(), actualFile.isDirectory());
+        if (actualFile.isDirectory())
+        {
+            List<File> expectedChildren = Arrays.asList(expectedFile.listFiles());
+            Collections.sort(expectedChildren);
+            List<String> expectedChildrenNames = expectedChildren.stream().map(File::getName).collect(Collectors.toList());
+            List<File> actualChildren = Arrays.asList(actualFile.listFiles());
+            Collections.sort(actualChildren);
+            List<String> actualChildrenNames = actualChildren.stream().map(File::getName).collect(Collectors.toList());
+            assertEquals(expectedChildrenNames, actualChildrenNames);
+            for (int i = 0, n = expectedChildren.size(); i < n; i++)
+            {
+                assertSameContent(expectedChildren.get(i), actualChildren.get(i));
+            }
+        } else
+        {
+            assertEquals(FileUtilities.loadToString(expectedFile), FileUtilities.loadToString(actualFile));
+        }
+    }
+
+    private static final class RecordingLogger implements ILogger
+    {
+        private List<Event> events = new ArrayList<>();
+
+        @Override
+        public boolean isEnabled(LogLevel level)
+        {
+            return LogLevel.INFO.compareTo(level) <= 0;
+        }
+
+        @Override
+        public void log(Class<?> clazz, LogLevel level, String message)
+        {
+            events.add(new Event("log", clazz, level, message));
+        }
+
+        @Override
+        public void log(Class<?> clazz, LogLevel level, String message, Throwable throwable)
+        {
+            events.add(new Event("logWithThrowable", clazz, level, message, throwable));
+        }
+
+        @Override
+        public String toString()
+        {
+            return render(events);
+        }
+    }
+
+    private static final class RecordingListener implements IDownloadListener
+    {
+        private List<Event> events = new ArrayList<>();
+
+        @Override
+        public void onDownloadStarted()
+        {
+            events.add(new Event("onDownloadStarted"));
+        }
+
+        @Override
+        public void onDownloadFinished(Map<IDownloadItemId, Path> itemPaths)
+        {
+            List<Entry<IDownloadItemId, Path>> list = new ArrayList<>(itemPaths.entrySet());
+            Collections.sort(list, new SimpleComparator<Entry<IDownloadItemId, Path>, String>()
+                {
+                    @Override
+                    public String evaluate(Entry<IDownloadItemId, Path> item)
+                    {
+                        return item.getKey().getId();
+                    }
+                });
+            events.add(new Event("onDownloadFinished", list));
+        }
+
+        @Override
+        public void onDownloadFailed(Collection<Exception> e)
+        {
+            events.add(new Event("onDownloadFailed", e));
+        }
+
+        @Override
+        public void onItemStarted(IDownloadItemId itemId)
+        {
+            events.add(new Event("onItemStarted", itemId));
+        }
+
+        @Override
+        public void onItemFinished(IDownloadItemId itemId, Path itemPath)
+        {
+            events.add(new Event("onItemFinished", itemId, itemPath));
+        }
+
+        @Override
+        public void onChunkDownloaded(int sequenceNumber)
+        {
+            events.add(new Event("onChunkDownloaded", sequenceNumber));
+        }
+
+        @Override
+        public String toString()
+        {
+            return render(events);
+        }
+    }
+
+    private static final class Event
+    {
+        private String type;
+
+        private Object[] parameters;
+
+        Event(String type, Object... parameters)
+        {
+            this.type = type;
+            this.parameters = parameters;
+        }
+    }
+
+    private static final String render(List<Event> events)
+    {
+        StringBuilder builder = new StringBuilder();
+        for (Event event : events)
+        {
+            builder.append(event.type).append(":");
+            for (Object parameter : event.parameters)
+            {
+                builder.append(" ").append(parameter);
+            }
+            builder.append("\n");
+        }
+        return builder.toString();
+    }
 }
\ No newline at end of file
diff --git a/openbis_api/source/java/ch/ethz/sis/openbis/generic/dssapi/v3/fastdownload/FastDownloader.java b/openbis_api/source/java/ch/ethz/sis/openbis/generic/dssapi/v3/fastdownload/FastDownloader.java
index 838cfcb33390e8c19f90ec9dd58baccdb1cf9ec4..52ad17d22d7fa9e766a9c672440c17a3a5b814e3 100644
--- a/openbis_api/source/java/ch/ethz/sis/openbis/generic/dssapi/v3/fastdownload/FastDownloader.java
+++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/dssapi/v3/fastdownload/FastDownloader.java
@@ -30,6 +30,7 @@ import ch.ethz.sis.filetransfer.DownloadClientConfig;
 import ch.ethz.sis.filetransfer.DownloadClientDownload;
 import ch.ethz.sis.filetransfer.DownloadException;
 import ch.ethz.sis.filetransfer.DownloadItemId;
+import ch.ethz.sis.filetransfer.DownloadItemNotFoundException;
 import ch.ethz.sis.filetransfer.DownloadPreferences;
 import ch.ethz.sis.filetransfer.DownloadSessionId;
 import ch.ethz.sis.filetransfer.FileSystemDownloadStore;
@@ -37,8 +38,6 @@ import ch.ethz.sis.filetransfer.IDownloadItemId;
 import ch.ethz.sis.filetransfer.IDownloadItemIdDeserializer;
 import ch.ethz.sis.filetransfer.IDownloadListener;
 import ch.ethz.sis.filetransfer.IDownloadServer;
-import ch.ethz.sis.filetransfer.IDownloadStore;
-import ch.ethz.sis.filetransfer.IDownloadStoreFactory;
 import ch.ethz.sis.filetransfer.ILogger;
 import ch.ethz.sis.filetransfer.IRetryProvider;
 import ch.ethz.sis.filetransfer.IRetryProviderFactory;
@@ -77,8 +76,6 @@ public class FastDownloader
 
     private List<IDownloadListener> listeners = new ArrayList<>();
 
-    private IDownloadStoreFactory storeFactory;
-
     private IRetryProviderFactory retryProviderFactory;
 
     private UserSessionId userSessionId;
@@ -157,18 +154,6 @@ public class FastDownloader
         return this;
     }
 
-    /**
-     * Sets the factory for an {@link IDownloadStore}. By default the downloaded files are stored in the root folder (parameter of method
-     * {{@link #downloadTo(File)}) as follows: <data set code>/<file path in the data set>
-     * 
-     * @return this instance
-     */
-    public FastDownloader withDownloadStoreFactory(IDownloadStoreFactory storeFactory)
-    {
-        this.storeFactory = storeFactory;
-        return this;
-    }
-
     /**
      * Sets the factory for an {@link IRetryProvider}. The default is {@link DefaultRetryProvider} with maximumNumberOfRetries = 3,
      * waitingTimeBetweenRetries = 1 sec, waitingTimeBetweenRetriesIncreasingFactor = 2.
@@ -207,18 +192,31 @@ public class FastDownloader
         DownloadClientConfig config = new DownloadClientConfig();
         config.setServer(getDownloadServer());
         config.setLogger(actualLogger);
-        config.setStore(storeFactory != null ? storeFactory.createStore(actualLogger, root)
-                : new FileSystemDownloadStore(actualLogger, root)
+        config.setStore(new FileSystemDownloadStore(actualLogger, root)
+            {
+                @Override
+                protected Path getItemDirectory(IUserSessionId userSessionId, DownloadSessionId downloadSessionId,
+                        IDownloadItemId itemId) throws DownloadException
+                {
+                    String[] splitted = itemId.getId().split("/", 2);
+                    String dataSetCode = splitted[0];
+                    return root.resolve(dataSetCode);
+                }
+
+                @Override
+                public Path getItemPath(IUserSessionId userSessionId, DownloadSessionId downloadSessionId, IDownloadItemId itemId)
+                        throws DownloadException
+                {
+                    Path itemDirectory = getItemDirectory(userSessionId, downloadSessionId, itemId);
+                    String[] splitted = itemId.getId().split("/", 2);
+                    Path itemPath = itemDirectory.resolve(splitted[1]);
+                    if (itemPath.toFile().exists())
                     {
-                        @Override
-                        protected Path getItemDirectory(IUserSessionId userSessionId, DownloadSessionId downloadSessionId,
-                                IDownloadItemId itemId) throws DownloadException
-                        {
-                            String[] splitted = itemId.getId().split("/", 2);
-                            String dataSetCode = splitted[0];
-                            return root.resolve(dataSetCode);
-                        }
-                    });
+                        return itemPath;
+                    }
+                    throw new DownloadItemNotFoundException("Store does not contain any files for download item id: " + itemId);
+                }
+            });
         config.setDeserializerProvider(new DefaultDeserializerProvider(actualLogger,
                 new IDownloadItemIdDeserializer()
                     {