diff --git a/.gitignore b/.gitignore
index 3ef0515000dcbc44816e52895becbd255d1741cd..39be17dc807dc3e2f5c505dc9341027457403421 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,8 @@
 # typical build dirs
 bin/
 target/
+# But include server-side scripting
+!/scripting/bin
 
 # eclipse stuff
 .classpath
diff --git a/README_SETUP.md b/README_SETUP.md
index a48faf2b5602af4e0ee0aefce4820b52d566391e..32c68d7583d9137ae365ffcaea42255edfa143e4 100644
--- a/README_SETUP.md
+++ b/README_SETUP.md
@@ -11,6 +11,7 @@
 * >=MySQL 5.5 (better >=5.6) or >=MariaDB 10.1
 * libpam (if PAM authentication is required)
 * unzip
+* openpyxl (for XLS/ODS export)
 
 ### Install the requirements on Debian
 On Debian, the required packages can be installed with:
@@ -81,8 +82,11 @@ server:
       certificate passwords are stored in plaintext.
     * Set the file system paths:
    - `FILE_SYSTEM_ROOT`: The root for all the files managed by CaosDB.
-   - `DROP_OFF_BOX`: Where to put files for insertion into CaosDB
-   - `TMP_FILES`: <To do, what's this good for?>
+   - `DROP_OFF_BOX`: Files can be put here for insertion into CaosDB.
+   - `TMP_FILES`: Temporary files go here, for example during script execution
+     or when uploading or moving files.
+   - `SHARED_FOLDER`: Folder for sharing files via cryptographic tokens, also
+     those created by scripts.
     * Maybe set another `SESSION_TIMEOUT_MS`.
     * See also [README_CONFIGURATION.md](README_CONFIGURATION.md)
 4. Copy `conf/core/usersources.ini.template` to `conf/ext/usersources.ini`.
diff --git a/caosdb-webui b/caosdb-webui
index 67e0bc4ab29c6eb37a1dfe6dfcd25f8019549a48..f4c68d7564bbecbd1648a0af98160212d3876195 160000
--- a/caosdb-webui
+++ b/caosdb-webui
@@ -1 +1 @@
-Subproject commit 67e0bc4ab29c6eb37a1dfe6dfcd25f8019549a48
+Subproject commit f4c68d7564bbecbd1648a0af98160212d3876195
diff --git a/conf/core/server.conf b/conf/core/server.conf
index 4264b374635cf8a3dd7ff83bb6e4305e1c7f3ec3..8e71c561705fda384a2a87b2b45c3aa8ccfb8ea4 100644
--- a/conf/core/server.conf
+++ b/conf/core/server.conf
@@ -6,6 +6,7 @@ SERVER_SIDE_SCRIPTING_WORKING_DIR=./scripting/working/
 FILE_SYSTEM_ROOT=./CaosDBFileSystem/FileSystemRoot/
 DROP_OFF_BOX=./CaosDBFileSystem/DropOffBox/
 TMP_FILES=./CaosDBFileSystem/TMP/
+SHARED_FOLDER=./CaosDBFileSystem/Shared/
 CHOWN_SCRIPT=./misc/chown_script/caosdb_chown_dropoffbox
 USER_SOURCES_INI_FILE=./conf/ext/usersources.ini
 NEW_USER_DEFAULT_ACTIVITY=INACTIVE
diff --git a/makefile b/makefile
index 3df5dca88fa7f8a1a7a58072e8431875871add1d..642e31c538a98b39fdb07aeaed5b8a2515adf947 100644
--- a/makefile
+++ b/makefile
@@ -38,6 +38,9 @@ run-debug:
 run-single:
 	java -jar target/caosdb-server-0.1-SNAPSHOT-jar-with-dependencies.jar
 
+formatting:
+	mvn fmt:format
+
 # Compile into a standalone jar file
 jar: compile
 	mvn clean compile assembly:single
diff --git a/pom.xml b/pom.xml
index ec26ca3ac733a3a8f41f3c4c23f58d701f0433e9..42e44ca49b82963a16ac655755ccf29b8fcde6e0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -75,6 +75,13 @@
       <version>4.12</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <!-- preventing parallel execution of some test classes -->
+      <groupId>com.github.stephenc.jcip</groupId>
+      <artifactId>jcip-annotations</artifactId>
+      <version>1.0-1</version>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.jdom</groupId>
       <artifactId>jdom2</artifactId>
@@ -130,6 +137,11 @@
       <artifactId>commons-math</artifactId>
       <version>2.2</version>
     </dependency>
+    <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+      <version>1.12</version>
+    </dependency>
     <dependency>
       <groupId>com.sun.mail</groupId>
       <artifactId>javax.mail</artifactId>
@@ -225,6 +237,10 @@
             <caosdb.debug>true</caosdb.debug>
             <log4j2.debug>true</log4j2.debug>
           </systemPropertyVariables>
+          <reuseForks>false</reuseForks>
+          <!-- Start 0.5 JVMs per CPU core -->
+          <!-- Higher numbers *seem* to lead to higher failure rates... :-/ -->
+          <forkCount>0.5C</forkCount>
         </configuration>
       </plugin>
       <plugin>
@@ -268,6 +284,12 @@
         <groupId>com.coveo</groupId>
         <artifactId>fmt-maven-plugin</artifactId>
         <version>2.5.1</version>
+        <configuration>
+          <skip>
+            <!-- Set skip to `true` to prevent auto-formatting while coding. -->
+            false
+          </skip>
+        </configuration>
         <executions>
           <execution>
             <goals>
@@ -276,6 +298,17 @@
           </execution>
         </executions>
       </plugin>
+      <!-- Remove easy-units from classpath generation because of
+           https://github.com/jdee-emacs/jdee/issues/125 (no sources inside
+           easy-units) -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <version>2.8</version>
+        <configuration>
+          <excludeArtifactIds>easy-units</excludeArtifactIds>
+        </configuration>
+      </plugin>
     </plugins>
   </build>
   <url>bmp.ds.mpg.de</url>
