diff --git a/src/core/js/uploader_ext.js b/src/core/js/uploader_ext.js
new file mode 100644
index 0000000000000000000000000000000000000000..c952b47da50d04f1840df556c83e25865a3cad48
--- /dev/null
+++ b/src/core/js/uploader_ext.js
@@ -0,0 +1,196 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+
+"use strict";
+
+// Convert callbacks to promise for async programming
+// Strongly inspired by: https://stackoverflow.com/questions/45052875/how-to-convert-fileentry-to-standard-javascript-file-object-using-chrome-apps-fi/53113059
+async function getFileWithPromise(fileEntry) {
+    try {
+        return await new Promise((success, failure) =>
+            fileEntry.file(success, failure));
+    } catch (error) {
+        console.log(error);
+    }
+}
+
+async function readEntriesWithPromise(dreader) {
+    try {
+        return await new Promise((success, failure) =>
+            dreader.readEntries(success, failure));
+    } catch (error) {
+        console.log(error);
+    }
+}
+
+/**
+ * uploader module
+ */
+var uploader = new function() {
+
+    var walkFiles = async function(el, path) {
+        var lst = [];
+        if (el.isFile) {
+            var file = await getFileWithPromise(el);
+            lst.push({file: file, filename: file.name, path: path});
+        } else if (el.isDirectory) {
+            var dreader = el.createReader();
+            var entries = await readEntriesWithPromise(dreader);
+                
+            for (var i=0; i<entries.length; i++) {
+                var fl = await uploader.walkFiles(entries[i], path + el.name + "/");
+                lst = lst.concat(fl);
+            }
+        }
+        return lst;
+    };
+
+    /**
+       lst is a list of the form:
+       [
+       {file: ...,
+        filename: ...,
+        path: ...},
+        ...
+       ]
+       */
+    var sendFiles = async function (lst, rootpath) {
+        var formData = new FormData();
+        // taken from fileupload.js
+        var request = "<Request>";
+        var index = 1;
+        for (const f of lst) {
+            var identifier = "file" + index;
+            // var identifier = f.path + f.filename;
+            request = request + '<File upload="' + identifier + '" name="' + f.filename + '" path="' + rootpath + f.path + f.filename + '">';
+            // if (typeof atom_par !== "undefined" && atom_par !== "FILE") {
+            //     // add parent
+            //     request = request + '<Parent name="' + atom_par + '" />';
+            // }
+            request = request + '</File>';
+            
+            
+            index++;
+        }
+        request = request + "</Request>";
+        
+        formData.append("FileRepresentation", request);
+
+        var index = 1;
+        for (const f of lst) {
+            var identifier = "file" + index;
+            formData.append(identifier, f.file, identifier);
+            index++;
+        }
+
+        console.log(formData);
+
+        try {
+            var response = await $.ajax({
+                url: connection.getBasePath() + "Entity",
+                method: 'POST',
+                dataType: "xml",
+                contentType: false,
+                processData: false,
+                data: formData,
+            });
+        } catch (error) {
+            if (error.status == 0) {
+                console.log(error);
+            } else if (error.status != null) {
+                throw new Error(
+                    "POST file upload returned with HTTP status " + error.status +
+                        " - " + error.statusText);
+            } else {
+                throw error;
+            }
+        }
+    };
+
+    var process_drop_action = async function(lst) {
+        var fileList = [];
+        for (var i=0; i<lst.length; i++) {
+            var el = lst[i].webkitGetAsEntry();
+            if (el) {
+                var fl = await uploader.walkFiles(el, "");
+                fileList = fileList.concat(fl);
+            }
+        }
+        
+        console.log(fileList);
+        uploader.sendFiles(fileList, "/DataAnalysis/");
+    }
+
+    var setup_drop = function(dropArea) {
+        dropArea.addEventListener("drop", function(ev) {
+            ev.stopPropagation();
+            ev.preventDefault();
+            dropArea.style.border = "1px dotted black";
+            
+            uploader.process_drop_action(ev.dataTransfer.items);
+        }, false);
+        
+        dropArea.addEventListener("dragover", function(ev) {
+            dropArea.style.border = "2px solid black";
+            ev.stopPropagation();
+            ev.preventDefault();
+        });
+        
+        dropArea.addEventListener("dragleave", function(ev) {
+            dropArea.style.border = "1px dotted black";
+            ev.stopPropagation();
+            ev.preventDefault();
+        });
+    };
+
+    var init = function () {
+        var target = $("#top-navbar").find("ul").first();
+        var upload_dropper = $('<li><div id="droparea">Drop files here to upload...</div></li>');
+        upload_dropper[0].style.padding = "12px";
+        upload_dropper[0].style.border = "1px dotted black"
+        $(target).append(upload_dropper);
+
+        uploader.setup_drop(upload_dropper[0]);
+    };
+
+    return {
+        process_drop_action: process_drop_action,
+        sendFiles: sendFiles,
+        setup_drop: setup_drop,
+        walkFiles: walkFiles,
+        init: init
+    };
+}();
+
+
+
+
+
+/**
+ * Add the extensions to the webui.
+ */
+$(document).ready(function() {
+    // if ("${BUILD_MODULE_EXT_BOTTOM_LINE}" == "ENABLED") {
+    caosdb_modules.register(uploader);
+    // }
+});
diff --git a/src/core/xsl/main.xsl b/src/core/xsl/main.xsl
index 4d4df12a667c0c119e69e714ce0078b690f6457d..fd58b1ae6b908cb0e5b41dfdce2176d380de74e4 100644
--- a/src/core/xsl/main.xsl
+++ b/src/core/xsl/main.xsl
@@ -285,6 +285,12 @@
         <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_bookmarks.js')"/>
       </xsl:attribute>
     </xsl:element>
+
+    <xsl:element name="script">
+      <xsl:attribute name="src">
+        <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/uploader_ext.js')"/>
+      </xsl:attribute>
+    </xsl:element>
     <!--JS_EXTENSIONS-->
   </xsl:template>
   <xsl:template name="caosdb-data-container">