diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/SessionWorkspaceFileUploadServlet.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/SessionWorkspaceFileUploadServlet.java
index 36f175546bfbec7cf65740711f837aa2c0f1b4fc..87ded2540f8fbf2c8e930dd09546b7e2373152d3 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/SessionWorkspaceFileUploadServlet.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/SessionWorkspaceFileUploadServlet.java
@@ -63,7 +63,10 @@ public class SessionWorkspaceFileUploadServlet extends HttpServlet
 
     private static final String STATUS_PARAM = "status";
 
-    public static final String UPLOAD_ID_PARAM = "uploadID";
+    /** If present, the file being uploaded is just an empty folder, not a file upload. */
+    private static final String IS_EMPTY_FOLDER = "emptyFolder";
+
+    private static final String UPLOAD_ID_PARAM = "uploadID";
 
     private IDssServiceRpcGeneric service;
 
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/OpenBisFacade.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/OpenBisFacade.java
deleted file mode 100644
index b49e16e60483b5ca8fc32af3d5d054887e777b35..0000000000000000000000000000000000000000
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/OpenBisFacade.java
+++ /dev/null
@@ -1,123 +0,0 @@
-package ch.ethz.sis.openbis.generic.server.asapi.v3;
-
-import java.io.File;
-import java.net.URI;
-import java.net.URLEncoder;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.time.Duration;
-import java.util.Map;
-
-import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.DataSetPermId;
-import ch.ethz.sis.openbis.generic.dssapi.v3.dto.dataset.create.UploadedDataSetCreation;
-import ch.systemsx.cisd.common.spring.HttpInvokerUtils;
-import ch.systemsx.cisd.openbis.generic.client.web.client.exception.UserFailureException;
-
-public class OpenBisFacade
-{
-
-    public static final int TIMEOUT_IN_MILLIS = 30000;
-
-    public static String uploadFileWorkspaceDss(final File[] filesOrFolders)
-    {
-//        final ServiceFinder serviceFinder = new ServiceFinder("openbis", "session_workspace_file_upload");
-//        serviceFinder.computeServerUrl()
-        final IApplicationServerApi v3 = HttpInvokerUtils.createServiceStub(IApplicationServerApi.class,
-                "http://localhost:8888/openbis/openbis" + IApplicationServerApi.SERVICE_URL, 10000);
-
-        final String sessionToken = v3.login("admin", "cahngeit");
-        System.out.println("Session token: " + sessionToken);
-
-        try
-        {
-            final File file = filesOrFolders[0];
-
-            if (!file.isFile())
-            {
-                throw new UserFailureException("File must me a file for now.");
-            }
-
-            final String fileName = file.getName();
-
-            final byte[] respose = request("POST", new URI("http://localhost:8889/datastore_server/session_workspace_file_upload"),
-                    Map.of(
-                            "sessionID", sessionToken,
-                            "filename", fileName,
-                            "id", "0",
-                            "startByte", "0",
-                            "endByte", String.valueOf(file.length()),
-                            "size", String.valueOf(file.length())
-                            ), Files.readAllBytes(file.toPath()));
-            System.out.println(new String(respose));
-        } catch (Exception e)
-        {
-            throw new RuntimeException(e);
-        } finally
-        {
-            v3.logout(sessionToken);
-        }
-
-        // TODO: implement.
-        return null;
-    }
-
-    public static DataSetPermId createUploadedDataSet(final String sessionToken, final UploadedDataSetCreation creation)
-    {
-        // TODO: implement.
-        return null;
-    }
-
-    @SuppressWarnings({ "OptionalGetWithoutIsPresent", "unchecked" })
-    private static byte[] request(final String httpMethod, final URI serverUri,
-            final Map<String, String> parameters, final byte [] body) throws Exception {
-        HttpClient client = HttpClient.newBuilder()
-                .version(HttpClient.Version.HTTP_1_1)
-                .followRedirects(HttpClient.Redirect.NORMAL)
-                .connectTimeout(Duration.ofMillis(TIMEOUT_IN_MILLIS))
-                .build();
-
-        final String query = parameters.entrySet().stream()
-                .map(entry -> urlEncode(entry.getKey()) + "=" + urlEncode(entry.getValue()))
-                .reduce((s1, s2) -> s1 + "&" + s2).get();
-
-        final URI uri = new URI(serverUri.getScheme(), null, serverUri.getHost(), serverUri.getPort(),
-                serverUri.getPath(), query, null);
-
-        final HttpRequest.Builder builder = HttpRequest.newBuilder()
-                .uri(uri)
-                .version(HttpClient.Version.HTTP_1_1)
-                .timeout(Duration.ofMillis(TIMEOUT_IN_MILLIS))
-                .method(httpMethod, HttpRequest.BodyPublishers.ofByteArray(body));
-
-        final HttpRequest request = builder.build();
-
-        final HttpResponse<byte[]> httpResponse = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
-
-        final int statusCode = httpResponse.statusCode();
-        if (statusCode >= 200 && statusCode < 300) {
-            return httpResponse.body();
-        } else if (statusCode >= 400 && statusCode < 500) {
-            throw new UserFailureException("User failure. Received status code: " + statusCode + ". Body: " +
-                    new String(httpResponse.body()));
-        } else if (statusCode >= 500 && statusCode < 600) {
-            throw new RuntimeException("Server failure. Received status code: " + statusCode);
-        } else {
-            throw new RuntimeException("Unknown failure. Received status code: " + statusCode);
-        }
-    }
-
-    private static String urlEncode(final String s) {
-        return URLEncoder.encode(s, StandardCharsets.UTF_8);
-    }
-
-    public static void main(String[] args)
-    {
-        final File file = new File("/home/viktor/Work/Projects/ETH/openbis/settings.gradle");
-        uploadFileWorkspaceDss(new File[]{file});
-    }
-
-}
diff --git a/openbis_api/source/java/ch/ethz/sis/openbis/generic/OpenBISAPI.java b/openbis_api/source/java/ch/ethz/sis/openbis/generic/OpenBISAPI.java
index 70987290eef62e1056ad40d1d605f78439a125eb..5fadcf3663b5da19305ad71c941a4ace3eb52de1 100644
--- a/openbis_api/source/java/ch/ethz/sis/openbis/generic/OpenBISAPI.java
+++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/OpenBISAPI.java
@@ -4,38 +4,149 @@ import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.DataSetPermId;
 import ch.ethz.sis.openbis.generic.dssapi.v3.IDataStoreServerApi;
 import ch.ethz.sis.openbis.generic.dssapi.v3.dto.dataset.create.UploadedDataSetCreation;