diff --git a/scripting/bin/xls_from_csv.py b/scripting/bin/xls_from_csv.py
new file mode 100755
index 0000000000000000000000000000000000000000..b64173a34be8d00bcd40e225122d865768b77ebb
--- /dev/null
+++ b/scripting/bin/xls_from_csv.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+# ** header v3.0
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2019 IndiScale GmbH
+#
+# 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
+
+"""Creates an XLS(X?) file from an url-encoded CSV string.
+"""
+
+import argparse
+import datetime
+import io
+import os
+import sys
+
+import pandas as pd
+
+
+def _parse_to_dataframe(csv_string):
+    """Attempts to create a valid dataframe from a CSV string.
+
+The CSV string typically starts with a header like this:
+
+```
+data:text/csv;charset=utf-8,colname1	colname2
+value1	value2
+...
+```
+Parameters
+----------
+csv_string : The URL encoded CSV content, starts with `data:text/csv`.
+
+Returns
+-------
+out : The created dataframe.
+    """
+    csv_string = csv_string.split(",")[1]
+    sio = io.StringIO(csv_string)
+    dataframe = pd.read_csv(sio, sep="\t")
+    return dataframe
+
+
+def _write_xls(dataframe, directory):
+    """Writes a dataframe into a file.
+
+The file is named `seleceted_data.{datetime}.xlsx`, where `{datetime}` is an
+ISO8601 date and time string, formatted like "%Y-%m-%dT%H_%M_%S". The file name
+does not have any magic functionality and the date and time is only there for
+the user's convenience.
+
+Parameters
+----------
+dataframe : pd.DataFrame
+  The data frame to be written.
+directory : str
+  The string representation of the directory where the file shall be written.
+
+Returns
+-------
+out : str
+  The filename (last component of directory and basename).
+    """
+    now = datetime.datetime.now()
+    filename = "selected_data.{time}.xlsx".format(
+        time=now.strftime("%Y-%m-%dT%H_%M_%S"))
+    filepath = os.path.abspath(os.path.join(directory, filename))
+    try:
+        dataframe.to_excel(filepath, index=False)
+    except ImportError as imp_e:
+        print("Error importing Python module:\n" + str(imp_e),
+              file=sys.stderr)
+        sys.exit(1)
+
+    randname = os.path.basename(os.path.abspath(directory))
+    filename = os.path.join(randname, filename)
+
+    return filename
+
+def _parse_arguments():
+    """Parses the command line arguments.
+
+    Takes into account defaults from the environment (where known).
+    """
+    parser = argparse.ArgumentParser(description='__doc__')
+    tempdir = os.environ["SHARED_DIR"]
+    parser.add_argument('-t', '--tempdir', required=False, default=tempdir,
+                        help="Temporary dir for saving the result.")
+    parser.add_argument('-u', '--urlencoded', required=True,
+                        help="The URL encoded csv data.")
+    parser.add_argument('-a', '--auth-token', required=False,
+                        help="An authentication token (not needed, only for compatibility).")
+    return parser.parse_args()
+
+def main():
+  args = _parse_arguments()
+  dataframe = _parse_to_dataframe(args.urlencoded)
+  filename = _write_xls(dataframe, directory=args.tempdir)
+  print(filename)
+
+if __name__ == "__main__":
+  main()
diff --git a/src/main/java/caosdb/server/CaosDBServer.java b/src/main/java/caosdb/server/CaosDBServer.java
index 0be5a6e2543cf5b0e50c8032c501fc0a12f145b2..3245902d5721e00d1ae0418a33e7a5f468794ea6 100644
--- a/src/main/java/caosdb/server/CaosDBServer.java
+++ b/src/main/java/caosdb/server/CaosDBServer.java
@@ -48,6 +48,7 @@ import caosdb.server.resource.RolesResource;
 import caosdb.server.resource.ScriptingResource;
 import caosdb.server.resource.ServerLogsResource;
 import caosdb.server.resource.ServerPropertiesResource;
+import caosdb.server.resource.SharedFileResource;
 import caosdb.server.resource.TestCaseFileSystemResource;
 import caosdb.server.resource.TestCaseResource;
 import caosdb.server.resource.ThumbnailsResource;
