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());