+import ch.systemsx.cisd.common.exceptions.UserFailureException;
 import ch.systemsx.cisd.common.spring.HttpInvokerUtils;
 
+import java.io.File;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Map;
+import java.util.Objects;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
 
 public class OpenBISAPI {
+
     private static final int DEFAULT_TIMEOUT_IN_MILLIS = 30000; //30 seconds
 
+    private static final int CHUNK_SIZE = 1048576;
+
     private final IApplicationServerApi asFacade;
+
     private final IDataStoreServerApi dssFacade;
+
     private String sessionToken;
 
-    public OpenBISAPI(String asURL, String dssURL, int timeout) {
-        asFacade = HttpInvokerUtils.createServiceStub(IApplicationServerApi.class, asURL, timeout);
-        dssFacade = HttpInvokerUtils.createServiceStub(IDataStoreServerApi.class, dssURL, timeout);
+    private final int timeout;
+
+    private final String asURL;
+
+    private final String dssURL;
+
+    public OpenBISAPI(final String asURL, final String dssURL) {
+        this(asURL, dssURL, DEFAULT_TIMEOUT_IN_MILLIS);
+    }
+
+    public OpenBISAPI(final String asURL, final String dssURL, final int timeout) {
+        this.timeout = timeout;
+        this.asURL = asURL;
+        asFacade = HttpInvokerUtils.createServiceStub(IApplicationServerApi.class, this.asURL, timeout);
+        this.dssURL = dssURL;
+        dssFacade = HttpInvokerUtils.createServiceStub(IDataStoreServerApi.class, this.dssURL, timeout);
     }
 
     public String getSessionToken() {
         return sessionToken;
     }
 
-    public void setSessionToken(String sessionToken) {
+    public void setSessionToken(final String sessionToken) {
         this.sessionToken = sessionToken;
     }
 
-    public String uploadFileWorkspaceDSS(Path fileOrFolder) {
-        // Upload file or folder to the DSS SessionWorkspaceFileUploadServlet and return the ID to be used by createUploadedDataSet
-        // This method hides the complexities of uploading a folder with many files and does the uploads in chunks.
-        return null;
+    /**
+     * Upload file or folder to the DSS SessionWorkspaceFileUploadServlet and return the ID to be used by createUploadedDataSet
+     * This method hides the complexities of uploading a folder with many files and does the uploads in chunks.
+     */
+    public String uploadFileWorkspaceDSS(final Path fileOrFolder) {
+        //        final ServiceFinder serviceFinder = new ServiceFinder("openbis", "session_workspace_file_upload");
+        //        serviceFinder.computeServerUrl()
+
+        Objects.requireNonNull(sessionToken);
+
+        try
+        {
+            final File file = fileOrFolder.toFile();
+
+            if (!file.isFile())
+            {
+                throw new UserFailureException("File must be a file for now.");
+            }
+
+            final String fileName = file.getName();
+
+            final String response = request("POST", new URI(dssURL + "/session_workspace_file_upload"),
+                    Map.of(
+                            "sessionID", sessionToken,
+                            "filename", fileName,
+                            "id", "0",
+                            "startByte", "0",
+                            "endByte", String.valueOf(file.length()),
+                            "size", String.valueOf(file.length())
+                    ), Files.readAllBytes(file.toPath()));
+            System.out.println(response);
+
+            @SuppressWarnings("unchecked")
+            final Map<String, String> values = new ObjectMapper().readValue(response, Map.class);
+
+            return values.get("uploadID");
+        } catch (Exception e)
+        {
+            throw new RuntimeException(e);
+        }
     }
 
-    public DataSetPermId createUploadedDataSet(UploadedDataSetCreation newDataSet) {
+    public DataSetPermId createUploadedDataSet(final UploadedDataSetCreation newDataSet) {
         return dssFacade.createUploadedDataSet(sessionToken, newDataSet);
     }
 
+    @SuppressWarnings({ "OptionalGetWithoutIsPresent", "unchecked" })
+    private String request(final String httpMethod, final URI serverUri,
+            final Map<String, String> parameters, final byte [] body) throws Exception {
+        HttpClient client = HttpClient.newBuilder()
+                .version(HttpClient.Version.HTTP_1_1)
+                .followRedirects(HttpClient.Redirect.NORMAL)
+                .connectTimeout(Duration.ofMillis(timeout))
+                .build();
+
+        final String query = parameters.entrySet().stream()
+                .map(entry -> urlEncode(entry.getKey()) + "=" + urlEncode(entry.getValue()))
+                .reduce((s1, s2) -> s1 + "&" + s2).get();
+
+        final URI uri = new URI(serverUri.getScheme(), null, serverUri.getHost(), serverUri.getPort(),
+                serverUri.getPath(), query, null);
+
+        final HttpRequest.Builder builder = HttpRequest.newBuilder()
+                .uri(uri)
+                .version(HttpClient.Version.HTTP_1_1)
+                .timeout(Duration.ofMillis(timeout))
+                .method(httpMethod, HttpRequest.BodyPublishers.ofByteArray(body));
+
+        final HttpRequest request = builder.build();
+
+        final HttpResponse<String> httpResponse = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+        final int statusCode = httpResponse.statusCode();
+        if (statusCode >= 200 && statusCode < 300) {
+            return httpResponse.body();
+        } else if (statusCode >= 400 && statusCode < 500) {
+            throw new UserFailureException("User failure. Received status code: " + statusCode + ". Body: " +
+                    new String(httpResponse.body()));
+        } else if (statusCode >= 500 && statusCode < 600) {
+            throw new RuntimeException("Server failure. Received status code: " + statusCode);
+        } else {
+            throw new RuntimeException("Unknown failure. Received status code: " + statusCode);
+        }
+    }
+
+    private static String urlEncode(final String s) {
+        return URLEncoder.encode(s, StandardCharsets.UTF_8);
+    }
+
 }