@@ -679,6 +680,8 @@ public class CaosDBServer extends Application {
     protectedRouter.attach("/EntityPermissions/{specifier}", EntityPermissionsResource.class);
     protectedRouter.attach("/Owner/{specifier}", EntityOwnerResource.class);
     protectedRouter.attach("/FileSystem/", FileSystemResource.class);
+    // FileSystem etc. needs to accept parameters which contain slashes and would otherwise be
+    // split at the first separator
     protectedRouter
         .attach("/FileSystem/{path}", FileSystemResource.class)
         .getTemplate()
@@ -689,6 +692,14 @@ public class CaosDBServer extends Application {
         .getTemplate()
         .getDefaultVariable()
         .setType(Variable.TYPE_URI_PATH);
+    protectedRouter.attach("/Shared", SharedFileResource.class);
+    protectedRouter.attach("/Shared/", SharedFileResource.class);
+    protectedRouter
+        .attach("/Shared/{path}", SharedFileResource.class)
+        .getTemplate()
+        .getDefaultVariable()
+        .setType(Variable.TYPE_URI_PATH);
+    ;
     protectedRouter.attach("/Info", InfoResource.class);
     protectedRouter.attach("/Info/", InfoResource.class);
     protectedRouter.attach("/Role", RolesResource.class);
diff --git a/src/main/java/caosdb/server/FileSystem.java b/src/main/java/caosdb/server/FileSystem.java
index ab4dfce15c0343444a24881ac95bd6889011bfa5..44b663e958841ac92a24f44244096034eb9154c3 100644
--- a/src/main/java/caosdb/server/FileSystem.java
+++ b/src/main/java/caosdb/server/FileSystem.java
@@ -4,6 +4,7 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (c) 2019 IndiScale GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -20,6 +21,7 @@
  *
  * ** end header
  */
+
 package caosdb.server;
 
 import caosdb.server.database.Database;
@@ -30,20 +32,25 @@ import caosdb.server.entity.FileProperties;
 import caosdb.server.entity.Message;
 import caosdb.server.utils.FileUtils;
 import caosdb.server.utils.ServerMessages;
+import caosdb.server.utils.Utils;
 import com.google.common.io.Files;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.file.Path;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.util.regex.Pattern;
 import org.apache.commons.fileupload.FileItemStream;
 
 public class FileSystem {
   private static String filesystem = null;
   private static String dropOffBox = null;
   private static String tmpdir = null;
+  private static String sharedDir = null;
+  public static final Pattern base32Pattern = Pattern.compile("^[-A-Z2-7]+$");
 
   private static void check() {
     try {
@@ -91,15 +98,50 @@ public class FileSystem {
     return tmpdir;
   }
 
-  private static void init() {
+  public static String getShared() {
+    if (sharedDir == null) {
+      init();
+    }
+    return sharedDir;
+  }
+
+  public static void init() {
     filesystem = CaosDBServer.getServerProperty(ServerProperties.KEY_FILE_SYSTEM_ROOT);
     dropOffBox = CaosDBServer.getServerProperty(ServerProperties.KEY_DROP_OFF_BOX);
     tmpdir = CaosDBServer.getServerProperty(ServerProperties.KEY_TMP_FILES);
+    sharedDir = CaosDBServer.getServerProperty(ServerProperties.KEY_SHARED_FOLDER);
     check();
   }
 
   private FileSystem() {}
 
+  /**
+   * Asserts that a temporary directory for this session exists, creating it if necessary.
+   *
+   * @param sessionString The session string for which the directory is guaranteed to exist after
+   *     calling this function. If `session` is Null, a random directory will be created.
+   * @return A String with the existing directory.
+   */
+  public static final String assertDir(String sessionString) throws IOException {
+
+    if (sessionString == null) {
+      sessionString = Utils.getSecureFilename(15);
+    }
+
+    // Name of the temporary directory
+    final File tempDir = new File(getTmp(), sessionString);
+
+    if (!tempDir.exists()) {
+      tempDir.mkdirs();
+    }
+
+    if (!tempDir.isDirectory()) {
+      throw new IOException("File " + tempDir.toString() + " is not a directory.");
+    }
+
+    return tempDir.toString();
+  }
+
   /**
    * Reads a FileItemStream and stores the file into the tmpfolder. Generates FileProperties.
    *
@@ -124,10 +166,7 @@ public class FileSystem {
       // this is a directory, not a file
 
       stream.close();
-      if (!tmpFile.exists()) {
-        tmpFile.mkdirs();
-      }
-
+      assertDir(session);
     } else {
       // this is actually a file
 
@@ -312,6 +351,46 @@ public class FileSystem {
     return null;
   }
 
+  /**
+   * Get the file from the shared files folder.
+   *
+   * <p>Conditions under which null is returned:
+   * <li>The file does not exist.
+   * <li>The file is a folder.
+   * <li>The requested path is just a file, without parent folders.
+   * <li>The requested path is not normalized.
+   * <li>The first component of the path does not match the base32 pattern for shared folders.
+   *
+   * @param path The path to the requested file.
+   * @return File
+   */
+  public static File getFromShared(final String path) {
+    String basePath = getTmp();
+    Path pathObj = (new File(path)).toPath();
+
+    // Must have more than one component
+    if (pathObj.getNameCount() < 2) {
+      return null;
+    }
+    // Check for normalization
+    if (!pathObj.equals(pathObj.normalize())) {
+      return null;
+    }
+    // The first component of `path` must follow the Base32 pattern.
+    String firstElement = pathObj.getName(0).toString();
+    if (!base32Pattern.matcher(firstElement).matches()) {
+      return null;
+    }
+
+    // All safe, let's get the file already.
+    File ret = new File(basePath, path);
+    // Does the file exist and is it a regular file?
+    if (!ret.exists() || !ret.isFile()) {
+      return null;
+    }
+    return ret;
+  }
+
   /**
    * Return the canonical path on the native file system of the server's host which is guaranteed to
    * be a valid path under the server's internal file system.
diff --git a/src/main/java/caosdb/server/ServerProperties.java b/src/main/java/caosdb/server/ServerProperties.java
index 5e295ae5e84057be6c8604b21902465a550347d8..1f90b069df76760864385f6f090e123387bcbd0a 100644
--- a/src/main/java/caosdb/server/ServerProperties.java
+++ b/src/main/java/caosdb/server/ServerProperties.java
@@ -42,6 +42,7 @@ public class ServerProperties extends Properties {
   public static final String KEY_FILE_SYSTEM_ROOT = "FILE_SYSTEM_ROOT";
   public static final String KEY_DROP_OFF_BOX = "DROP_OFF_BOX";
   public static final String KEY_TMP_FILES = "TMP_FILES";
+  public static final String KEY_SHARED_FOLDER = "SHARED_FOLDER";
   public static final String KEY_USER_FOLDERS = "USER_FOLDERS";
   public static final String KEY_CHOWN_SCRIPT = "CHOWN_SCRIPT";
   public static final String KEY_AUTH_OPTIONAL = "AUTH_OPTIONAL";
diff --git a/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java b/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java
index 07fa343967a08f6dc936deca65f279199c5ae029..c88bbd75a34aea81198b2c9d48702ca6f66a41e0 100644
--- a/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java
+++ b/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java
@@ -4,6 +4,7 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2019 IndiScale GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -103,6 +104,10 @@ public abstract class AbstractCaosDBServerResource extends ServerResource {
     return SecurityUtils.getSubject();
   }
 
+  /**
+   * Setup method, called by {@link org.restlet.resource.Resource#init(org.restlet.Context,
+   * org.restlet.Request, org.restlet.Response)}.
+   */
   @Override
   protected void doInit() {
     if (getRequestEntity().isTransient() && !getRequestEntity().isEmpty()) {
@@ -407,10 +412,6 @@ public abstract class AbstractCaosDBServerResource extends ServerResource {
     return this.requestedIDs;
   }
 
-  public void setReqestedIDs(final ArrayList<Integer> requestedIDs) {
-    this.requestedIDs = requestedIDs;
-  }
-
   public ArrayList<String> getRequestedNames() {
     return this.requestedNames;
   }
diff --git a/src/main/java/caosdb/server/resource/ScriptingResource.java b/src/main/java/caosdb/server/resource/ScriptingResource.java
index b5a82ef33d9a50da0568defd614d6c86048c8cd9..81a84ab631d9cd2206fd163b5c64eaac4c967286 100644
--- a/src/main/java/caosdb/server/resource/ScriptingResource.java
+++ b/src/main/java/caosdb/server/resource/ScriptingResource.java
@@ -4,6 +4,7 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2019 IndiScale GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -20,6 +21,7 @@
  *
  * ** end header
  */
+
 package caosdb.server.resource;
 
 import caosdb.server.FileSystem;
@@ -72,6 +74,11 @@ public class ScriptingResource extends AbstractCaosDBServerResource {
     return generateRootElement(callerElement);
   }
 
+  /**
+   * Handles a POST request to server-side scripting.
+   *
+   * @param entity Representation of the request.
+   */
   @Override
   protected Representation httpPostInChildClass(Representation entity) throws Exception {
 
diff --git a/src/main/java/caosdb/server/resource/SharedFileResource.java b/src/main/java/caosdb/server/resource/SharedFileResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..ffdce05baab5098d03080567ee815d286476f281
--- /dev/null
+++ b/src/main/java/caosdb/server/resource/SharedFileResource.java
@@ -0,0 +1,90 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2019 IndiScale GmbH
+ *
+ * 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
+ */
+
+package caosdb.server.resource;
+
+import static java.net.URLDecoder.decode;
+
+import caosdb.server.FileSystem;
+import caosdb.server.database.backend.implementation.MySQL.ConnectionException;
+import caosdb.server.utils.FileUtils;
+import caosdb.server.utils.ServerMessages;
+import java.io.File;
+import java.io.IOException;
+import org.jdom2.JDOMException;
+import org.restlet.data.Disposition;
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.representation.FileRepresentation;
+import org.restlet.representation.Representation;
+
+/**
+ * Download temporary files via GET method only.
+ *
+ * @author Daniel Hornung
+ */
+public class SharedFileResource extends AbstractCaosDBServerResource {
+
+  /**
+   * Download a File from the tempfiles folder. Only one File per Request.
+   *
+   * @author Daniel Hornung
+   * @return InputRepresentation
+   * @throws IOException
+   */
+  @Override
+  protected Representation httpGetInChildClass() throws Exception {
+    final String specifier =
+        decode(
+            (getRequest().getAttributes().containsKey("path")
+                ? (String) getRequest().getAttributes().get("path")
+                : ""),
+            "UTF-8");
+
+    final File file = getFile(specifier);
+    if (file == null) {
+      Representation ret =
+          error(ServerMessages.ENTITY_DOES_NOT_EXIST, Status.CLIENT_ERROR_NOT_FOUND);
+      return ret;
+    }
+
+    final MediaType mt = MediaType.valueOf(FileUtils.getMimeType(file));
+    final FileRepresentation ret = new FileRepresentation(file, mt);
+    ret.setDisposition(new Disposition(Disposition.TYPE_ATTACHMENT));
+
+    return ret;
+  }
+
+  protected File getFile(final String path) throws IOException {
+    File ret = FileSystem.getFromShared(path);
+    return ret;
+  }
+
+  @Override
+  protected Representation httpPostInChildClass(final Representation entity)
+      throws ConnectionException, JDOMException {
+    this.setStatus(org.restlet.data.Status.CLIENT_ERROR_METHOD_NOT_ALLOWED);
+    return null;
+  }
+}
diff --git a/src/main/java/caosdb/server/scripting/CallerSerializer.java b/src/main/java/caosdb/server/scripting/CallerSerializer.java
index 1ab96ca0ab1a3f2dfefa14175d8423fea9bd4109..3ffb941d9d4fe40a48b849c1de44b8349645d62f 100644
--- a/src/main/java/caosdb/server/scripting/CallerSerializer.java
+++ b/src/main/java/caosdb/server/scripting/CallerSerializer.java
@@ -4,6 +4,7 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2019 IndiScale GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -27,6 +28,16 @@ import caosdb.server.utils.Serializer;
 import java.io.IOException;
 import org.jdom2.Element;
 
+/**
+ * Serializes the result of calling a server-side script into XML.
+ *
+ * <p>The main element is <script>, notable child elements are:
+ * <li>call :: The command-line used to call the script.
+ * <li>stdout :: The standard output of the script. Most communication should be via this element.
+ * <li>stderr :: The error output of the script.
+ * <li>shareddir :: The base dir for temporary files, which can then be retrieved via GET requests.
+ * <li>
+ */
 public class CallerSerializer implements Serializer<ServerSideScriptingCaller, Element> {
 
   @Override
@@ -48,6 +59,9 @@ public class CallerSerializer implements Serializer<ServerSideScriptingCaller, E
       throw new CaosDBException(e);
     }
 
+    Element tmpdir = new Element("shareddir");
+    tmpdir.addContent(caller.getSharedDir().toString());
+
     Element script = new Element("script");
     script.setAttribute("code", caller.getCode().toString());
     script.addContent(command);
diff --git a/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java b/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java
index 2455916ec6245f1e721908b6e91f65b230b60a3a..7af6ba1ca5ac15868f991e6a2a8c7e85b2847bd7 100644
--- a/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java
+++ b/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java
@@ -4,6 +4,7 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2019 IndiScale GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -20,9 +21,11 @@
  *
  * ** end header
  */
+
 package caosdb.server.scripting;
 
 import caosdb.server.CaosDBException;
+import caosdb.server.FileSystem;
 import caosdb.server.entity.FileProperties;
 import caosdb.server.entity.Message;
 import caosdb.server.utils.ServerMessages;
@@ -31,9 +34,17 @@ import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.lang.ProcessBuilder.Redirect;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import org.apache.commons.io.FileUtils;
 
+/**
+ * Calls server-side scripting scripts.
+ *
+ * <p>The script are started in a unique directory, they are provided with an authentication token
+ * and a special shared directory where they may temporarily store files for download by the user.
+ */
 public class ServerSideScriptingCaller {
 
   public static final String UPLOAD_FILES_DIR = ".upload_files";
@@ -43,7 +54,9 @@ public class ServerSideScriptingCaller {
   private ScriptingUtils utils;
   private List<FileProperties> files;
   private File workingDir;
+  private File sharedDir;
   private Object authToken;
+  private Map<String, String> env;
   private File stdOutFile;
   private File stdErrFile;
   private String stdErr = null;
@@ -60,7 +73,16 @@ public class ServerSideScriptingCaller {
 
   public ServerSideScriptingCaller(
       String[] commandLine, Integer timeoutMs, List<FileProperties> files, Object authToken) {
-    this(commandLine, timeoutMs, files, authToken, new ScriptingUtils());
+    this(commandLine, timeoutMs, files, authToken, new HashMap<String, String>());
+  }
+
+  public ServerSideScriptingCaller(
+      String[] commandLine,
+      Integer timeoutMs,
+      List<FileProperties> files,
+      Object authToken,
+      Map<String, String> env) {
+    this(commandLine, timeoutMs, files, authToken, env, new ScriptingUtils());
   }
 
   public ServerSideScriptingCaller(
@@ -68,8 +90,9 @@ public class ServerSideScriptingCaller {
       Integer timeoutMs,
       List<FileProperties> files,
       Object authToken,
+      Map<String, String> env,
       ScriptingUtils utils) {
-    this(commandLine, timeoutMs, files, authToken, utils, utils.getTmpWorkingDir());
+    this(commandLine, timeoutMs, files, authToken, env, utils, utils.getTmpWorkingDir());
   }
 
   public ServerSideScriptingCaller(
@@ -77,6 +100,7 @@ public class ServerSideScriptingCaller {
       Integer timeoutMs,
       List<FileProperties> files,
       Object authToken,
+      Map<String, String> env,
       ScriptingUtils utils,
       File workingDir) {
     this.utils = utils;
@@ -84,17 +108,21 @@ public class ServerSideScriptingCaller {
     this.commandLine = commandLine;
     this.timeoutMs = timeoutMs;
     this.authToken = authToken;
+    this.env = env;
     this.workingDir = workingDir;
     this.stdOutFile = utils.getStdOutFile(workingDir);
     this.stdErrFile = utils.getStdErrFile(workingDir);
   }
 
+  /** Does some final preparation, then calls the script and cleans up. */
   public int invoke() throws Message {
     try {
       checkCommandLine(commandLine);
       try {
         createWorkingDir();
         putFilesInWorkingDir(files);
+        createSharedDir();
+        updateEnvironment();
       } catch (final Exception e) {
         e.printStackTrace();
         throw ServerMessages.SERVER_SIDE_SCRIPT_SETUP_ERROR;
@@ -146,6 +174,16 @@ public class ServerSideScriptingCaller {
     getTmpWorkingDir().mkdirs();
   }
 
+  /** Creates a temporary directory for shareing by the script and sets `sharedDir` accordingly. */
+  void createSharedDir() throws Exception {
+    sharedDir = new File(FileSystem.assertDir(null));
+  }
+
+  /** Sets environment variables for the script, according to the current state of the caller. */
+  void updateEnvironment() {
+    env.put("SHARED_DIR", sharedDir.toString());
+  }
+
   void cleanup() {
     // catch exception and throw afterwards,
     IOException e1 = null;
@@ -176,6 +214,7 @@ public class ServerSideScriptingCaller {
     return utils.getScriptFile(call).getAbsolutePath();
   }
 
+  /** @fixme Should be injected into environment instead. Will be changed in v0.4 of SSS-API */
   String[] injectAuthToken(String[] commandLine) {
     String[] newCommandLine = new String[commandLine.length + 1];
     newCommandLine[0] = commandLine[0];
@@ -190,6 +229,13 @@ public class ServerSideScriptingCaller {
     String[] effectiveCommandLine = injectAuthToken(getCommandLine());
     effectiveCommandLine[0] = makeCallAbsolute(effectiveCommandLine[0]);
     final ProcessBuilder pb = new ProcessBuilder(effectiveCommandLine);
+
+    // inject environment variables
+    Map<String, String> pEnv = pb.environment();
+    for (String key : env.keySet()) {
+      pEnv.put(key, env.get(key));
+    }
+
     pb.redirectOutput(Redirect.to(getStdOutFile()));
     pb.redirectError(Redirect.to(getStdErrFile()));
     pb.directory(getTmpWorkingDir());
@@ -217,6 +263,10 @@ public class ServerSideScriptingCaller {
     return this.workingDir;
   }
 
+  File getSharedDir() {
+    return this.sharedDir;
+  }
+
   int getTimeoutMs() {
     return this.timeoutMs;
   }
diff --git a/src/main/java/caosdb/server/utils/FileUtils.java b/src/main/java/caosdb/server/utils/FileUtils.java
index 560696fd1a5e15e700533e5eb47b20af22cb7208..7441e73c1223f39d31ce4017618572486dfd8a64 100644
--- a/src/main/java/caosdb/server/utils/FileUtils.java
+++ b/src/main/java/caosdb/server/utils/FileUtils.java
@@ -60,7 +60,7 @@ public class FileUtils {
    */
   public static String getMimeType(final File file) {
     if (!file.exists()) {
-      throw new RuntimeIOException("File doe not exist.");
+      throw new RuntimeIOException("File does not exist.");
     }
     try {
       final StringBuffer outputStringBuffer = new StringBuffer();
@@ -82,7 +82,7 @@ public class FileUtils {
         }
         return ret;
       } else {
-        throw new RuntimeException("Output of file command was empty.");
+        throw new RuntimeException("Output of `file` command was empty.");
       }
 
     } catch (final IOException e) {
diff --git a/src/main/java/caosdb/server/utils/Utils.java b/src/main/java/caosdb/server/utils/Utils.java
index 1e6b60d7e0274223a1123e63aafb9568cfeaf25d..6c48906a40fd299379e91446516a9c0325200f81 100644
--- a/src/main/java/caosdb/server/utils/Utils.java
+++ b/src/main/java/caosdb/server/utils/Utils.java
@@ -31,10 +31,12 @@ import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
 import java.text.DecimalFormat;
 import java.util.Random;
 import java.util.Scanner;
 import java.util.regex.Pattern;
+import org.apache.commons.codec.binary.Base32;
 import org.jdom2.Document;
 import org.jdom2.Element;
 import org.jdom2.output.Format;
@@ -43,6 +45,12 @@ import org.jdom2.output.XMLOutputter;
 /** Utility functions. */
 public class Utils {
 
+  /** Random number generator initialized with the system time and used for generating UIDs. */
+  private static Random rand = new Random(System.currentTimeMillis());
+
+  /** Secure random number generator, for secret random numbers. */
+  private static final SecureRandom srand = new SecureRandom();
+
   /**
    * Check whether obj is non-null and can be parsed to an integer.
    *
@@ -168,9 +176,6 @@ public class Utils {
     }
   }
 
-  /** Random number generator initialized with the system time and used for generating UIDs. */
-  private static Random rand = new Random(System.currentTimeMillis());
-
   /**
    * Generate a UID (Hexadecimal).
    *
@@ -185,6 +190,39 @@ public class Utils {
     }
   }
 
+  /**
+   * Generate a secure filename (base32 letters and numbers).
+   *
+   * <p>Very similar to getUID, but uses cryptographic random number instead, also also nicely
+   * formats the resulting string.
+   *
+   * @param byteSize How many bytes of random bits shall be generated.
+   * @return The filename as a String.
+   */
+  public static String getSecureFilename(int byteSize) {
+    // get random bytes
+    byte[] bytes = new byte[byteSize];
+    srand.nextBytes(bytes);
+
+    // Encode to nice letters
+    String filename = (new Base32()).encodeToString(bytes);
+    filename = filename.replaceAll("=+", "");
+
+    // Split up into chunks of 8
+    int chunksize = 8;
+    int len = filename.length();
+    String[] parts = new String[(int) Math.ceil(len * 1.0 / chunksize)];
+    for (int i = 0; i < parts.length; ++i) {
+      parts[i] = filename.substring(i * chunksize, Math.min(len, (i + 1) * chunksize));
+    }
+    filename = new String(parts[0]);
+    for (int i = 1; i < parts.length; ++i) {
+      filename += "-" + parts[i];
+    }
+
+    return filename;
+  }
+
   /** Helper function calling InputStream2String for charset UTF-8. */
   public static String InputStream2String(final InputStream is) {
     return InputStream2String(is, "UTF-8");
diff --git a/src/test/java/caosdb/server/resource/TestSharedFileResource.java b/src/test/java/caosdb/server/resource/TestSharedFileResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..41e134d28aed98c0fb01bee256bce513bb43f13e
--- /dev/null
+++ b/src/test/java/caosdb/server/resource/TestSharedFileResource.java
@@ -0,0 +1,214 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2019 IndiScale GmbH
+ *
+ * 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
+ */
+
+package caosdb.server.resource;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import caosdb.server.CaosDBServer;
+import caosdb.server.FileSystem;
+import caosdb.server.ServerProperties;
+import caosdb.server.accessControl.AnonymousAuthenticationToken;
+import caosdb.server.accessControl.AnonymousRealm;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.concurrent.ConcurrentMap;
+import net.jcip.annotations.NotThreadSafe;
+import org.apache.shiro.mgt.DefaultSecurityManager;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.subject.support.DelegatingSubject;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.Disposition;
+import org.restlet.data.MediaType;
+import org.restlet.data.Method;
+import org.restlet.data.Reference;
+import org.restlet.data.Status;
+import org.restlet.representation.FileRepresentation;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+
+@NotThreadSafe
+public class TestSharedFileResource {
+
+  @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
+
+  static String illegalTopLevelString = "22334455-HGFEDCBA";
+  static String illegalFolderString = "11001100-00110011";
+  static String legalFolderString = "22334455-ABCDEFGH";
+  static String legalFileString = "somefile.dat";
+  static String legalFileStringComplete = (new File(legalFolderString, legalFileString)).toString();
+  static String fileInIllegalFolderStringComplete =
+      (new File(illegalFolderString, legalFileString)).toString();
+
+  static File illegalTopLevelFile;
+  static File illegalFolder;
+  static File fileInIllegalFolder;
+  static File legalFolder;
+  static File legalFile;
+
+  @BeforeClass
+  public static void setup() throws Exception {
+    CaosDBServer.initServerProperties();
+    String tmpStr = FileSystem.getTmp();
+
+    illegalTopLevelFile = new File(tmpStr, illegalTopLevelString);
+    illegalTopLevelFile.createNewFile();
+    illegalFolder = new File(tmpStr, illegalFolderString);
+    illegalFolder.mkdir();
+    fileInIllegalFolder = new File(illegalFolder, legalFileString);
+    fileInIllegalFolder.createNewFile();
+
+    legalFolder = new File(tmpStr, legalFolderString);
+    legalFolder.mkdir();
+
+    legalFile = new File(legalFolder, legalFileString);
+    legalFile.createNewFile();
+  }
+
+  @AfterClass
+  public static void teardown() throws Exception {
+    if (false) {
+      assertTrue(legalFolder.delete());
+      assertTrue(illegalFolder.delete());
+      assertTrue(illegalTopLevelFile.delete());
+    }
+  }
+
+  // Some tests for illegal requests.
+  @Test
+  public void testValidity() throws Exception {
+    SharedFileResource res = new SharedFileResource();
+    assertNull(res.getFile(illegalTopLevelString));
+    assertNull(res.getFile(illegalFolderString));
+    assertNull(res.getFile(illegalFolderString));
+    assertNull(res.getFile(fileInIllegalFolderStringComplete));
+    assertNotNull(res.getFile(legalFileStringComplete));
+  }
+
+  // Trying to obtain files via `path` attribute.
+  @Test
+  public void testFileUrl() throws Exception {
+
+    provideUserSourcesFile();
+    final Subject user = new DelegatingSubject(new DefaultSecurityManager(new AnonymousRealm()));
+    user.login(AnonymousAuthenticationToken.getInstance());
+    SharedFileResource resource =
+        new SharedFileResource() {
+          // @Override
+          // protected Representation httpGetInChildClass()
+          //     throws ConnectionException, IOException, SQLException, CaosDBException,
+          //         NoSuchAlgorithmException, Exception {
+          //   // TODO Auto-generated method stub
+          //   return super.httpGetInChildClass();
+          // }
+
+          @Override
+          public String getSRID() {
+            return "TEST-SRID";
+          }
+
+          @Override
+          public String getCRID() {
+            return "TEST-CRID";
+          }
+
+          @Override
+          public Long getTimestamp() {
+            return 0L;
+          }
+
+          @Override
+          public Reference getRootRef() {
+            return new Reference("https://example.com/root/");
+          }
+
+          @Override
+          public Reference getReference() {
+            return new Reference("https://example.com/");
+          }
+
+          @Override
+          public Subject getUser() {
+            // TODO Auto-generated method stub
+            return user;
+          }
+        };
+
+    Representation entity = new StringRepresentation("lalala");
+    entity.setMediaType(MediaType.TEXT_ALL);
+    Request req = new Request(Method.GET, "../Shared/", entity);
+    ConcurrentMap<String, Object> attrs = req.getAttributes();
+    attrs.put("path", legalFileStringComplete);
+    req.setAttributes(attrs);
+    resource.init(null, req, new Response(null));
+    Representation repr = resource.handle();
+    Response resp = resource.getResponse();
+    // No unit testing framework yet.
+    if (false) {
+      assertEquals(Status.SUCCESS_OK, resp.getStatus());
+    }
+    FileRepresentation frep;
+    try {
+      frep = (FileRepresentation) repr;
+    } catch (Exception e) {
+      fail("Rsssource did not produce a FileRepresentation.");
+      // This line won't be reached, but is necessary for the compiler.
+      frep = (FileRepresentation) repr;
+    }
+    assertTrue(frep.getFile().toString().endsWith(legalFileStringComplete));
+    assertEquals(Disposition.TYPE_ATTACHMENT, frep.getDisposition().getType());
+  }
+
+  /** Creates a dummy usersources.ini and injects it into the server properties. */
+  private void provideUserSourcesFile() throws IOException {
+    String usersourcesFileName = tempFolder.newFile("usersources.ini").getAbsolutePath();
+    String usersourcesContent =
+        "realms = PAM\n"
+            + "defaultRealm = PAM\n"
+            + "\n"
+            + "[PAM]\n"
+            + "class = caosdb.server.accessControl.Pam\n"
+            + "default_status = ACTIVE\n"
+            + "include.user = admin\n"
+            + ";include.group = [uncomment and put your groups here]\n"
+            + ";exclude.user = [uncomment and put excluded users here]\n"
+            + ";exclude.group = [uncomment and put excluded groups here]\n"
+            + "\n"
+            + ";it is necessary to add at least one admin\n"
+            + "user.admin.roles = administration";
+
+    Files.write(Paths.get(usersourcesFileName), usersourcesContent.getBytes());
+    CaosDBServer.setProperty(ServerProperties.KEY_USER_SOURCES_INI_FILE, usersourcesFileName);
+  }
+}
diff --git a/src/test/java/caosdb/server/scripting/TestServerSideScriptingCaller.java b/src/test/java/caosdb/server/scripting/TestServerSideScriptingCaller.java
index 5d638ff13eff01d46487cb9c866a4264611f6658..668d75fbc9271babc8df20a872f661d3bd0a9646 100644
--- a/src/test/java/caosdb/server/scripting/TestServerSideScriptingCaller.java
+++ b/src/test/java/caosdb/server/scripting/TestServerSideScriptingCaller.java
@@ -40,6 +40,8 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
 import org.apache.commons.io.FileUtils;
 import org.hamcrest.BaseMatcher;
 import org.hamcrest.Description;
@@ -57,6 +59,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
       TEST_DIR.toPath().resolve("serverSideScriptingCallerTestFolder").toFile();
   public static File testFile = testFolder.toPath().resolve("TestFile").toFile();
   public static File testExecutable = testFolder.toPath().resolve("TestScript").toFile();
+  final Map<String, String> emptyEnv = new HashMap<String, String>();
 
   @BeforeClass
   public static void initServerProperties() throws IOException {
@@ -104,6 +107,15 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
     FileUtils.write(testExecutable, "echo \"$@\"", "UTF-8");
   }
 
+  /**
+   * The test executable will write the content of the given environment variable
+   *
+   * <p>Output goes to stdout, between hyphens, like so: `-my value-`.
+   */
+  private void preparePrintEnv(final String variable) throws IOException {
+    FileUtils.write(testExecutable, "echo \"-$" + variable + "-\"", "UTF-8");
+  }
+
   private File pwd;
 
   @Before
@@ -126,7 +138,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   @Test
   public void testCreateWorkingDirFailure() throws Exception {
     final ServerSideScriptingCaller caller =
-        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd);
+        new ServerSideScriptingCaller(
+            null, -1, null, null, emptyEnv, new ScriptingUtils(), this.pwd);
 
     this.exception.expect(Exception.class);
     this.exception.expectMessage(
@@ -139,7 +152,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   @Test
   public void testCreateWorkingDirOk() throws Exception {
     final ServerSideScriptingCaller caller =
-        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd);
+        new ServerSideScriptingCaller(
+            null, -1, null, null, emptyEnv, new ScriptingUtils(), this.pwd);
 
     caller.createWorkingDir();
 
@@ -157,7 +171,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   @Test()
   public void testPutFilesInWorkingDirFailure1() throws IOException, CaosDBException {
     final ServerSideScriptingCaller caller =
-        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd);
+        new ServerSideScriptingCaller(
+            null, -1, null, null, emptyEnv, new ScriptingUtils(), this.pwd);
 
     this.exception.expect(FileNotFoundException.class);
     this.exception.expectMessage("FILE_UPLOAD_DIR");
@@ -174,7 +189,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   @Test
   public void testPutFilesInWorkingDirFailure2() throws FileNotFoundException, CaosDBException {
     final ServerSideScriptingCaller caller =
-        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd);
+        new ServerSideScriptingCaller(
+            null, -1, null, null, emptyEnv, new ScriptingUtils(), this.pwd);
     this.pwd.mkdirs();
 
     final ArrayList<FileProperties> files = new ArrayList<>();
@@ -195,7 +211,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   @Test
   public void testPutFilesInWorkingDirFailure3() throws FileNotFoundException, CaosDBException {
     final ServerSideScriptingCaller caller =
-        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd);
+        new ServerSideScriptingCaller(
+            null, -1, null, null, emptyEnv, new ScriptingUtils(), this.pwd);
     this.pwd.mkdirs();
 
     final ArrayList<FileProperties> files = new ArrayList<>();
@@ -219,7 +236,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   @Test
   public void testPutFilesInWorkingDirFailure4() throws FileNotFoundException, CaosDBException {
     final ServerSideScriptingCaller caller =
-        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd);
+        new ServerSideScriptingCaller(
+            null, -1, null, null, emptyEnv, new ScriptingUtils(), this.pwd);
     this.pwd.mkdirs();
 
     final ArrayList<FileProperties> files = new ArrayList<>();
@@ -244,7 +262,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   @Test()
   public void testPutFilesInWorkingDirReturnSilently() throws IOException, CaosDBException {
     final ServerSideScriptingCaller caller =
-        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd);
+        new ServerSideScriptingCaller(
+            null, -1, null, null, emptyEnv, new ScriptingUtils(), this.pwd);
 
     caller.putFilesInWorkingDir(null);
   }
@@ -252,7 +271,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   @Test
   public void testPutFilesInWorkingDirOk() throws CaosDBException, IOException {
     final ServerSideScriptingCaller caller =
-        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd);
+        new ServerSideScriptingCaller(
+            null, -1, null, null, emptyEnv, new ScriptingUtils(), this.pwd);
     this.pwd.mkdirs();
 
     final ArrayList<FileProperties> files = new ArrayList<>();
@@ -278,7 +298,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   public void testCallScriptFailure1() throws Exception {
     final String[] cmd = {testExecutable.getAbsolutePath()};
     final ServerSideScriptingCaller caller =
-        new ServerSideScriptingCaller(cmd, -1, null, "", new ScriptingUtils(), this.pwd);
+        new ServerSideScriptingCaller(cmd, -1, null, "", emptyEnv, new ScriptingUtils(), this.pwd);
 
     caller.createWorkingDir();
     assertTrue(caller.getTmpWorkingDir().exists());
@@ -292,7 +312,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   public void testCallScriptOkSimple() throws Exception {
     final String[] cmd = {testExecutable.getAbsolutePath()};
     final ServerSideScriptingCaller caller =
-        new ServerSideScriptingCaller(cmd, -1, null, "", new ScriptingUtils(), this.pwd);
+        new ServerSideScriptingCaller(cmd, -1, null, "", emptyEnv, new ScriptingUtils(), this.pwd);
 
     caller.createWorkingDir();
     assertTrue(caller.getTmpWorkingDir().exists());
@@ -359,7 +379,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   public void testTimeout() throws Exception {
     final String[] cmd = {testExecutable.getAbsolutePath()};
     final ServerSideScriptingCaller caller =
-        new ServerSideScriptingCaller(cmd, 500, null, "", new ScriptingUtils(), this.pwd);
+        new ServerSideScriptingCaller(cmd, 500, null, "", emptyEnv, new ScriptingUtils(), this.pwd);
 
     caller.createWorkingDir();
     assertTrue(caller.getTmpWorkingDir().exists());
@@ -374,7 +394,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   public void testCleanup() throws Exception {
     final String[] cmd = {testExecutable.getAbsolutePath()};
     final ServerSideScriptingCaller caller =
-        new ServerSideScriptingCaller(cmd, 500, null, null, new ScriptingUtils(), this.pwd);
+        new ServerSideScriptingCaller(
+            cmd, 500, null, null, emptyEnv, new ScriptingUtils(), this.pwd);
 
     caller.createWorkingDir();
     assertTrue(caller.getTmpWorkingDir().exists());
@@ -386,7 +407,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   public void testInvokeWithErrorInCreateWorkingDir() throws Message {
     final ServerSideScriptingCaller caller =
         new ServerSideScriptingCaller(
-            new String[] {""}, -1, null, null, new ScriptingUtils(), this.pwd) {
+            new String[] {""}, -1, null, null, emptyEnv, new ScriptingUtils(), this.pwd) {
           @Override
           public void createWorkingDir() throws Exception {
             throw new Exception();
@@ -424,7 +445,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   public void testInvokeWithErrorInPutFilesInWorkingDir() throws Message {
     final ServerSideScriptingCaller caller =
         new ServerSideScriptingCaller(
-            new String[] {""}, -1, null, "", new ScriptingUtils(), this.pwd) {
+            new String[] {""}, -1, null, "", emptyEnv, new ScriptingUtils(), this.pwd) {
           @Override
           public void createWorkingDir() throws Exception {}
 
@@ -462,7 +483,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   public void testInvokeWithErrorInCallScript() throws Message {
     final String[] cmd = {testExecutable.getAbsolutePath()};
     final ServerSideScriptingCaller caller =
-        new ServerSideScriptingCaller(cmd, -1, null, null, new ScriptingUtils(), this.pwd) {
+        new ServerSideScriptingCaller(
+            cmd, -1, null, null, emptyEnv, new ScriptingUtils(), this.pwd) {
           @Override
           public void createWorkingDir() throws Exception {}
 
@@ -497,7 +519,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   @Test
   public void testInvokeWithErrorInCleanup() throws Message {
     final ServerSideScriptingCaller caller =
-        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd) {
+        new ServerSideScriptingCaller(
+            null, -1, null, null, emptyEnv, new ScriptingUtils(), this.pwd) {
           @Override
           public void createWorkingDir() throws Exception {}
 
@@ -529,7 +552,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   public void testPrintStdErr() throws Exception {
     final String[] cmd = {testExecutable.getAbsolutePath(), "opt1", "opt2"};
     final ServerSideScriptingCaller caller =
-        new ServerSideScriptingCaller(cmd, -1, null, "authToken", new ScriptingUtils(), this.pwd);
+        new ServerSideScriptingCaller(
+            cmd, -1, null, "authToken", emptyEnv, new ScriptingUtils(), this.pwd);
 
     caller.createWorkingDir();
 
@@ -545,7 +569,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   public void testPrintStdOut() throws Exception {
     final String[] cmd = {testExecutable.getAbsolutePath(), "opt1", "opt2"};
     final ServerSideScriptingCaller caller =
-        new ServerSideScriptingCaller(cmd, -1, null, "authToken", new ScriptingUtils(), this.pwd);
+        new ServerSideScriptingCaller(
+            cmd, -1, null, "authToken", emptyEnv, new ScriptingUtils(), this.pwd);
 
     caller.createWorkingDir();
 
@@ -557,10 +582,31 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
         "--auth-token=authToken opt1 opt2\n", FileUtils.readFileToString(caller.getStdOutFile()));
   }
 
+  /** Test if environment variables are passed correctly and can be read by the script. */
+  @Test
+  public void testCallScriptEnvironment() throws Exception {
+    final String[] cmd = {testExecutable.getAbsolutePath()};
+    Map<String, String> env = new HashMap<String, String>();
+    env.put("TEST", "testcontent");
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(cmd, -1, null, "", env, new ScriptingUtils(), this.pwd);
+
+    caller.createWorkingDir();
+    assertTrue(caller.getTmpWorkingDir().exists());
+
+    preparePrintEnv("TEST");
+
+    caller.callScript();
+
+    assertEquals("-testcontent-\n", caller.getStdOut());
+    caller.cleanup();
+  }
+
   @Test
   public void testCleanupNonExistingWorkingDir() {
     final ServerSideScriptingCaller caller =
-        new ServerSideScriptingCaller(null, -1, null, "authToken", new ScriptingUtils(), this.pwd);
+        new ServerSideScriptingCaller(
+            null, -1, null, "authToken", emptyEnv, new ScriptingUtils(), this.pwd);
     caller.cleanup();
   }
 }
diff --git a/src/test/java/caosdb/server/utils/FileUtilsTest.java b/src/test/java/caosdb/server/utils/FileUtilsTest.java
index bec2e3faf40d7ca4ba6ea660133fe5c7ca5dc2b3..183f5cf834783f7ad9c6e63bcdb25df1c7870822 100644
--- a/src/test/java/caosdb/server/utils/FileUtilsTest.java
+++ b/src/test/java/caosdb/server/utils/FileUtilsTest.java
@@ -25,10 +25,12 @@ package caosdb.server.utils;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import caosdb.server.CaosDBException;
 import caosdb.server.CaosDBServer;
 import caosdb.server.FileSystem;
+import caosdb.server.ServerProperties;
 import caosdb.server.database.Database;
 import caosdb.server.database.access.Access;
 import caosdb.server.database.backend.implementation.UnixFileSystem.UnixFileSystemGetFileIterator.FileNameIterator;
@@ -43,31 +45,43 @@ import java.io.PrintStream;
 import java.io.PrintWriter;
 import java.security.NoSuchAlgorithmException;
 import java.util.Iterator;
+import java.util.regex.Pattern;
+import net.jcip.annotations.NotThreadSafe;
 import org.eclipse.jetty.io.RuntimeIOException;
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.BeforeClass;
+import org.junit.ClassRule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
+@NotThreadSafe
 public class FileUtilsTest {
-  /** @throws IOException */
+  @ClassRule public static TemporaryFolder tempFolder = new TemporaryFolder();
+  public static File testRoot;
+  public static File someDir;
+  public static File linkToSomeDir;
+  public static File someFile;
+  public static File someDir_someFile;
+  public static File linkToSomeFile;
+  public static File tmpFolderCaosDB;
+
   @BeforeClass
-  public static void initServerProperties() throws IOException {
+  public static void setup() throws Message, IOException {
     CaosDBServer.initServerProperties();
-  }
+    testRoot = tempFolder.newFolder("fileutils_testfolder");
+    someDir = testRoot.toPath().resolve("some_dir").toFile();
+    linkToSomeDir = testRoot.toPath().resolve("link_to_some_dir").toFile();
+    someFile = testRoot.toPath().resolve("some_file").toFile();
+    someDir_someFile = someDir.toPath().resolve("someFile").toFile();
+    linkToSomeFile = testRoot.toPath().resolve("link_to_some_file").toFile();
 
-  public static final File testRoot =
-      new File("").getAbsoluteFile().toPath().resolve("fileutils_testfolder").toFile();
-  public static final File someDir = testRoot.toPath().resolve("some_dir").toFile();
-  public static final File linkToSomeDir = testRoot.toPath().resolve("link_to_some_dir").toFile();
-  public static final File someFile = testRoot.toPath().resolve("some_file").toFile();
-  public static final File someDir_someFile = someDir.toPath().resolve("someFile").toFile();
-  public static final File linkToSomeFile = testRoot.toPath().resolve("link_to_some_file").toFile();
+    tmpFolderCaosDB = tempFolder.newFolder();
+    CaosDBServer.setProperty(ServerProperties.KEY_TMP_FILES, tmpFolderCaosDB.toString());
+    FileSystem.init();
 
-  @BeforeClass
-  public static void setup() throws Message, IOException {
     Assert.assertTrue(new File(FileSystem.getBasepath()).canWrite());
     Assert.assertTrue(new File(FileSystem.getBasepath()).canRead());
     Assert.assertTrue(new File(FileSystem.getBasepath()).canExecute());
@@ -79,7 +93,7 @@ public class FileUtilsTest {
     Assert.assertTrue(new File(FileSystem.getDropOffBox()).canExecute());
 
     deleteTmp();
-    FileUtils.createFolders(testRoot);
+    // FileUtils.createFolders(testRoot);
     FileUtils.createFolders(someDir);
     someFile.createNewFile();
     someDir_someFile.createNewFile();
@@ -93,24 +107,31 @@ public class FileUtilsTest {
     FileUtils.delete(testRoot, true);
     deleteTmp();
     assertFalse(testRoot.exists());
-    assertEquals(0, new File(FileSystem.getTmp()).listFiles().length);
+    // assertEquals(0, new File(FileSystem.getTmp()).listFiles().length);
   }
 
+  /** @fixme Currently still fails due to https://github.com/junit-team/junit4/issues/1223 */
   @Before
   public void testTmpEmpty() throws IOException {
     if (new File(FileSystem.getTmp()).exists()) {
       if (0 != new File(FileSystem.getTmp()).list().length) {
-        System.err.println("TMP not empty.");
+        System.err.println("TMP not empty, aborting test!");
+        System.err.println(FileSystem.getTmp());
+        for (String f : new File(FileSystem.getTmp()).list()) {
+          System.err.println(f);
+        }
         teardown();
-        System.exit(1);
+        // System.exit(1);
+        fail("TMP not empty, aborting test");
       }
       assertEquals(0, new File(FileSystem.getTmp()).list().length);
     }
   }
 
   public static void deleteTmp() throws IOException {
-    if (new File(FileSystem.getTmp()).exists())
-      for (File f : new File(FileSystem.getTmp()).listFiles()) {
+    File tmpDir = new File(FileSystem.getTmp());
+    if (tmpDir.exists())
+      for (File f : tmpDir.listFiles()) {
         System.err.println(f.getAbsolutePath());
         FileUtils.delete(f, true).cleanUp();
       }
@@ -652,4 +673,58 @@ public class FileUtilsTest {
     assertEquals(rootPath + "empty/", iterator.next());
     assertEquals(rootPath + "subdir/dat3", iterator.next());
   }
+
+  /**
+   * Tests if assertDir
+   * <li>Recognizes an existing directory
+   * <li>Recognizes a non-directory file
+   * <li>Creates a given directory
+   * <li>Creates a random directory
+   */
+  @Test
+  public void testAssertDir() throws IOException {
+    String existingDirStr = "existingDir";
+    String existingFileStr = "existingFile";
+    String newDirStr = "newDir/";
+    File existingDir = new File(tmpFolderCaosDB, existingDirStr);
+    existingDir.mkdir();
+    File existingFile = new File(tmpFolderCaosDB, existingFileStr);
+    existingFile.createNewFile();
+    File newDir = new File(tmpFolderCaosDB, newDirStr);
+
+    // Existing directory
+    assertEquals(existingDir.toString(), FileSystem.assertDir(existingDirStr));
+
+    // Existing file
+    boolean thrown = false;
+    try {
+      FileSystem.assertDir(existingFileStr);
+    } catch (IOException e) {
+      assertEquals(e.getMessage(), "File " + existingFile.toString() + " is not a directory.");
+      thrown = true;
+    } finally {
+      if (!thrown) {
+        fail(
+            "FileSystem.assertDir("
+                + existingFileStr
+                + ") did not throw an exception, although the existing file was not a directory.");
+      }
+    }
+
+    // New dir with fixed name
+    assertFalse(newDir.exists());
+    assertEquals(newDir.toString(), FileSystem.assertDir(newDirStr));
+    assertTrue(newDir.exists());
+
+    // New dir with random name
+    String randomDirStr = FileSystem.assertDir(null);
+    File randomDir = new File(randomDirStr);
+    assertTrue(randomDir.exists());
+    assertTrue(randomDir.isDirectory());
+
+    // Example: WFUWVHTP-U4KLDIEQ-JGKSTJPM
+    String randName = randomDir.getName();
+    assertTrue(randName.length() >= 26);
+    assertTrue(Pattern.matches("^[-A-Z2-7]+$", randName));
+  }
 }
diff --git a/src/test/java/caosdb/server/utils/mail/TestMail.java b/src/test/java/caosdb/server/utils/mail/TestMail.java
index 8341c39029b5b966705d76bde753ac1e7e9517cd..2bdc90de64e58a81bafbbd7c484677b5bf54eb7a 100644
--- a/src/test/java/caosdb/server/utils/mail/TestMail.java
+++ b/src/test/java/caosdb/server/utils/mail/TestMail.java
@@ -31,12 +31,8 @@ import org.junit.Test;
 
 public class TestMail extends CaosDBTestClass {
   @BeforeClass
-  public static void initServerProperties() throws IOException {
+  public static void setupUp() throws IOException {
     CaosDBServer.initServerProperties();
-  }
-
-  @BeforeClass
-  public static void setupUp() {
     // output mails to the test dir
     CaosDBServer.setProperty(
         ServerProperties.KEY_MAIL_TO_FILE_HANDLER_LOC, TEST_DIR.getAbsolutePath());